Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to mute audio sinks #16813

Merged
merged 14 commits into from
Dec 15, 2024
16 changes: 8 additions & 8 deletions crates/bevy_audio/src/audio_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,25 +167,25 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
match settings.mode {
PlaybackMode::Loop => {
sink.append(audio_source.decoder().repeat_infinite());
commands.entity(entity).insert(SpatialAudioSink { sink });
commands.entity(entity).insert(SpatialAudioSink::new(sink));
}
PlaybackMode::Once => {
sink.append(audio_source.decoder());
commands.entity(entity).insert(SpatialAudioSink { sink });
commands.entity(entity).insert(SpatialAudioSink::new(sink));
}
PlaybackMode::Despawn => {
sink.append(audio_source.decoder());
commands
.entity(entity)
// PERF: insert as bundle to reduce archetype moves
.insert((SpatialAudioSink { sink }, PlaybackDespawnMarker));
.insert((SpatialAudioSink::new(sink), PlaybackDespawnMarker));
}
PlaybackMode::Remove => {
sink.append(audio_source.decoder());
commands
.entity(entity)
// PERF: insert as bundle to reduce archetype moves
.insert((SpatialAudioSink { sink }, PlaybackRemoveMarker));
.insert((SpatialAudioSink::new(sink), PlaybackRemoveMarker));
}
};
} else {
Expand All @@ -207,25 +207,25 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
match settings.mode {
PlaybackMode::Loop => {
sink.append(audio_source.decoder().repeat_infinite());
commands.entity(entity).insert(AudioSink { sink });
commands.entity(entity).insert(AudioSink::new(sink));
}
PlaybackMode::Once => {
sink.append(audio_source.decoder());
commands.entity(entity).insert(AudioSink { sink });
commands.entity(entity).insert(AudioSink::new(sink));
}
PlaybackMode::Despawn => {
sink.append(audio_source.decoder());
commands
.entity(entity)
// PERF: insert as bundle to reduce archetype moves
.insert((AudioSink { sink }, PlaybackDespawnMarker));
.insert((AudioSink::new(sink), PlaybackDespawnMarker));
}
PlaybackMode::Remove => {
sink.append(audio_source.decoder());
commands
.entity(entity)
// PERF: insert as bundle to reduce archetype moves
.insert((AudioSink { sink }, PlaybackRemoveMarker));
.insert((AudioSink::new(sink), PlaybackRemoveMarker));
}
};
}
Expand Down
133 changes: 125 additions & 8 deletions crates/bevy_audio/src/sinks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,32 @@ 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.
///
/// 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 sets the managed volume rather than the
/// sink's actual volume. This allows you to control the volume even when
/// the sink is muted, so that when it is unmuted, the volume is restored
/// and all calls to this function are respected.
///
/// # Note on Audio Volume
///
/// An increase of 10 decibels (dB) roughly corresponds to the perceived volume doubling in intensity.
/// As this function scales not the volume but the amplitude, a conversion might be necessary.
/// 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.
///
Expand Down Expand Up @@ -71,6 +80,24 @@ 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.
fn mute(&mut self);

/// Unmutes the sink.
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.
Expand All @@ -86,15 +113,46 @@ 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<f32>,
}

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()
if let Some(volume) = self.managed_volume {
volume
} 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 {
Expand Down Expand Up @@ -124,6 +182,22 @@ 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 {
self.sink.set_volume(volume);
self.managed_volume = None;
}
}
}

/// Used to control spatial audio during playback.
Expand All @@ -139,15 +213,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<f32>,
}

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()
}

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 {
Expand Down Expand Up @@ -177,6 +278,22 @@ 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 {
self.sink.set_volume(volume);
self.managed_volume = None;
}
}
}

impl SpatialAudioSink {
Expand Down
22 changes: 18 additions & 4 deletions examples/audio/audio_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -30,10 +30,24 @@ fn pause(keyboard_input: Res<ButtonInput<KeyCode>>, sink: Single<&AudioSink, Wit
}
}

fn volume(keyboard_input: Res<ButtonInput<KeyCode>>, sink: Single<&AudioSink, With<MyMusic>>) {
fn mute(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut sink: Single<&mut AudioSink, With<MyMusic>>,
) {
if keyboard_input.just_pressed(KeyCode::KeyM) {
sink.toggle_mute();
}
}

fn volume(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut sink: Single<&mut AudioSink, With<MyMusic>>,
) {
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);
}
}
10 changes: 6 additions & 4 deletions examples/audio/soundtrack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,9 @@ fn fade_in(
mut audio_sink: Query<(&mut AudioSink, Entity), With<FadeIn>>,
time: Res<Time>,
) {
for (audio, entity) in audio_sink.iter_mut() {
audio.set_volume(audio.volume() + time.delta_secs() / FADE_TIME);
for (mut audio, entity) in audio_sink.iter_mut() {
let current_volume = audio.volume();
audio.set_volume(current_volume + time.delta_secs() / FADE_TIME);
if audio.volume() >= 1.0 {
audio.set_volume(1.0);
commands.entity(entity).remove::<FadeIn>();
Expand All @@ -129,8 +130,9 @@ fn fade_out(
mut audio_sink: Query<(&mut AudioSink, Entity), With<FadeOut>>,
time: Res<Time>,
) {
for (audio, entity) in audio_sink.iter_mut() {
audio.set_volume(audio.volume() - time.delta_secs() / FADE_TIME);
for (mut audio, entity) in audio_sink.iter_mut() {
let current_volume = audio.volume();
audio.set_volume(current_volume - time.delta_secs() / FADE_TIME);
if audio.volume() <= 0.0 {
commands.entity(entity).despawn_recursive();
}
Expand Down
Loading