Skip to main content

rusty_engine/
audio.rs

1//! Facilities for interacting with audio, including: [`AudioManager`], [`MusicPreset`], and
2//! [`SfxPreset`]
3//!
4//! You may add your own sound files to the `assets/` directory or any of its subdirectories
5//! and play them as sound effects or music by providing the relative path to the file. For example,
6//!  if you place a file named `my_sound_effect.mp3` in `assets/`, you can play it with:
7//!
8//! ```rust,no_run
9//! # use rusty_engine::prelude::*;
10//! #
11//! # #[derive(Resource)]
12//! # struct GameState;
13//! #
14//! # fn main() {
15//! # let mut game = Game::new();
16//! // Inside your logic function...
17//! game.audio_manager.play_sfx("my_sound_effect.mp3", 1.0);
18//! # game.run(GameState);
19//! # }
20//! ```
21//!
22//! Or, if you create a `assets/my_game/` subdirectory and place a file named `spooky_loop.ogg`, you
23//! could play it as continuous music with:
24//!
25//! ```rust,no_run
26//! # use rusty_engine::prelude::*;
27//! #
28//! # #[derive(Resource)]
29//! # struct GameState;
30//! #
31//! # fn main() {
32//! # let mut game = Game::new();
33//! // Inside your logic function...
34//! game.audio_manager.play_music("my_game/spooky_loop.ogg", 1.0);
35//! # game.run(GameState);
36//! # }
37//! ```
38//!
39//! The sound effects provided in this asset pack have convenient `enum`s defined that you can use
40//! instead of a path to the file: `SfxPreset` and `MusicPreset`. For example:
41//!
42//! ```rust,no_run
43//! // Import the enums into scope first
44//! use rusty_engine::prelude::*;
45//!
46//! # #[derive(Resource)]
47//! # struct GameState;
48//! #
49//! # fn main() {
50//! # let mut game = Game::new();
51//! // Inside your logic function...
52//! game.audio_manager.play_sfx(SfxPreset::Confirmation1, 1.0);
53//! game.audio_manager.play_music(MusicPreset::Classy8Bit, 1.0);
54//! # game.run(GameState);
55//! # }
56//! ```
57//!
58
59use crate::prelude::Engine;
60use bevy::{
61    audio::{AudioSink, PlaybackMode, Volume},
62    prelude::*,
63};
64use std::{array::IntoIter, fmt::Debug};
65
66#[derive(Default)]
67#[doc(hidden)]
68/// Use a Bevy plugin to run a Bevy system to handle our audio logic
69pub struct AudioManagerPlugin;
70
71impl Plugin for AudioManagerPlugin {
72    fn build(&self, app: &mut bevy::prelude::App) {
73        app.add_systems(Update, queue_managed_audio_system);
74    }
75}
76
77/// You will interact with the [`AudioManager`] for all audio needs in Rusty Engine. It is exposed
78/// through the [`Engine`](crate::prelude::Engine) struct provided to your logic function
79/// each frame as the [`audio_manager`](crate::prelude::Engine::audio_manager) field. It is also
80/// accessible through the [`Game`](crate::prelude::Game) struct in your `main` function.
81#[derive(Default)]
82pub struct AudioManager {
83    sfx_queue: Vec<(String, f32)>,
84    music_queue: Vec<Option<(String, f32)>>,
85    playing: Option<Entity>,
86    music_playing: bool,
87}
88
89impl Debug for AudioManager {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        f.debug_struct("AudioManager")
92            .field("sfx_queue", &self.sfx_queue)
93            .field("music_queue", &self.music_queue)
94            .field("music_playing", &self.music_playing)
95            .finish()
96    }
97}
98
99impl AudioManager {
100    /// Play a sound effect. `volume` ranges from `0.0` to `1.0`. `sfx` can be an [`SfxPreset`] or a
101    /// string containing the relative path/filename of a sound file within the `assets/`
102    /// directory. Sound effects are "fire and forget". They will play to completion and then stop.
103    /// Multiple sound effects will be mixed and play simultaneously.
104    pub fn play_sfx<S: Into<String>>(&mut self, sfx: S, volume: f32) {
105        self.sfx_queue.push((sfx.into(), volume.clamp(0.0, 1.0)));
106    }
107    /// Play looping music. `volume` ranges from `0.0` to `1.0`. Music will loop until stopped with
108    /// [`stop_music`](AudioManager::stop_music). Playing music stops any previously playing music.
109    /// `music` can be a [`MusicPreset`] or a string containing the relative path/filename of a
110    /// sound file within the `assets/` directory.
111    pub fn play_music<S: Into<String>>(&mut self, music: S, volume: f32) {
112        self.music_playing = true;
113        self.music_queue
114            .push(Some((music.into(), volume.clamp(0.0, 1.0))));
115    }
116    /// Stop any music currently playing. Ignored if no music is currently playing.
117    pub fn stop_music(&mut self) {
118        if self.music_playing {
119            self.music_playing = false;
120            self.music_queue.push(None);
121        }
122    }
123    /// Whether music is currently playing.
124    pub fn music_playing(&self) -> bool {
125        self.music_playing
126    }
127}
128
129#[derive(Copy, Clone, Debug)]
130/// Sound effects included with the downloadable asset pack. You can hear these all played in the
131/// `sfx` example by cloning the `rusty_engine` repository and running the following command:
132///
133/// ```text
134/// cargo run --release --example sfx_sampler
135/// ```
136pub enum SfxPreset {
137    Click,
138    Confirmation1,
139    Confirmation2,
140    Congratulations,
141    Forcefield1,
142    Forcefield2,
143    Impact1,
144    Impact2,
145    Impact3,
146    Jingle1,
147    Jingle2,
148    Jingle3,
149    Minimize1,
150    Minimize2,
151    Switch1,
152    Switch2,
153    Tones1,
154    Tones2,
155}
156
157impl SfxPreset {
158    pub fn variant_iter() -> IntoIter<SfxPreset, 18> {
159        static SFX_PRESETS: [SfxPreset; 18] = [
160            SfxPreset::Click,
161            SfxPreset::Confirmation1,
162            SfxPreset::Confirmation2,
163            SfxPreset::Congratulations,
164            SfxPreset::Forcefield1,
165            SfxPreset::Forcefield2,
166            SfxPreset::Impact1,
167            SfxPreset::Impact2,
168            SfxPreset::Impact3,
169            SfxPreset::Jingle1,
170            SfxPreset::Jingle2,
171            SfxPreset::Jingle3,
172            SfxPreset::Minimize1,
173            SfxPreset::Minimize2,
174            SfxPreset::Switch1,
175            SfxPreset::Switch2,
176            SfxPreset::Tones1,
177            SfxPreset::Tones2,
178        ];
179        SFX_PRESETS.into_iter()
180    }
181}
182
183impl From<SfxPreset> for String {
184    fn from(sfx_preset: SfxPreset) -> Self {
185        match sfx_preset {
186            SfxPreset::Click => "sfx/click.ogg".into(),
187            SfxPreset::Confirmation1 => "sfx/confirmation1.ogg".into(),
188            SfxPreset::Confirmation2 => "sfx/confirmation2.ogg".into(),
189            SfxPreset::Congratulations => "sfx/congratulations.ogg".into(),
190            SfxPreset::Forcefield1 => "sfx/forcefield1.ogg".into(),
191            SfxPreset::Forcefield2 => "sfx/forcefield2.ogg".into(),
192            SfxPreset::Impact1 => "sfx/impact1.ogg".into(),
193            SfxPreset::Impact2 => "sfx/impact2.ogg".into(),
194            SfxPreset::Impact3 => "sfx/impact3.ogg".into(),
195            SfxPreset::Jingle1 => "sfx/jingle1.ogg".into(),
196            SfxPreset::Jingle2 => "sfx/jingle2.ogg".into(),
197            SfxPreset::Jingle3 => "sfx/jingle3.ogg".into(),
198            SfxPreset::Minimize1 => "sfx/minimize1.ogg".into(),
199            SfxPreset::Minimize2 => "sfx/minimize2.ogg".into(),
200            SfxPreset::Switch1 => "sfx/switch1.ogg".into(),
201            SfxPreset::Switch2 => "sfx/switch2.ogg".into(),
202            SfxPreset::Tones1 => "sfx/tones1.ogg".into(),
203            SfxPreset::Tones2 => "sfx/tones2.ogg".into(),
204        }
205    }
206}
207
208/// Music included with the downloadable asset pack. You can hear this music in the `music` example
209/// by cloning the `rusty_engine` repository and running the following command:
210///
211/// ```text
212/// cargo run --release --example music_sampler
213/// ```
214#[derive(Copy, Clone, Debug)]
215pub enum MusicPreset {
216    Classy8Bit,
217    MysteriousMagic,
218    WhimsicalPopsicle,
219}
220
221impl MusicPreset {
222    pub fn variant_iter() -> IntoIter<MusicPreset, 3> {
223        static MUSIC_PRESETS: [MusicPreset; 3] = [
224            MusicPreset::Classy8Bit,
225            MusicPreset::MysteriousMagic,
226            MusicPreset::WhimsicalPopsicle,
227        ];
228        MUSIC_PRESETS.into_iter()
229    }
230}
231
232impl From<MusicPreset> for String {
233    fn from(music_preset: MusicPreset) -> String {
234        match music_preset {
235            MusicPreset::Classy8Bit => "music/Classy 8-Bit.ogg".into(),
236            MusicPreset::MysteriousMagic => "music/Mysterious Magic.ogg".into(),
237            MusicPreset::WhimsicalPopsicle => "music/Whimsical Popsicle.ogg".into(),
238        }
239    }
240}
241
242#[derive(Component)]
243pub struct Music;
244
245/// The Bevy system that checks to see if there is any audio management that needs to be done.
246#[doc(hidden)]
247pub fn queue_managed_audio_system(
248    mut commands: Commands,
249    music_query: Query<(Entity, &AudioSink), With<Music>>,
250    asset_server: Res<AssetServer>,
251    mut game_state: ResMut<Engine>,
252) {
253    for (sfx, volume) in game_state.audio_manager.sfx_queue.drain(..) {
254        commands.spawn((
255            AudioPlayer::<AudioSource>(asset_server.load(format!("audio/{}", sfx))),
256            PlaybackSettings {
257                mode: PlaybackMode::Despawn,
258                volume: Volume::Linear(volume),
259                ..Default::default()
260            },
261        ));
262    }
263    let last_music_item = game_state.audio_manager.music_queue.pop();
264    game_state.audio_manager.music_queue.clear();
265    if let Some(item) = last_music_item {
266        // stop any music currently playing
267        if let Ok((entity, music)) = music_query.single() {
268            music.stop();
269            commands.entity(entity).despawn();
270        }
271        // start the new music...if we have some
272        if let Some((music, volume)) = item {
273            let entity = commands
274                .spawn((
275                    AudioPlayer::<AudioSource>(asset_server.load(format!("audio/{}", music))),
276                    PlaybackSettings {
277                        mode: PlaybackMode::Loop,
278                        volume: Volume::Linear(volume),
279                        ..Default::default()
280                    },
281                    Music,
282                ))
283                .id();
284            game_state.audio_manager.playing = Some(entity);
285        }
286    }
287}