diff --git a/src/audio_manager.odin b/src/audio_manager.odin new file mode 100644 index 0000000..449ba38 --- /dev/null +++ b/src/audio_manager.odin @@ -0,0 +1,148 @@ +package game + +import rl "vendor:raylib" + +Fade_State :: enum { + None, + In, + Out, +} + +// setup sound IDs so gameplay code never +// touches raw Raylib sound handles +Sound_ID :: enum { + Ball_Collision, + Score_Goal, + UI_Select, + UI_Confirm, +} + +Audio_Manager :: struct { + sounds: map[Sound_ID]rl.Sound, + music: rl.Music, + music_loaded: bool, + + sfx_vol: f32, // User SFX volume setting + user_music_vol: f32, // User Music volume setting + current_music_vol: f32, // Live interpolated output volume + + // Runtime Fade State + fade_state: Fade_State, + fade_elapsed_sec: f32, + fade_duration_sec: f32, +} +// ------------------------------------------------------------------------------ +// Lifecycle procs +// ------------------------------------------------------------------------------ +audio_init :: proc(audio: ^Audio_Manager) { + rl.InitAudioDevice() + audio.sounds = make(map[Sound_ID]rl.Sound) + + // Load SFX + audio.sfx_vol = 0.5 + audio.sounds[.UI_Select] = rl.LoadSound("assets/audio/ui/select.ogg") + audio.sounds[.Ball_Collision] = rl.LoadSound("assets/audio/gameplay/ballhit.ogg") + + // Load Music Stream + audio.user_music_vol = 0.1 + audio.current_music_vol = 0.0 + audio.fade_state = .None + audio.music = rl.LoadMusicStream("assets/audio/gameplay/music/track_1.wav") + audio.music_loaded = true + rl.SetMusicVolume(audio.music, audio.current_music_vol) +} + +audio_update :: proc(audio: ^Audio_Manager, dt: f32) { + if !audio.music_loaded do return + + // Process Fade if the audio state machine requires + if audio.fade_state != .None { + audio.fade_elapsed_sec += dt + // convert elapsed seconds over target seconds into a 0.0 -> 1.0 percentage + progress_pct := clamp(audio.fade_elapsed_sec / audio.fade_duration_sec, 0.0, 1.0) + + #partial switch audio.fade_state { + case .In: + audio.current_music_vol = rl.Lerp(0.0, audio.user_music_vol, progress_pct) + // progress is 100% or more stop fading in music + if progress_pct >= 1.0 do audio.fade_state = .None + + case .Out: + audio.current_music_vol = rl.Lerp(audio.user_music_vol, 0.0, progress_pct) + // progress is 100% or more stop fading out music and stop the stream + if progress_pct >= 1.0 { + audio.fade_state = .None + rl.StopMusicStream(audio.music) + } + } + } else { + // not fading, output matches the user setting directly + audio.current_music_vol = audio.user_music_vol + } + + rl.SetMusicVolume(audio.music, audio.current_music_vol) + + // keep audio stream buffer fed + if rl.IsMusicStreamPlaying(audio.music) { + rl.UpdateMusicStream(audio.music) + } +} + +audio_manager_quit :: proc(audio: ^Audio_Manager) { + for _, sound in audio.sounds { + rl.UnloadSound(sound) + } + delete(audio.sounds) + rl.UnloadMusicStream(audio.music) + rl.CloseAudioDevice() +} + +// ------------------------------------------------------------------------------ +// Utilities +// ------------------------------------------------------------------------------ +audio_play_music :: proc(audio: ^Audio_Manager) { + if audio.music_loaded { + // Only trigger start if it isn't playing already + if !rl.IsMusicStreamPlaying(audio.music) { + rl.PlayMusicStream(audio.music) + } + } +} + +audio_stop_music :: proc(audio: ^Audio_Manager) { + if audio.music_loaded && rl.IsMusicStreamPlaying(audio.music) { + rl.StopMusicStream(audio.music) + } +} + +audio_play_sfx :: proc(audio: ^Audio_Manager, id: Sound_ID) { + if sound, ok := audio.sounds[id]; ok { + rl.SetSoundVolume(sound, audio.sfx_vol) + rl.PlaySound(sound) + } +} + +// ------------------------------------------------------------------------------ +// Audio Fading +// ------------------------------------------------------------------------------ +audio_fade_in :: proc(audio: ^Audio_Manager, duration: f32 = 1.0) { + if !audio.music_loaded do return + + if !rl.IsMusicStreamPlaying(audio.music) { + rl.PlayMusicStream(audio.music) + } + + audio.fade_state = .In + audio.fade_duration_sec = max(0.001, duration) + audio.fade_elapsed_sec = 0.0 + audio.current_music_vol = 0.0 + rl.SetMusicVolume(audio.music, 0.0) +} + +audio_fade_out :: proc(audio: ^Audio_Manager, duration: f32 = 1.0) { + if !audio.music_loaded || !rl.IsMusicStreamPlaying(audio.music) do return + + audio.fade_state = .Out + audio.fade_duration_sec = max(0.001, duration) + audio.fade_elapsed_sec = 0.0 +} \ No newline at end of file diff --git a/src/game.odin b/src/game.odin index 2a7ce50..e749890 100644 --- a/src/game.odin +++ b/src/game.odin @@ -13,6 +13,7 @@ Game :: struct { scene: Scene, overlay_scene: Maybe(Scene), should_close: bool, + audio: Audio_Manager, } // ------------------------------------------------------------------------------ @@ -22,6 +23,9 @@ game_init :: proc(game: ^Game) { rl.InitWindow(WINDOW_W, WINDOW_H, WINDOW_TITLE) rl.SetTargetFPS(60) + // init any global managers + audio_init(&game.audio) + // allocate some event capacity to stop early dynamic reallocations GLOBAL_EVENTS = make([dynamic]Event, 0, 16) @@ -43,6 +47,9 @@ game_update :: proc(game: ^Game) { scene_update(&game.scene, game, input, dt) } + // background music buffer streaming + audio_update(&game.audio, dt) + // flush any events triggered in this loop flush_events() } @@ -72,6 +79,10 @@ game_render :: proc(game: ^Game) { game_quit :: proc(game: ^Game) { delete(GLOBAL_EVENTS) rl.UnloadRenderTexture(game.viewport) + + // cleanup global managers + audio_manager_quit(&game.audio) + rl.CloseWindow() }