Tainted\\Coders

Bevy Audio

Bevy version: 0.19Last updated:

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:

  1. AudioSource is an asset type that holds the audio data
  2. AudioPlayer is a component that we spawn to play an audio asset
  3. PlaybackSettings is the component an AudioPlayer requires, which configures how the sound plays (including whether it is spatial)
  4. AudioSink is the component Bevy adds to control a playing sound
  5. SpatialAudioSink is 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:

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:

SettingDescription
PlaybackSettings::ONCEWill play the associated audio only once
PlaybackSettings::LOOPWill loop the audio
PlaybackSettings::DESPAWNWill play the audio once then despawn the entity
PlaybackSettings::REMOVEWill 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:

MethodDescription
playResumes playback
pausePause playback
stopStop the playback, cannot be restarted after
volumeGet the current volume
set_volumeSet the volume (requires &mut AudioSink)
muteMute the playback (requires &mut AudioSink)
unmuteUnmute the playback (requires &mut AudioSink)
toggle_playbackToggle the playback
toggle_muteToggle muting the playback (requires &mut AudioSink)
is_pausedReturns true if the sink is paused
is_mutedReturns true if the sink is muted
speedGet the speed of the sound
set_speedControl the speed of the playback
positionGet the current playback position
emptyReturns true if the sink has no more sounds to play
try_seekSeek 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:

  1. Global volume
  2. 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.