Tainted\\Coders

Bevy Audio

Bevy version: 0.15Last updated:

Bevy's audio system has you covered on the basics. You can play sounds and control volume, even spatially.

Audio is controlled through the entity component system by adding certain components to our entities that the AudioPlugin uses to play our sounds.

AudioSource holds the audio data and is connected to an AudioSink which is usually done by spawning an AudioPlayer

Playing audio

We can trigger our sounds to play by spawning an AudioPlayer on any entity.

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)
    .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 actual playing of this audio happens in a system that was added in the AudioPlugin. When the audio playback begins, the system adds an AudioSink to the AudioPlayer we just added which we use to control the playback.

The data must be one of the file formats supported by Bevy:

Bevy will include ogg by default, but for additional audio formats you will need to include the feature inside your Cargo.toml:

[dependencies]
bevy = { version = "0.15", features = ["mp3"] }

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 despawn the component

Controlling playback

To control the playback of our AudioPlayer we can use the AudioSink which was added by the AudioPlugin when we spawned our entity:

fn volume_system(
  keyboard_input: Res<ButtonInput<KeyCode>>,
  music_box_query: Query<&AudioSink, With<MusicBox>>
) {
  if let Ok(sink) = music_box_query.get_single() {
    if keyboard_input.just_pressed(KeyCode::Equal) {
      sink.set_volume(sink.volume() + 0.1);
    } else if keyboard_input.just_pressed(KeyCode::Minus) {
      sink.set_volume(sink.volume() - 0.1);
    }
  }
}

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    .add_systems(Startup, play_background_audio)
    .add_systems(Update, volume_system)
    .run();
}

Your audio sink can also set_speed and toggle to play or stop the audio.

Spatial audio

The example above will play a flat unmodified sound for whatever source we feed our bundle.

To change our spatial audio settings globally we can set the audio plugin settings:

use bevy::audio::{SpatialScale, AudioPlugin};
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 with a SpatialListener component and move them relative to whatever entity is emitting the sound:

fn play_2d_spatial_audio(
  mut commands: Commands,
  asset_server: Res<AssetServer>,
) {
  // Spawn our emitter
  commands.spawn((
    Player,
    AudioPlayer::new(asset_server.load("flight_of_the_valkaries.ogg")),
    PlaybackSettings::LOOP
  ));

  // 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.

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::Volume;

fn change_global_volume(
  mut volume: ResMut<GlobalVolume>,
) {
  volume.volume = Volume::new(0.5);
}

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    .insert_resource(GlobalVolume::new(0.2))
    .add_systems(Startup, change_global_volume)
    .run();
}

Then for the individual audio sinks we can use their public interface within our systems to modify their individual values:

fn volume_system(
  keyboard_input: Res<ButtonInput<KeyCode>>,
  music_box_query: Query<&AudioSink, With<MusicBox>>
) {
  if let Ok(sink) = music_box_query.get_single() {
    if keyboard_input.just_pressed(KeyCode::Equal) {
      sink.set_volume(sink.volume() + 0.1);
    } else if keyboard_input.just_pressed(KeyCode::Minus) {
      sink.set_volume(sink.volume() - 0.1);
    }
  }
}

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    .add_systems(Update, volume_system)
    .run();
}

Internals

Internally, Bevy is using rodio to decode these sources.

The AudioPlayer is made up of both a source and some settings which controls the playback:

#[derive(Component, Reflect)]
#[reflect(Component)]
#[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.