Bevy Audio
Bevy's audio system has us covered on the basics. We can play sounds and control volume, even spatially.
There are a few main pieces that make up audio in Bevy:
AudioSourceis an asset type that holds the audio dataAudioPlayeris a component that we spawn to play an audio assetPlaybackSettingsis the component anAudioPlayerrequires, which configures how the sound plays (including whether it is spatial)AudioSinkis the component Bevy adds to control a playing soundSpatialAudioSinkis the component Bevy adds to control a playing spatial sound
Rodio is the library Bevy uses to manage sound playback. An AudioSink wraps rodio::Player, and a SpatialAudioSink wraps rodio::SpatialPlayer.
Playing simple tones
The most simple way to get some audio playing is to use a Pitch asset which will play a single tone at a specific frequency.
We only need to spawn an entity with an AudioPlayer (its required PlaybackSettings component is added automatically). An AudioPlayer will add an AudioSink component to the entity when the asset is loaded and playback begins.
use bevy::prelude::*;
use std::time::Duration;
fn play_pitch(mut pitch_assets: ResMut<Assets<Pitch>>, mut commands: Commands) {
info!("playing pitch with frequency: {}", 220.0);
commands.spawn((
AudioPlayer(pitch_assets.add(Pitch::new(220.0, Duration::new(1, 0)))),
PlaybackSettings::DESPAWN,
));
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, play_pitch)
.run();
}
This system spawns a Pitch that plays a single 220Hz tone for 1 second and then despawns. Because it has no spatial settings, it plays as a flat sound that goes straight to your headphones without any positional effects.
Playing audio
Audio from an asset works in a similar way. We need to spawn an entity with an AudioPlayer component that takes a Handle<T> of what we want to play.
use bevy::prelude::*;
fn play_background_audio(
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
let audio = asset_server.load("background_audio.ogg");
// Create an entity dedicated to playing our background music
commands.spawn((AudioPlayer::new(audio), PlaybackSettings::LOOP));
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
// We only want to spawn a single player once at startup
.add_systems(Startup, play_background_audio)
.run();
}
Once the asset is loaded the music will start playing in a loop until this entity we spawned is despawned or the component is removed.
The data must be one of the file formats supported by Bevy:
wavogg(and the relatedogaandspx)flacmp3
Bevy enables the vorbis codec by default (as part of the audio feature collection), so ogg, oga, and spx files work out of the box. For additional audio formats we need to enable the matching feature in our Cargo.toml:
[dependencies]
bevy = { version = "0.19", features = ["mp3"] }
The supported format features are wav, flac, mp3, aac, and mp4. If we want them all, we enable the audio-all-formats feature collection.
There are a few different playback settings that are built in:
| Setting | Description |
|---|---|
PlaybackSettings::ONCE | Will play the associated audio only once |
PlaybackSettings::LOOP | Will loop the audio |
PlaybackSettings::DESPAWN | Will play the audio once then despawn the entity |
PlaybackSettings::REMOVE | Will play the audio once then remove the audio components from the entity |
Controlling playback
To control the playback of our AudioPlayer we can query the AudioSink that Bevy automatically adds to the entity once the asset is loaded and playback begins. Spatial sounds get a SpatialAudioSink instead:
use bevy::prelude::*;
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
MusicBox,
AudioPlayer::new(asset_server.load("background_audio.ogg")),
));
}
fn pause(
keyboard_input: Res<ButtonInput<KeyCode>>,
music_controller: Query<&AudioSink, With<MusicBox>>,
) {
let Ok(sink) = music_controller.single() else {
return;
};
if keyboard_input.just_pressed(KeyCode::Space) {
sink.toggle_playback();
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, pause)
.run();
}
You can use the AudioSink (or SpatialAudioSink) public interface to:
| Method | Description |
|---|---|
play | Resumes playback |
pause | Pause playback |
stop | Stop the playback, cannot be restarted after |
volume | Get the current volume |
set_volume | Set the volume (requires &mut AudioSink) |
mute | Mute the playback (requires &mut AudioSink) |
unmute | Unmute the playback (requires &mut AudioSink) |
toggle_playback | Toggle the playback |
toggle_mute | Toggle muting the playback (requires &mut AudioSink) |
is_paused | Returns true if the sink is paused |
is_muted | Returns true if the sink is muted |
speed | Get the speed of the sound |
set_speed | Control the speed of the playback |
position | Get the current playback position |
empty | Returns true if the sink has no more sounds to play |
try_seek | Seek to a certain point in the source sound |
These methods come from the AudioSinkPlayback trait, which is implemented by both AudioSink and SpatialAudioSink.
Volume
There are two separate sources of volume for our apps:
- Global volume
- Audio sink volume
To change the global volume we modify the GlobalVolume resource:
use bevy::{
audio::{GlobalVolume, Volume},
prelude::*,
};
fn change_global_volume(mut volume: ResMut<GlobalVolume>) {
volume.volume = Volume::Linear(0.5);
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(GlobalVolume::new(Volume::Linear(0.2)))
.add_systems(Startup, change_global_volume)
.run();
}
Note that changing GlobalVolume does not affect audio that is already playing. The new value is only applied when a sink is created, and the same is true for changing PlaybackSettings on a playing entity.
Then for the individual audio sinks we can use their public interface within our systems to modify their individual values:
use bevy::prelude::*;
#[derive(Component)]
struct MusicBox;
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
MusicBox,
AudioPlayer::new(asset_server.load("background_audio.ogg")),
));
}
fn volume_system(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut sink: Single<&mut AudioSink, With<MusicBox>>,
) {
let current_volume = sink.volume();
if keyboard_input.just_pressed(KeyCode::Equal) {
sink.set_volume(current_volume.increase_by_percentage(10.0));
} else if keyboard_input.just_pressed(KeyCode::Minus) {
sink.set_volume(current_volume.decrease_by_percentage(10.0));
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, volume_system)
.run();
}
Spatial audio
By default, sounds play as flat, unmodified audio that is unaffected by position. To make a sound spatial we set spatial: true on its PlaybackSettings (or use the .with_spatial(true) builder). The entity also needs a Transform so Bevy knows where the sound is coming from.
To change our spatial audio settings globally we can set the audio plugin settings globally by overriding them in our DefaultPlugins:
use bevy::{
audio::{AudioPlugin, SpatialScale},
prelude::*,
};
const AUDIO_SCALE: f32 = 1. / 100.;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(AudioPlugin {
default_spatial_scale: SpatialScale::new_2d(AUDIO_SCALE),
..default()
}))
.run();
}
Then to play our sounds we can add a listener. A SpatialListener component will modify the sound we hear relative to whatever entity is emitting the sound:
use bevy::{
audio::{AudioPlugin, SpatialScale},
prelude::*,
};
#[derive(Component)]
struct Player;
fn play_2d_spatial_audio(
mut commands: Commands,
asset_server: Res<AssetServer>,
) {
// Spawn our emitter, offset to the right so it is audible in the right ear
commands.spawn((
Player,
AudioPlayer::new(asset_server.load("flight_of_the_valkaries.ogg")),
PlaybackSettings::LOOP.with_spatial(true),
Transform::from_xyz(100.0, 0.0, 0.0),
));
// Spawn our listener
commands.spawn((
SpatialListener::new(100.), // Gap between the ears
Transform::default(),
));
}
This will spawn a player entity with the sound emitting from their position. So for example, other players around could hear it according to how far away from us they are.
Only a single SpatialListener should exist in your app at any given time.
Internals
Internally, Bevy is using rodio to decode these sources.
The AudioPlayer holds the handle to the asset to play. Its settings live on the required PlaybackSettings component rather than on AudioPlayer itself:
#[derive(Component, Reflect, FromTemplate)]
#[reflect(Component, Clone)]
#[require(PlaybackSettings)]
pub struct AudioPlayer<Source = AudioSource>(pub Handle<Source>)
where
Source: Asset + Decodable;
The Decodable trait is what allows Bevy to convert the source file into a rodio compatible rodio::Source type. Types that implement this trait will hold raw sound data that is then converted into an iterator of samples.