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 }