
# 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 <solarliner@gmail.com>
156 lines
4.8 KiB
Rust
156 lines
4.8 KiB
Rust
//! This example illustrates how to load and play different soundtracks,
|
|
//! transitioning between them as the game state changes.
|
|
|
|
use bevy::prelude::*;
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, (cycle_game_state, fade_in, fade_out))
|
|
.add_systems(Update, change_track)
|
|
.run();
|
|
}
|
|
|
|
// This resource simulates game states
|
|
#[derive(Resource, Default)]
|
|
enum GameState {
|
|
#[default]
|
|
Peaceful,
|
|
Battle,
|
|
}
|
|
|
|
// This timer simulates game state changes
|
|
#[derive(Resource)]
|
|
struct GameStateTimer(Timer);
|
|
|
|
// This resource will hold the track list for your soundtrack
|
|
#[derive(Resource)]
|
|
struct SoundtrackPlayer {
|
|
track_list: Vec<Handle<AudioSource>>,
|
|
}
|
|
|
|
impl SoundtrackPlayer {
|
|
fn new(track_list: Vec<Handle<AudioSource>>) -> Self {
|
|
Self { track_list }
|
|
}
|
|
}
|
|
|
|
// This component will be attached to an entity to fade the audio in
|
|
#[derive(Component)]
|
|
struct FadeIn;
|
|
|
|
// This component will be attached to an entity to fade the audio out
|
|
#[derive(Component)]
|
|
struct FadeOut;
|
|
|
|
fn setup(asset_server: Res<AssetServer>, mut commands: Commands) {
|
|
// Instantiate the game state resources
|
|
commands.insert_resource(GameState::default());
|
|
commands.insert_resource(GameStateTimer(Timer::from_seconds(
|
|
10.0,
|
|
TimerMode::Repeating,
|
|
)));
|
|
|
|
// Create the track list
|
|
let track_1 = asset_server.load::<AudioSource>("sounds/Mysterious acoustic guitar.ogg");
|
|
let track_2 = asset_server.load::<AudioSource>("sounds/Epic orchestra music.ogg");
|
|
let track_list = vec![track_1, track_2];
|
|
commands.insert_resource(SoundtrackPlayer::new(track_list));
|
|
}
|
|
|
|
// Every time the GameState resource changes, this system is run to trigger the song change.
|
|
fn change_track(
|
|
mut commands: Commands,
|
|
soundtrack_player: Res<SoundtrackPlayer>,
|
|
soundtrack: Query<Entity, With<AudioSink>>,
|
|
game_state: Res<GameState>,
|
|
) {
|
|
if game_state.is_changed() {
|
|
// Fade out all currently running tracks
|
|
for track in soundtrack.iter() {
|
|
commands.entity(track).insert(FadeOut);
|
|
}
|
|
|
|
// Spawn a new `AudioPlayer` with the appropriate soundtrack based on
|
|
// the game state.
|
|
//
|
|
// Volume is set to start at zero and is then increased by the fade_in system.
|
|
match game_state.as_ref() {
|
|
GameState::Peaceful => {
|
|
commands.spawn((
|
|
AudioPlayer(soundtrack_player.track_list.first().unwrap().clone()),
|
|
PlaybackSettings {
|
|
mode: bevy::audio::PlaybackMode::Loop,
|
|
volume: bevy::audio::Volume::ZERO,
|
|
..default()
|
|
},
|
|
FadeIn,
|
|
));
|
|
}
|
|
GameState::Battle => {
|
|
commands.spawn((
|
|
AudioPlayer(soundtrack_player.track_list.get(1).unwrap().clone()),
|
|
PlaybackSettings {
|
|
mode: bevy::audio::PlaybackMode::Loop,
|
|
volume: bevy::audio::Volume::ZERO,
|
|
..default()
|
|
},
|
|
FadeIn,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fade effect duration
|
|
const FADE_TIME: f32 = 2.0;
|
|
|
|
// Fades in the audio of entities that has the FadeIn component. Removes the FadeIn component once
|
|
// full volume is reached.
|
|
fn fade_in(
|
|
mut commands: Commands,
|
|
mut audio_sink: Query<(&mut AudioSink, Entity), With<FadeIn>>,
|
|
time: Res<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>();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fades out the audio of entities that has the FadeOut component. Despawns the entities once audio
|
|
// volume reaches zero.
|
|
fn fade_out(
|
|
mut commands: Commands,
|
|
mut audio_sink: Query<(&mut AudioSink, Entity), With<FadeOut>>,
|
|
time: Res<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();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Every time the timer ends, switches between the "Peaceful" and "Battle" state.
|
|
fn cycle_game_state(
|
|
mut timer: ResMut<GameStateTimer>,
|
|
mut game_state: ResMut<GameState>,
|
|
time: Res<Time>,
|
|
) {
|
|
timer.0.tick(time.delta());
|
|
if timer.0.just_finished() {
|
|
match game_state.as_ref() {
|
|
GameState::Battle => *game_state = GameState::Peaceful,
|
|
GameState::Peaceful => *game_state = GameState::Battle,
|
|
}
|
|
}
|
|
}
|