Skip to content

Commit

Permalink
Add ability to mute audio sinks (#16813)
Browse files Browse the repository at this point in the history
# Objective

- Allow users to mute audio.

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

- I want to be able to press, say, `M` and mute all my audio. I want
this for dev, but I'm sure it's a useful player setting as well.
- Muting is different to pausing—I don't want to pause my sounds, I want
them to keep playing but with no volume. For example if I have
background music playing which is made up of 5 tracks, I want to be able
to temporarily mute my background music, and if I unmute at, say, track
4, I want to play track 4 rather than have had everything paused and
still be on the first track.
- I want to be able to continue to control the volume of my audio even
when muted. Like in the example, if I have muted my audio but I use the
volume up/down controls, I want Bevy to remember those volume changes so
that when I unmute, the volume corresponds to that.

## Solution

- Add methods to audio to allow muting, unmuting and toggling muting.
- To preserve the user's intended volume, each sink needs to keep track
of a "managed volume".
- I checked `rodio` and I don't see any built in support for doing this,
so I added it to `bevy_audio`.
- I'm interested to hear if this is a good idea or a bad idea. To me,
this API looks nice and looks usable, but I'm aware it involves some
changes to the existing API and now also requires mutable access in some
places compared to before.
- I'm also aware of work on *Better Audio*, but I'm hoping that if this
change isn't too wild it might be a useful addition considering we don't
really know when we'll eventually get better audio.

## Testing

- Update and run the example:  `cargo run --example audio_control`
- Run the example:  `cargo run --example soundtrack`
- Update and run the example:  `cargo run --example spatial_audio_3d`
- Add unit tests.

---

## Showcase

See 2 changed examples that show how you can mute an audio sink and a
spatial audio sink.

## Migration Guide

- The `AudioSinkPlayback` trait now has 4 new methods to allow you to
mute audio sinks: `is_muted`, `mute`, `unmute` and `toggle_mute`. You
can use these methods on `bevy_audio`'s `AudioSink` and
`SpatialAudioSink` components to manage the sink's mute state.
- `AudioSinkPlayback`'s `set_volume` method now takes a mutable
reference instead of an immutable one. Update your code which calls
`set_volume` on `AudioSink` and `SpatialAudioSink` components to take a
mutable reference. E.g.:

Before:

```rust
fn increase_volume(sink: Single<&AudioSink>) {
    sink.set_volume(sink.volume() + 0.1);
}
```

After:

```rust
fn increase_volume(mut sink: Single<&mut AudioSink>) {
    let current_volume = sink.volume();
    sink.set_volume(current_volume + 0.1);
}
```

- The `PlaybackSettings` component now has a `muted` field which you can
use to spawn your audio in a muted state. `PlaybackSettings` also now
has a helper method `muted` which you can use when building the
component. E.g.:

```rust
commands.spawn((
    // ...
    AudioPlayer::new(asset_server.load("sounds/Windless Slopes.ogg")),
    PlaybackSettings::LOOP.with_spatial(true).muted(),
));
```

---------

Co-authored-by: Nathan Graule <[email protected]>
  • Loading branch information
mgi388 and SolarLiner authored Dec 15, 2024
1 parent 3af0b29 commit 7749c99
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 64 deletions.
12 changes: 12 additions & 0 deletions crates/bevy_audio/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
Expand Down Expand Up @@ -100,6 +105,7 @@ impl PlaybackSettings {
volume: Volume(1.0),
speed: 1.0,
paused: false,
muted: false,
spatial: false,
spatial_scale: None,
};
Expand Down Expand Up @@ -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;
Expand Down
90 changes: 45 additions & 45 deletions crates/bevy_audio/src/audio_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
///
Expand Down Expand Up @@ -157,6 +157,19 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
}
};

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);

Expand All @@ -165,28 +178,15 @@ 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 });
}
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) {
Expand All @@ -197,6 +197,19 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
}
};

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);

Expand All @@ -205,28 +218,15 @@ 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 });
}
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)),
};
}
}
Expand Down
Loading

0 comments on commit 7749c99

Please sign in to comment.