bones_framework/audio/
audio_center.rs

1//! Audio Center resource and systems.
2
3use super::{Audio, AudioManager, AudioSource};
4use crate::prelude::*;
5use kira;
6use kira::{
7    sound::{static_sound::StaticSoundSettings, PlaybackState},
8    tween,
9    tween::Tween,
10    Volume,
11};
12use std::collections::VecDeque;
13use std::time::Duration;
14use tracing::warn;
15
16/// A resource that can be used to control game audios.
17#[derive(HasSchema)]
18#[schema(no_clone)]
19pub struct AudioCenter {
20    /// Buffer for audio events that have not yet been processed.
21    events: VecDeque<AudioEvent>,
22    /// The handle to the current music.
23    music: Option<Audio>,
24    /// The volume scale for main audio.
25    main_volume_scale: f32,
26    /// The volume scale for music.
27    music_volume_scale: f32,
28    /// The volume scale for sound effects.
29    effects_volume_scale: f32,
30    /// How long music should fade out for when stopping.
31    music_fade_duration: Duration,
32    /// How long music should fade out for when stopping.
33    sounds_fade_duration: Duration,
34}
35
36impl Default for AudioCenter {
37    fn default() -> Self {
38        Self {
39            events: VecDeque::with_capacity(16),
40            music: None,
41            main_volume_scale: 1.0,
42            music_volume_scale: 1.0,
43            effects_volume_scale: 1.0,
44            music_fade_duration: Duration::from_millis(500),
45            sounds_fade_duration: Duration::from_millis(500),
46        }
47    }
48}
49
50impl AudioCenter {
51    /// Push an audio event to the queue for later processing.
52    pub fn push_event(&mut self, event: AudioEvent) {
53        self.events.push_back(event);
54    }
55
56    /// Returns the currently played music.
57    pub fn music(&self) -> Option<&Audio> {
58        self.music.as_ref()
59    }
60
61    /// Get the playback state of the current music.
62    pub fn music_state(&self) -> Option<PlaybackState> {
63        self.music().map(|m| m.handle.state())
64    }
65
66    /// Play a sound. These are usually short audios that indicate something
67    /// happened in game, e.g. a player jump, an explosion, etc.
68    /// Volume is scaled by both main_volume_scale, and effects_volume_scale.
69    pub fn play_sound(&mut self, sound_source: Handle<AudioSource>, volume: f64) {
70        self.events.push_back(AudioEvent::PlaySound {
71            sound_source,
72            volume,
73        })
74    }
75
76    /// Plays music, forcibly stopping any current music.
77    /// Volume is scaled by both main_volume_scale and music_volume_scale.
78    pub fn play_music(&mut self, sound_source: Handle<AudioSource>, volume: f64, loop_music: bool) {
79        let clamped_volume = volume.clamp(0.0, 1.0);
80        let mut settings = StaticSoundSettings::new().volume(Volume::Amplitude(clamped_volume));
81
82        if loop_music {
83            settings = settings.loop_region(kira::sound::Region {
84                start: 0.0.into(),
85                end: kira::sound::EndPosition::EndOfAudio,
86            });
87        }
88
89        self.events.push_back(AudioEvent::PlayMusic {
90            sound_source,
91            sound_settings: Box::new(settings),
92            force_restart: true,
93        });
94    }
95
96    /// Plays music with advanced settings.
97    /// Volume is scaled by both main_volume_scale and music_volume_scale.
98    ///
99    /// # Parameters
100    ///
101    /// * `sound_source` - The handle for the audio source to play
102    /// * `volume` - The volume to play the music at (0.0 to 1.0)
103    /// * `loop_music` - Whether the music should loop indefinitely
104    /// * `reverse` - Whether to play the audio in reverse
105    /// * `start_position` - The position in seconds to start playback from
106    /// * `playback_rate` - The playback rate (1.0 is normal speed, 0.5 is half speed, 2.0 is double speed)
107    /// * `skip_restart` - If true, won't restart the music if it's already playing the same track
108    #[allow(clippy::too_many_arguments)]
109    pub fn play_music_advanced(
110        &mut self,
111        sound_source: Handle<AudioSource>,
112        volume: f64,
113        loop_music: bool,
114        reverse: bool,
115        start_position: f64,
116        playback_rate: f64,
117        skip_restart: bool,
118    ) {
119        let clamped_volume = volume.clamp(0.0, 1.0);
120        let mut settings = StaticSoundSettings::new()
121            .volume(Volume::Amplitude(clamped_volume))
122            .start_position(kira::sound::PlaybackPosition::Seconds(start_position))
123            .reverse(reverse)
124            .playback_rate(playback_rate);
125
126        if loop_music {
127            settings = settings.loop_region(kira::sound::Region {
128                start: 0.0.into(),
129                end: kira::sound::EndPosition::EndOfAudio,
130            });
131        }
132
133        self.events.push_back(AudioEvent::PlayMusic {
134            sound_source,
135            sound_settings: Box::new(settings),
136            force_restart: !skip_restart,
137        });
138    }
139
140    /// Plays music with custom StaticSoundSettings.
141    /// Volume is scaled by both main_volume_scale and music_volume_scale.
142    pub fn play_music_custom(
143        &mut self,
144        sound_source: Handle<AudioSource>,
145        sound_settings: StaticSoundSettings,
146        skip_restart: bool,
147    ) {
148        self.events.push_back(AudioEvent::PlayMusic {
149            sound_source,
150            sound_settings: Box::new(sound_settings),
151            force_restart: !skip_restart,
152        });
153    }
154
155    /// Sets the volume scale for main audio within the range of 0.0 to 1.0.
156    /// The main volume scale impacts all other volume scales.
157    pub fn set_main_volume_scale(&mut self, main: f32) {
158        self.main_volume_scale = main.clamp(0.0, 1.0);
159    }
160
161    /// Sets the volume scale for music within the range of 0.0 to 1.0.
162    pub fn set_music_volume_scale(&mut self, music: f32) {
163        self.music_volume_scale = music.clamp(0.0, 1.0);
164    }
165
166    /// Sets the volume scale for effects within the range of 0.0 to 1.0.
167    pub fn set_effects_volume_scale(&mut self, effects: f32) {
168        self.effects_volume_scale = effects.clamp(0.0, 1.0);
169    }
170
171    /// Sets the volume scales for main, music, and effects within the range of 0.0 to 1.0.
172    pub fn set_volume_scales(&mut self, main: f32, music: f32, effects: f32) {
173        self.set_main_volume_scale(main);
174        self.set_music_volume_scale(music);
175        self.set_effects_volume_scale(effects);
176        self.events.push_back(AudioEvent::VolumeScaleUpdate {
177            main_volume_scale: self.main_volume_scale,
178            music_volume_scale: self.music_volume_scale,
179            effects_volume_scale: self.effects_volume_scale,
180        });
181    }
182
183    /// Returns the main volume scale (which impacts all other volume scales).
184    pub fn main_volume_scale(&self) -> f32 {
185        self.main_volume_scale
186    }
187
188    /// Returns the music volume scale.
189    pub fn music_volume_scale(&self) -> f32 {
190        self.music_volume_scale
191    }
192
193    /// Returns the volume scale for sound effects.
194    pub fn effects_volume_scale(&self) -> f32 {
195        self.effects_volume_scale
196    }
197
198    /// Returns the duration for music fade out.
199    pub fn music_fade_duration(&self) -> Duration {
200        self.music_fade_duration
201    }
202
203    /// Sets the duration for music fade out.
204    pub fn set_music_fade_duration(&mut self, duration: Duration) {
205        self.music_fade_duration = duration;
206    }
207
208    /// Returns the duration for audio fade out.
209    pub fn sounds_fade_duration(&self) -> Duration {
210        self.sounds_fade_duration
211    }
212
213    /// Sets the duration for audio fade out.
214    pub fn set_sounds_fade_duration(&mut self, duration: Duration) {
215        self.sounds_fade_duration = duration;
216    }
217
218    /// Stops the currently playing music
219    /// * `fade_out` - If true, fades out the music using the Audio Center's music fade duration. If false, stops the sounds instantly.
220    pub fn stop_music(&mut self, fade_out: bool) {
221        self.events.push_back(AudioEvent::StopMusic { fade_out });
222    }
223
224    /// Stops all currently playing sounds.
225    /// * `fade_out` - If true, fades out the sounds using the Audio Center's sounds fade duration. If false, stops the sounds instantly.
226    pub fn stop_all_sounds(&mut self, fade_out: bool) {
227        self.events
228            .push_back(AudioEvent::StopAllSounds { fade_out });
229    }
230}
231
232/// An audio event that may be sent to the [`AudioCenter`] resource for
233/// processing.
234#[derive(Clone, Debug)]
235pub enum AudioEvent {
236    /// Update the volume of all audios using the new scale values.
237    /// This event is used to adjust the overall volume of the application.
238    VolumeScaleUpdate {
239        /// The main volume scale factor.
240        main_volume_scale: f32,
241        /// The music volume scale factor.
242        music_volume_scale: f32,
243        /// The effects volume scale factor.
244        effects_volume_scale: f32,
245    },
246    /// Play some music.
247    ///
248    /// Any current music is stopped if force_restart is true or if the new music is different.
249    PlayMusic {
250        /// The handle for the music.
251        sound_source: Handle<AudioSource>,
252        /// The settings for the music.
253        sound_settings: Box<StaticSoundSettings>,
254        /// Whether to force restart the music even if it's the same as the current music.
255        force_restart: bool,
256    },
257    /// Stop the currently playing music.
258    StopMusic {
259        /// Whether to fade out the sounds or stop them instantly.
260        fade_out: bool,
261    },
262    /// Play a sound.
263    PlaySound {
264        /// The handle to the sound to play.
265        sound_source: Handle<AudioSource>,
266        /// The volume to play the sound at.
267        volume: f64,
268    },
269    /// Stop all currently playing sounds.
270    StopAllSounds {
271        /// Whether to fade out the sounds or stop them instantly.
272        fade_out: bool,
273    },
274}
275
276/// Internally used sytem for processing audio events in the bones audio session.
277pub fn _process_audio_events(
278    mut audio_manager: ResMut<AudioManager>,
279    mut audio_center: ResMut<AudioCenter>,
280    assets: ResInit<AssetServer>,
281    mut entities: ResMut<Entities>,
282    mut audios: CompMut<Audio>,
283) {
284    for event in audio_center.events.drain(..).collect::<Vec<_>>() {
285        match event {
286            AudioEvent::VolumeScaleUpdate {
287                main_volume_scale,
288                music_volume_scale,
289                effects_volume_scale,
290            } => {
291                let tween = Tween::default();
292                // Update music volume
293                if let Some(music) = &mut audio_center.music {
294                    let volume =
295                        (main_volume_scale as f64) * (music_volume_scale as f64) * music.volume;
296                    music.handle.set_volume(volume, tween);
297                }
298                // Update sound volumes
299                for audio in audios.iter_mut() {
300                    let volume =
301                        (main_volume_scale as f64) * (effects_volume_scale as f64) * audio.volume;
302                    audio.handle.set_volume(volume, tween);
303                }
304            }
305            AudioEvent::PlayMusic {
306                sound_source,
307                mut sound_settings,
308                force_restart,
309            } => {
310                let should_play = force_restart
311                    || audio_center
312                        .music
313                        .as_ref()
314                        .is_none_or(|current_music| sound_source != current_music.bones_handle);
315
316                if should_play {
317                    // Stop the current music
318                    if let Some(mut music) = audio_center.music.take() {
319                        let tween = Tween {
320                            start_time: kira::StartTime::Immediate,
321                            duration: audio_center.music_fade_duration,
322                            easing: tween::Easing::Linear,
323                        };
324                        music.handle.stop(tween);
325                    }
326                    // Scale the requested volume by the volume scales
327                    let volume = match sound_settings.volume {
328                        tween::Value::Fixed(vol) => vol.as_amplitude(),
329                        _ => 1.0,
330                    };
331                    let scaled_volume = (audio_center.main_volume_scale as f64)
332                        * (audio_center.music_volume_scale as f64)
333                        * volume;
334                    sound_settings.volume = tween::Value::Fixed(Volume::Amplitude(scaled_volume));
335                    // Play the new music
336                    let sound_data = assets.get(sound_source).with_settings(*sound_settings);
337                    match audio_manager.play(sound_data) {
338                        Err(err) => warn!("Error playing music: {err}"),
339                        Ok(handle) => {
340                            audio_center.music = Some(Audio {
341                                handle,
342                                volume,
343                                bones_handle: sound_source,
344                            })
345                        }
346                    }
347                }
348            }
349            AudioEvent::StopMusic { fade_out } => {
350                if let Some(mut music) = audio_center.music.take() {
351                    if fade_out {
352                        let tween = Tween {
353                            start_time: kira::StartTime::Immediate,
354                            duration: audio_center.music_fade_duration,
355                            easing: tween::Easing::Linear,
356                        };
357                        music.handle.stop(tween);
358                    } else {
359                        music.handle.stop(Tween::default());
360                    }
361                }
362            }
363            AudioEvent::StopAllSounds { fade_out } => {
364                let tween = if fade_out {
365                    Tween {
366                        start_time: kira::StartTime::Immediate,
367                        duration: audio_center.sounds_fade_duration,
368                        easing: tween::Easing::Linear,
369                    }
370                } else {
371                    Tween::default()
372                };
373
374                for (_, audio) in entities.iter_with(&mut audios) {
375                    audio.handle.stop(tween);
376                }
377            }
378            AudioEvent::PlaySound {
379                sound_source,
380                volume,
381            } => {
382                let scaled_volume = (audio_center.main_volume_scale as f64)
383                    * (audio_center.effects_volume_scale as f64)
384                    * volume;
385                let sound_data = assets
386                    .get(sound_source)
387                    .with_settings(StaticSoundSettings::default().volume(scaled_volume));
388                match audio_manager.play(sound_data) {
389                    Err(err) => warn!("Error playing sound: {err}"),
390                    Ok(handle) => {
391                        let audio_ent = entities.create();
392                        audios.insert(
393                            audio_ent,
394                            Audio {
395                                handle,
396                                volume,
397                                bones_handle: sound_source,
398                            },
399                        );
400                    }
401                }
402            }
403        }
404    }
405}
406
407/// Internally used sytem for killing finished audios (generally sounds) which were emitted as separate entities.
408/// Used in the bones audio session.
409pub fn _kill_finished_audios(entities: Res<Entities>, audios: Comp<Audio>, mut commands: Commands) {
410    for (audio_ent, audio) in entities.iter_with(&audios) {
411        if audio.handle.state() == PlaybackState::Stopped {
412            commands.add(move |mut entities: ResMut<Entities>| entities.kill(audio_ent));
413        }
414    }
415}