diff --git a/crates/bevy_audio/src/audio.rs b/crates/bevy_audio/src/audio.rs index 89bd186e1acbf..be05582ab54de 100644 --- a/crates/bevy_audio/src/audio.rs +++ b/crates/bevy_audio/src/audio.rs @@ -69,6 +69,11 @@ pub struct PlaybackSettings { /// Useful for "deferred playback", if you want to prepare /// the entity, but hear the sound later. pub paused: bool, + /// Whether to create the sink in muted state or not. + /// + /// This is useful for audio that should be initially muted. You can still + /// set the initial volume and it is applied when the audio is unmuted. + pub muted: bool, /// Enables spatial audio for this source. /// /// See also: [`SpatialListener`]. @@ -100,6 +105,7 @@ impl PlaybackSettings { volume: Volume(1.0), speed: 1.0, paused: false, + muted: false, spatial: false, spatial_scale: None, }; @@ -128,6 +134,12 @@ impl PlaybackSettings { self } + /// Helper to start muted. + pub const fn muted(mut self) -> Self { + self.muted = true; + self + } + /// Helper to set the volume from start of playback. pub const fn with_volume(mut self, volume: Volume) -> Self { self.volume = volume; diff --git a/crates/bevy_audio/src/audio_output.rs b/crates/bevy_audio/src/audio_output.rs index bbb9c5b682133..45896c81ba7ac 100644 --- a/crates/bevy_audio/src/audio_output.rs +++ b/crates/bevy_audio/src/audio_output.rs @@ -10,7 +10,7 @@ use bevy_transform::prelude::GlobalTransform; use bevy_utils::tracing::warn; use rodio::{OutputStream, OutputStreamHandle, Sink, Source, SpatialSink}; -use crate::AudioSink; +use crate::{AudioSink, AudioSinkPlayback}; /// Used internally to play audio on the current "audio device" /// @@ -157,6 +157,19 @@ pub(crate) fn play_queued_audio_system( } }; + match settings.mode { + PlaybackMode::Loop => sink.append(audio_source.decoder().repeat_infinite()), + PlaybackMode::Once | PlaybackMode::Despawn | PlaybackMode::Remove => { + sink.append(audio_source.decoder()); + } + }; + + let mut sink = SpatialAudioSink::new(sink); + + if settings.muted { + sink.mute(); + } + sink.set_speed(settings.speed); sink.set_volume(settings.volume.0 * global_volume.volume.0); @@ -165,28 +178,15 @@ pub(crate) fn play_queued_audio_system( } match settings.mode { - PlaybackMode::Loop => { - sink.append(audio_source.decoder().repeat_infinite()); - commands.entity(entity).insert(SpatialAudioSink { sink }); - } - PlaybackMode::Once => { - sink.append(audio_source.decoder()); - commands.entity(entity).insert(SpatialAudioSink { sink }); - } - PlaybackMode::Despawn => { - sink.append(audio_source.decoder()); - commands - .entity(entity) - // PERF: insert as bundle to reduce archetype moves - .insert((SpatialAudioSink { sink }, PlaybackDespawnMarker)); - } - PlaybackMode::Remove => { - sink.append(audio_source.decoder()); - commands - .entity(entity) - // PERF: insert as bundle to reduce archetype moves - .insert((SpatialAudioSink { sink }, PlaybackRemoveMarker)); - } + PlaybackMode::Loop | PlaybackMode::Once => commands.entity(entity).insert(sink), + PlaybackMode::Despawn => commands + .entity(entity) + // PERF: insert as bundle to reduce archetype moves + .insert((sink, PlaybackDespawnMarker)), + PlaybackMode::Remove => commands + .entity(entity) + // PERF: insert as bundle to reduce archetype moves + .insert((sink, PlaybackRemoveMarker)), }; } else { let sink = match Sink::try_new(stream_handle) { @@ -197,6 +197,19 @@ pub(crate) fn play_queued_audio_system( } }; + match settings.mode { + PlaybackMode::Loop => sink.append(audio_source.decoder().repeat_infinite()), + PlaybackMode::Once | PlaybackMode::Despawn | PlaybackMode::Remove => { + sink.append(audio_source.decoder()); + } + }; + + let mut sink = AudioSink::new(sink); + + if settings.muted { + sink.mute(); + } + sink.set_speed(settings.speed); sink.set_volume(settings.volume.0 * global_volume.volume.0); @@ -205,28 +218,15 @@ pub(crate) fn play_queued_audio_system( } match settings.mode { - PlaybackMode::Loop => { - sink.append(audio_source.decoder().repeat_infinite()); - commands.entity(entity).insert(AudioSink { sink }); - } - PlaybackMode::Once => { - sink.append(audio_source.decoder()); - commands.entity(entity).insert(AudioSink { sink }); - } - PlaybackMode::Despawn => { - sink.append(audio_source.decoder()); - commands - .entity(entity) - // PERF: insert as bundle to reduce archetype moves - .insert((AudioSink { sink }, PlaybackDespawnMarker)); - } - PlaybackMode::Remove => { - sink.append(audio_source.decoder()); - commands - .entity(entity) - // PERF: insert as bundle to reduce archetype moves - .insert((AudioSink { sink }, PlaybackRemoveMarker)); - } + PlaybackMode::Loop | PlaybackMode::Once => commands.entity(entity).insert(sink), + PlaybackMode::Despawn => commands + .entity(entity) + // PERF: insert as bundle to reduce archetype moves + .insert((sink, PlaybackDespawnMarker)), + PlaybackMode::Remove => commands + .entity(entity) + // PERF: insert as bundle to reduce archetype moves + .insert((sink, PlaybackRemoveMarker)), }; } } diff --git a/crates/bevy_audio/src/sinks.rs b/crates/bevy_audio/src/sinks.rs index 4c2cee3b935a7..dcd8fb01f3d1d 100644 --- a/crates/bevy_audio/src/sinks.rs +++ b/crates/bevy_audio/src/sinks.rs @@ -7,8 +7,12 @@ use rodio::{Sink, SpatialSink}; pub trait AudioSinkPlayback { /// Gets the volume of the sound. /// - /// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0` - /// will multiply each sample by this value. + /// The value `1.0` is the "normal" volume (unfiltered input). Any value + /// other than `1.0` will multiply each sample by this value. + /// + /// If the sink is muted, this returns the managed volume rather than the + /// sink's actual volume. This allows you to use the volume as if the sink + /// were not muted, because a muted sink has a volume of 0. fn volume(&self) -> f32; /// Changes the volume of the sound. @@ -16,6 +20,12 @@ pub trait AudioSinkPlayback { /// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0` /// will multiply each sample by this value. /// + /// If the sink is muted, changing the volume won't unmute it, i.e. the + /// sink's volume will remain at `0.0`. However, the sink will remember the + /// volume change and it will be used when [`unmute`](Self::unmute) is + /// called. This allows you to control the volume even when the sink is + /// muted. + /// /// # Note on Audio Volume /// /// An increase of 10 decibels (dB) roughly corresponds to the perceived volume doubling in intensity. @@ -23,7 +33,7 @@ pub trait AudioSinkPlayback { /// For example, to halve the perceived volume you need to decrease the volume by 10 dB. /// This corresponds to 20log(x) = -10dB, solving x = 10^(-10/20) = 0.316. /// Multiply the current volume by 0.316 to halve the perceived volume. - fn set_volume(&self, volume: f32); + fn set_volume(&mut self, volume: f32); /// Gets the speed of the sound. /// @@ -71,6 +81,29 @@ pub trait AudioSinkPlayback { /// Returns true if this sink has no more sounds to play. fn empty(&self) -> bool; + + /// Returns true if the sink is muted. + fn is_muted(&self) -> bool; + + /// Mutes the sink. + /// + /// Muting a sink sets the volume to 0. Use [`unmute`](Self::unmute) to + /// unmute the sink and restore the original volume. + fn mute(&mut self); + + /// Unmutes the sink. + /// + /// Restores the volume to the value it was before it was muted. + fn unmute(&mut self); + + /// Toggles whether the sink is muted or not. + fn toggle_mute(&mut self) { + if self.is_muted() { + self.unmute(); + } else { + self.mute(); + } + } } /// Used to control audio during playback. @@ -86,15 +119,42 @@ pub trait AudioSinkPlayback { #[derive(Component)] pub struct AudioSink { pub(crate) sink: Sink, + + /// Managed volume allows the sink to be muted without losing the user's + /// intended volume setting. + /// + /// This is used to restore the volume when [`unmute`](Self::unmute) is + /// called. + /// + /// If the sink is not muted, this is `None`. + /// + /// If the sink is muted, this is `Some(volume)` where `volume` is the + /// user's intended volume setting, even if the underlying sink's volume is + /// 0. + pub(crate) managed_volume: Option, +} + +impl AudioSink { + /// Create a new audio sink. + pub fn new(sink: Sink) -> Self { + Self { + sink, + managed_volume: None, + } + } } impl AudioSinkPlayback for AudioSink { fn volume(&self) -> f32 { - self.sink.volume() + self.managed_volume.unwrap_or_else(|| self.sink.volume()) } - fn set_volume(&self, volume: f32) { - self.sink.set_volume(volume); + fn set_volume(&mut self, volume: f32) { + if self.is_muted() { + self.managed_volume = Some(volume); + } else { + self.sink.set_volume(volume); + } } fn speed(&self) -> f32 { @@ -124,6 +184,21 @@ impl AudioSinkPlayback for AudioSink { fn empty(&self) -> bool { self.sink.empty() } + + fn is_muted(&self) -> bool { + self.managed_volume.is_some() + } + + fn mute(&mut self) { + self.managed_volume = Some(self.volume()); + self.sink.set_volume(0.0); + } + + fn unmute(&mut self) { + if let Some(volume) = self.managed_volume.take() { + self.sink.set_volume(volume); + } + } } /// Used to control spatial audio during playback. @@ -139,15 +214,42 @@ impl AudioSinkPlayback for AudioSink { #[derive(Component)] pub struct SpatialAudioSink { pub(crate) sink: SpatialSink, + + /// Managed volume allows the sink to be muted without losing the user's + /// intended volume setting. + /// + /// This is used to restore the volume when [`unmute`](Self::unmute) is + /// called. + /// + /// If the sink is not muted, this is `None`. + /// + /// If the sink is muted, this is `Some(volume)` where `volume` is the + /// user's intended volume setting, even if the underlying sink's volume is + /// 0. + pub(crate) managed_volume: Option, +} + +impl SpatialAudioSink { + /// Create a new spatial audio sink. + pub fn new(sink: SpatialSink) -> Self { + Self { + sink, + managed_volume: None, + } + } } impl AudioSinkPlayback for SpatialAudioSink { fn volume(&self) -> f32 { - self.sink.volume() + self.managed_volume.unwrap_or_else(|| self.sink.volume()) } - fn set_volume(&self, volume: f32) { - self.sink.set_volume(volume); + fn set_volume(&mut self, volume: f32) { + if self.is_muted() { + self.managed_volume = Some(volume); + } else { + self.sink.set_volume(volume); + } } fn speed(&self) -> f32 { @@ -177,6 +279,21 @@ impl AudioSinkPlayback for SpatialAudioSink { fn empty(&self) -> bool { self.sink.empty() } + + fn is_muted(&self) -> bool { + self.managed_volume.is_some() + } + + fn mute(&mut self) { + self.managed_volume = Some(self.volume()); + self.sink.set_volume(0.0); + } + + fn unmute(&mut self) { + if let Some(volume) = self.managed_volume.take() { + self.sink.set_volume(volume); + } + } } impl SpatialAudioSink { @@ -199,3 +316,60 @@ impl SpatialAudioSink { self.sink.set_emitter_position(position.to_array()); } } + +#[cfg(test)] +mod tests { + use rodio::Sink; + + use super::*; + + fn test_audio_sink_playback(mut audio_sink: T) { + // Test volume + assert_eq!(audio_sink.volume(), 1.0); // default volume + audio_sink.set_volume(0.5); + assert_eq!(audio_sink.volume(), 0.5); + audio_sink.set_volume(1.0); + assert_eq!(audio_sink.volume(), 1.0); + + // Test speed + assert_eq!(audio_sink.speed(), 1.0); // default speed + audio_sink.set_speed(0.5); + assert_eq!(audio_sink.speed(), 0.5); + audio_sink.set_speed(1.0); + assert_eq!(audio_sink.speed(), 1.0); + + // Test pause + assert!(!audio_sink.is_paused()); // default pause state + audio_sink.pause(); + assert!(audio_sink.is_paused()); + audio_sink.play(); + assert!(!audio_sink.is_paused()); + + // Test mute + assert!(!audio_sink.is_muted()); // default mute state + audio_sink.mute(); + assert!(audio_sink.is_muted()); + audio_sink.unmute(); + assert!(!audio_sink.is_muted()); + + // Test volume with mute + audio_sink.set_volume(0.5); + audio_sink.mute(); + assert_eq!(audio_sink.volume(), 0.5); // returns managed volume even though sink volume is 0 + audio_sink.unmute(); + assert_eq!(audio_sink.volume(), 0.5); // managed volume is restored + + // Test toggle mute + audio_sink.toggle_mute(); + assert!(audio_sink.is_muted()); + audio_sink.toggle_mute(); + assert!(!audio_sink.is_muted()); + } + + #[test] + fn test_audio_sink() { + let (sink, _queue_rx) = Sink::new_idle(); + let audio_sink = AudioSink::new(sink); + test_audio_sink_playback(audio_sink); + } +} diff --git a/examples/audio/audio_control.rs b/examples/audio/audio_control.rs index b56865c8910c9..c13509266a201 100644 --- a/examples/audio/audio_control.rs +++ b/examples/audio/audio_control.rs @@ -6,7 +6,7 @@ fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems(Update, (update_speed, pause, volume)) + .add_systems(Update, (update_speed, pause, mute, volume)) .run(); } @@ -15,6 +15,20 @@ fn setup(mut commands: Commands, asset_server: Res) { AudioPlayer::new(asset_server.load("sounds/Windless Slopes.ogg")), MyMusic, )); + + // example instructions + commands.spawn(( + Text::new("-/=: Volume Down/Up\nSpace: Pause Playback\nM: Toggle Mute"), + Node { + position_type: PositionType::Absolute, + bottom: Val::Px(12.0), + left: Val::Px(12.0), + ..default() + }, + )); + + // camera + commands.spawn(Camera3d::default()); } #[derive(Component)] @@ -30,10 +44,24 @@ fn pause(keyboard_input: Res>, sink: Single<&AudioSink, Wit } } -fn volume(keyboard_input: Res>, sink: Single<&AudioSink, With>) { +fn mute( + keyboard_input: Res>, + mut sink: Single<&mut AudioSink, With>, +) { + if keyboard_input.just_pressed(KeyCode::KeyM) { + sink.toggle_mute(); + } +} + +fn volume( + keyboard_input: Res>, + mut sink: Single<&mut AudioSink, With>, +) { if keyboard_input.just_pressed(KeyCode::Equal) { - sink.set_volume(sink.volume() + 0.1); + let current_volume = sink.volume(); + sink.set_volume(current_volume + 0.1); } else if keyboard_input.just_pressed(KeyCode::Minus) { - sink.set_volume(sink.volume() - 0.1); + let current_volume = sink.volume(); + sink.set_volume(current_volume - 0.1); } } diff --git a/examples/audio/soundtrack.rs b/examples/audio/soundtrack.rs index 0108594d94588..c5fce88570939 100644 --- a/examples/audio/soundtrack.rs +++ b/examples/audio/soundtrack.rs @@ -113,8 +113,9 @@ fn fade_in( mut audio_sink: Query<(&mut AudioSink, Entity), With>, time: Res