bevy_audio: ECS-based API redesign (#8424)

# Objective

Improve the `bevy_audio` API to make it more user-friendly and
ECS-idiomatic. This PR is a first-pass at addressing some of the most
obvious (to me) problems. In the interest of keeping the scope small,
further improvements can be done in future PRs.

The current `bevy_audio` API is very clunky to work with, due to how it
(ab)uses bevy assets to represent audio sinks.

The user needs to write a lot of boilerplate (accessing
`Res<Assets<AudioSink>>`) and deal with a lot of cognitive overhead
(worry about strong vs. weak handles, etc.) in order to control audio
playback.

Audio playback is initiated via a centralized `Audio` resource, which
makes it difficult to keep track of many different sounds playing in a
typical game.

Further, everything carries a generic type parameter for the sound
source type, making it difficult to mix custom sound sources (such as
procedurally generated audio or unofficial formats) with regular audio
assets.

Let's fix these issues.

## Solution

Refactor `bevy_audio` to a more idiomatic ECS API. Remove the `Audio`
resource. Do everything via entities and components instead.

Audio playback data is now stored in components:
- `PlaybackSettings`, `SpatialSettings`, `Handle<AudioSource>` are now
components. The user inserts them to tell Bevy to play a sound and
configure the initial playback parameters.
- `AudioSink`, `SpatialAudioSink` are now components instead of special
magical "asset" types. They are inserted by Bevy when it actually begins
playing the sound, and can be queried for by the user in order to
control the sound during playback.

Bundles: `AudioBundle` and `SpatialAudioBundle` are available to make it
easy for users to play sounds. Spawn an entity with one of these bundles
(or insert them to a complex entity alongside other stuff) to play a
sound.

Each entity represents a sound to be played.

There is also a new "auto-despawn" feature (activated using
`PlaybackSettings`), which, if enabled, tells Bevy to despawn entities
when the sink playback finishes. This allows for "fire-and-forget" sound
playback. Users can simply
spawn entities whenever they want to play sounds and not have to worry
about leaking memory.

## Unsolved Questions

I think the current design is *fine*. I'd be happy for it to be merged.
It has some possibly-surprising usability pitfalls, but I think it is
still much better than the old `bevy_audio`. Here are some discussion
questions for things that we could further improve. I'm undecided on
these questions, which is why I didn't implement them. We should decide
which of these should be addressed in this PR, and what should be left
for future PRs. Or if they should be addressed at all.

### What happens when sounds start playing?

Currently, the audio sink components are inserted and the bundle
components are kept. Should Bevy remove the bundle components? Something
else?

The current design allows an entity to be reused for playing the same
sound with the same parameters repeatedly. This is a niche use case I'd
like to be supported, but if we have to give it up for a simpler design,
I'd be fine with that.

### What happens if users remove any of the components themselves?

As described above, currently, entities can be reused. Removing the
audio sink causes it to be "detached" (I kept the old `Drop` impl), so
the sound keeps playing. However, if the audio bundle components are not
removed, Bevy will detect this entity as a "queued" sound entity again
(has the bundle compoenents, without a sink component), just like before
playing the sound the first time, and start playing the sound again.

This behavior might be surprising? Should we do something different?

### Should mutations to `PlaybackSettings` be applied to the audio sink?

We currently do not do that. `PlaybackSettings` is just for the initial
settings when the sound starts playing. This is clearly documented.

Do we want to keep this behavior, or do we want to allow users to use
`PlaybackSettings` instead of `AudioSink`/`SpatialAudioSink` to control
sounds during playback too?

I think I prefer for them to be kept separate. It is not a bad mental
model once you understand it, and it is documented.

### Should `AudioSink` and `SpatialAudioSink` be unified into a single
component type?

They provide a similar API (via the `AudioSinkPlayback` trait) and it
might be annoying for users to have to deal with both of them. The
unification could be done using an enum that is matched on internally by
the methods. Spatial audio has extra features, so this might make it
harder to access. I think we shouldn't.

### Automatic synchronization of spatial sound properties from
Transforms?

Should Bevy automatically apply changes to Transforms to spatial audio
entities? How do we distinguish between listener and emitter? Which one
does the transform represent? Where should the other one come from?

Alternatively, leave this problem for now, and address it in a future
PR. Or do nothing, and let users deal with it, as shown in the
`spatial_audio_2d` and `spatial_audio_3d` examples.

---

## Changelog

Added:
- `AudioBundle`/`SpatialAudioBundle`, add them to entities to play
sounds.

Removed:
 - The `Audio` resource.
 - `AudioOutput` is no longer `pub`.

Changed:
 - `AudioSink`, `SpatialAudioSink` are now components instead of assets.

## Migration Guide

// TODO: write a more detailed migration guide, after the "unsolved
questions" are answered and this PR is finalized.

Before:

```rust

/// Need to store handles somewhere
#[derive(Resource)]
struct MyMusic {
    sink: Handle<AudioSink>,
}

fn play_music(
    asset_server: Res<AssetServer>,
    audio: Res<Audio>,
    audio_sinks: Res<Assets<AudioSink>>,
    mut commands: Commands,
) {
    let weak_handle = audio.play_with_settings(
        asset_server.load("music.ogg"),
        PlaybackSettings::LOOP.with_volume(0.5),
    );
    // upgrade to strong handle and store it
    commands.insert_resource(MyMusic {
        sink: audio_sinks.get_handle(weak_handle),
    });
}

fn toggle_pause_music(
    audio_sinks: Res<Assets<AudioSink>>,
    mymusic: Option<Res<MyMusic>>,
) {
    if let Some(mymusic) = &mymusic {
        if let Some(sink) = audio_sinks.get(&mymusic.sink) {
            sink.toggle();
        }
    }
}
```

Now:

```rust
/// Marker component for our music entity
#[derive(Component)]
struct MyMusic;

fn play_music(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
) {
    commands.spawn((
        AudioBundle::from_audio_source(asset_server.load("music.ogg"))
            .with_settings(PlaybackSettings::LOOP.with_volume(0.5)),
        MyMusic,
    ));
}

fn toggle_pause_music(
    // `AudioSink` will be inserted by Bevy when the audio starts playing
    query_music: Query<&AudioSink, With<MyMusic>>,
) {
    if let Ok(sink) = query.get_single() {
        sink.toggle();
    }
}
```
This commit is contained in:
Ida "Iyes 2023-07-08 02:01:17 +03:00 committed by GitHub
parent 95ade6d6a0
commit fb4c21e3e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 452 additions and 569 deletions

View File

@ -1,256 +1,9 @@
use crate::{AudioSink, AudioSource, Decodable, SpatialAudioSink};
use bevy_asset::{Asset, Handle, HandleId};
use crate::{AudioSource, Decodable};
use bevy_asset::{Asset, Handle};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::system::Resource;
use bevy_ecs::prelude::*;
use bevy_math::Vec3;
use bevy_transform::prelude::Transform;
use parking_lot::RwLock;
use std::{collections::VecDeque, fmt};
/// Use this [`Resource`] to play audio.
///
/// ```
/// # use bevy_ecs::system::Res;
/// # use bevy_asset::AssetServer;
/// # use bevy_audio::Audio;
/// fn play_audio_system(asset_server: Res<AssetServer>, audio: Res<Audio>) {
/// audio.play(asset_server.load("my_sound.ogg"));
/// }
/// ```
#[derive(Resource)]
pub struct Audio<Source = AudioSource>
where
Source: Asset + Decodable,
{
/// Queue for playing audio from asset handles
pub(crate) queue: RwLock<VecDeque<AudioToPlay<Source>>>,
}
impl<Source: Asset> fmt::Debug for Audio<Source>
where
Source: Decodable,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Audio").field("queue", &self.queue).finish()
}
}
impl<Source> Default for Audio<Source>
where
Source: Asset + Decodable,
{
fn default() -> Self {
Self {
queue: Default::default(),
}
}
}
impl<Source> Audio<Source>
where
Source: Asset + Decodable,
{
/// Play audio from a [`Handle`] to the audio source
///
/// ```
/// # use bevy_ecs::system::Res;
/// # use bevy_asset::AssetServer;
/// # use bevy_audio::Audio;
/// fn play_audio_system(asset_server: Res<AssetServer>, audio: Res<Audio>) {
/// audio.play(asset_server.load("my_sound.ogg"));
/// }
/// ```
///
/// Returns a weak [`Handle`] to the [`AudioSink`]. If this handle isn't changed to a
/// strong one, the sink will be detached and the sound will continue playing. Changing it
/// to a strong handle allows you to control the playback through the [`AudioSink`] asset.
///
/// ```
/// # use bevy_ecs::system::Res;
/// # use bevy_asset::{AssetServer, Assets};
/// # use bevy_audio::{Audio, AudioSink};
/// fn play_audio_system(
/// asset_server: Res<AssetServer>,
/// audio: Res<Audio>,
/// audio_sinks: Res<Assets<AudioSink>>,
/// ) {
/// // This is a weak handle, and can't be used to control playback.
/// let weak_handle = audio.play(asset_server.load("my_sound.ogg"));
/// // This is now a strong handle, and can be used to control playback.
/// let strong_handle = audio_sinks.get_handle(weak_handle);
/// }
/// ```
pub fn play(&self, audio_source: Handle<Source>) -> Handle<AudioSink> {
let id = HandleId::random::<AudioSink>();
let config = AudioToPlay {
settings: PlaybackSettings::ONCE,
sink_handle: id,
source_handle: audio_source,
spatial: None,
};
self.queue.write().push_back(config);
Handle::<AudioSink>::weak(id)
}
/// Play audio from a [`Handle`] to the audio source with [`PlaybackSettings`] that
/// allows looping or changing volume from the start.
///
/// ```
/// # use bevy_ecs::system::Res;
/// # use bevy_asset::AssetServer;
/// # use bevy_audio::{Audio, Volume};
/// # use bevy_audio::PlaybackSettings;
/// fn play_audio_system(asset_server: Res<AssetServer>, audio: Res<Audio>) {
/// audio.play_with_settings(
/// asset_server.load("my_sound.ogg"),
/// PlaybackSettings::LOOP.with_volume(Volume::new_relative(0.75)),
/// );
/// }
/// ```
///
/// See [`Self::play`] on how to control playback once it's started.
pub fn play_with_settings(
&self,
audio_source: Handle<Source>,
settings: PlaybackSettings,
) -> Handle<AudioSink> {
let id = HandleId::random::<AudioSink>();
let config = AudioToPlay {
settings,
sink_handle: id,
source_handle: audio_source,
spatial: None,
};
self.queue.write().push_back(config);
Handle::<AudioSink>::weak(id)
}
/// Play audio from a [`Handle`] to the audio source, placing the listener at the given
/// transform, an ear on each side separated by `gap`. The audio emitter will placed at
/// `emitter`.
///
/// `bevy_audio` is not using HRTF for spatial audio, but is transforming the sound to a mono
/// track, and then changing the level of each stereo channel according to the distance between
/// the emitter and each ear by amplifying the difference between what the two ears hear.
///
/// ```
/// # use bevy_ecs::system::Res;
/// # use bevy_asset::AssetServer;
/// # use bevy_audio::Audio;
/// # use bevy_math::Vec3;
/// # use bevy_transform::prelude::Transform;
/// fn play_spatial_audio_system(asset_server: Res<AssetServer>, audio: Res<Audio>) {
/// // Sound will be to the left and behind the listener
/// audio.play_spatial(
/// asset_server.load("my_sound.ogg"),
/// Transform::IDENTITY,
/// 1.0,
/// Vec3::new(-2.0, 0.0, 1.0),
/// );
/// }
/// ```
///
/// Returns a weak [`Handle`] to the [`SpatialAudioSink`]. If this handle isn't changed to a
/// strong one, the sink will be detached and the sound will continue playing. Changing it
/// to a strong handle allows you to control the playback, or move the listener and emitter
/// through the [`SpatialAudioSink`] asset.
///
/// ```
/// # use bevy_ecs::system::Res;
/// # use bevy_asset::{AssetServer, Assets};
/// # use bevy_audio::{Audio, SpatialAudioSink};
/// # use bevy_math::Vec3;
/// # use bevy_transform::prelude::Transform;
/// fn play_spatial_audio_system(
/// asset_server: Res<AssetServer>,
/// audio: Res<Audio>,
/// spatial_audio_sinks: Res<Assets<SpatialAudioSink>>,
/// ) {
/// // This is a weak handle, and can't be used to control playback.
/// let weak_handle = audio.play_spatial(
/// asset_server.load("my_sound.ogg"),
/// Transform::IDENTITY,
/// 1.0,
/// Vec3::new(-2.0, 0.0, 1.0),
/// );
/// // This is now a strong handle, and can be used to control playback, or move the emitter.
/// let strong_handle = spatial_audio_sinks.get_handle(weak_handle);
/// }
/// ```
pub fn play_spatial(
&self,
audio_source: Handle<Source>,
listener: Transform,
gap: f32,
emitter: Vec3,
) -> Handle<SpatialAudioSink> {
let id = HandleId::random::<SpatialAudioSink>();
let config = AudioToPlay {
settings: PlaybackSettings::ONCE,
sink_handle: id,
source_handle: audio_source,
spatial: Some(SpatialSettings {
left_ear: (listener.translation + listener.left() * gap / 2.0).to_array(),
right_ear: (listener.translation + listener.right() * gap / 2.0).to_array(),
emitter: emitter.to_array(),
}),
};
self.queue.write().push_back(config);
Handle::<SpatialAudioSink>::weak(id)
}
/// Play spatial audio from a [`Handle`] to the audio source with [`PlaybackSettings`] that
/// allows looping or changing volume from the start. The listener is placed at the given
/// transform, an ear on each side separated by `gap`. The audio emitter is placed at
/// `emitter`.
///
/// `bevy_audio` is not using HRTF for spatial audio, but is transforming the sound to a mono
/// track, and then changing the level of each stereo channel according to the distance between
/// the emitter and each ear by amplifying the difference between what the two ears hear.
///
/// ```
/// # use bevy_ecs::system::Res;
/// # use bevy_asset::AssetServer;
/// # use bevy_audio::{Audio, Volume};
/// # use bevy_audio::PlaybackSettings;
/// # use bevy_math::Vec3;
/// # use bevy_transform::prelude::Transform;
/// fn play_spatial_audio_system(asset_server: Res<AssetServer>, audio: Res<Audio>) {
/// audio.play_spatial_with_settings(
/// asset_server.load("my_sound.ogg"),
/// PlaybackSettings::LOOP.with_volume(Volume::new_relative(0.75)),
/// Transform::IDENTITY,
/// 1.0,
/// Vec3::new(-2.0, 0.0, 1.0),
/// );
/// }
/// ```
///
/// See [`Self::play_spatial`] on how to control playback once it's started, or how to move
/// the listener or the emitter.
pub fn play_spatial_with_settings(
&self,
audio_source: Handle<Source>,
settings: PlaybackSettings,
listener: Transform,
gap: f32,
emitter: Vec3,
) -> Handle<SpatialAudioSink> {
let id = HandleId::random::<SpatialAudioSink>();
let config = AudioToPlay {
settings,
sink_handle: id,
source_handle: audio_source,
spatial: Some(SpatialSettings {
left_ear: (listener.translation + listener.left() * gap / 2.0).to_array(),
right_ear: (listener.translation + listener.right() * gap / 2.0).to_array(),
emitter: emitter.to_array(),
}),
};
self.queue.write().push_back(config);
Handle::<SpatialAudioSink>::weak(id)
}
}
/// Defines the volume to play an audio source at.
#[derive(Clone, Copy, Debug)]
@ -278,7 +31,7 @@ impl Volume {
}
}
/// A volume level equivalent to a positive only float.
/// A volume level equivalent to a non-negative float.
#[derive(Clone, Copy, Deref, DerefMut, Debug)]
pub struct VolumeLevel(pub(crate) f32);
@ -300,38 +53,83 @@ impl VolumeLevel {
}
}
/// Settings to control playback from the start.
#[derive(Clone, Copy, Debug)]
/// How should Bevy manage the sound playback?
#[derive(Debug, Clone, Copy)]
pub enum PlaybackMode {
/// Play the sound once. Do nothing when it ends.
Once,
/// Repeat the sound forever.
Loop,
/// Despawn the entity when the sound finishes playing.
Despawn,
/// Remove the audio components from the entity, when the sound finishes playing.
Remove,
}
/// Initial settings to be used when audio starts playing.
/// If you would like to control the audio while it is playing, query for the
/// [`AudioSink`][crate::AudioSink] or [`SpatialAudioSink`][crate::SpatialAudioSink]
/// components. Changes to this component will *not* be applied to already-playing audio.
#[derive(Component, Clone, Copy, Debug)]
pub struct PlaybackSettings {
/// Play in repeat
pub repeat: bool,
/// The desired playback behavior.
pub mode: PlaybackMode,
/// Volume to play at.
pub volume: Volume,
/// Speed to play at.
pub speed: f32,
/// Create the sink in paused state.
/// Useful for "deferred playback", if you want to prepare
/// the entity, but hear the sound later.
pub paused: bool,
}
impl Default for PlaybackSettings {
fn default() -> Self {
// TODO: what should the default be: ONCE/DESPAWN/REMOVE?
Self::ONCE
}
}
impl PlaybackSettings {
/// Will play the associate audio source once.
/// Will play the associated audio source once.
pub const ONCE: PlaybackSettings = PlaybackSettings {
repeat: false,
mode: PlaybackMode::Once,
volume: Volume::Relative(VolumeLevel(1.0)),
speed: 1.0,
paused: false,
};
/// Will play the associate audio source in a loop.
/// Will play the associated audio source in a loop.
pub const LOOP: PlaybackSettings = PlaybackSettings {
repeat: true,
mode: PlaybackMode::Loop,
volume: Volume::Relative(VolumeLevel(1.0)),
speed: 1.0,
paused: false,
};
/// Will play the associated audio source once and despawn the entity afterwards.
pub const DESPAWN: PlaybackSettings = PlaybackSettings {
mode: PlaybackMode::Despawn,
volume: Volume::Relative(VolumeLevel(1.0)),
speed: 1.0,
paused: false,
};
/// Will play the associated audio source once and remove the audio components afterwards.
pub const REMOVE: PlaybackSettings = PlaybackSettings {
mode: PlaybackMode::Remove,
volume: Volume::Relative(VolumeLevel(1.0)),
speed: 1.0,
paused: false,
};
/// Helper to start in a paused state.
pub const fn paused(mut self) -> Self {
self.paused = true;
self
}
/// Helper to set the volume from start of playback.
pub const fn with_volume(mut self, volume: Volume) -> Self {
self.volume = volume;
@ -345,40 +143,35 @@ impl PlaybackSettings {
}
}
#[derive(Clone)]
pub(crate) struct SpatialSettings {
/// Settings for playing spatial audio.
///
/// Note: Bevy does not currently support HRTF or any other high-quality 3D sound rendering
/// features. Spatial audio is implemented via simple left-right stereo panning.
#[derive(Component, Clone, Debug)]
pub struct SpatialSettings {
pub(crate) left_ear: [f32; 3],
pub(crate) right_ear: [f32; 3],
pub(crate) emitter: [f32; 3],
}
#[derive(Clone)]
pub(crate) struct AudioToPlay<Source>
where
Source: Asset + Decodable,
{
pub(crate) sink_handle: HandleId,
pub(crate) source_handle: Handle<Source>,
pub(crate) settings: PlaybackSettings,
pub(crate) spatial: Option<SpatialSettings>,
}
impl<Source> fmt::Debug for AudioToPlay<Source>
where
Source: Asset + Decodable,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AudioToPlay")
.field("sink_handle", &self.sink_handle)
.field("source_handle", &self.source_handle)
.field("settings", &self.settings)
.finish()
impl SpatialSettings {
/// Configure spatial audio coming from the `emitter` position and heard by a `listener`.
///
/// The `listener` transform provides the position and rotation where the sound is to be
/// heard from. `gap` is the distance between the left and right "ears" of the listener.
/// `emitter` is the position where the sound comes from.
pub fn new(listener: Transform, gap: f32, emitter: Vec3) -> Self {
SpatialSettings {
left_ear: (listener.translation + listener.left() * gap / 2.0).to_array(),
right_ear: (listener.translation + listener.right() * gap / 2.0).to_array(),
emitter: emitter.to_array(),
}
}
}
/// Use this [`Resource`] to control the global volume of all audio with a [`Volume::Relative`] volume.
///
/// Keep in mind that changing this value will not affect already playing audio.
/// Note: changing this value will not affect already playing audio.
#[derive(Resource, Default, Clone, Copy)]
pub struct GlobalVolume {
/// The global volume of all audio.
@ -393,3 +186,67 @@ impl GlobalVolume {
}
}
}
/// Bundle for playing a standard bevy audio asset
pub type AudioBundle = AudioSourceBundle<AudioSource>;
/// Bundle for playing a standard bevy audio asset with a 3D position
pub type SpatialAudioBundle = SpatialAudioSourceBundle<AudioSource>;
/// Bundle for playing a sound.
///
/// Insert this bundle onto an entity to trigger a sound source to begin playing.
///
/// If the handle refers to an unavailable asset (such as if it has not finished loading yet),
/// the audio will not begin playing immediately. The audio will play when the asset is ready.
///
/// When Bevy begins the audio playback, an [`AudioSink`][crate::AudioSink] component will be
/// added to the entity. You can use that component to control the audio settings during playback.
#[derive(Bundle)]
pub struct AudioSourceBundle<Source = AudioSource>
where
Source: Asset + Decodable,
{
/// Asset containing the audio data to play.
pub source: Handle<Source>,
/// Initial settings that the audio starts playing with.
/// If you would like to control the audio while it is playing,
/// query for the [`AudioSink`][crate::AudioSink] component.
/// Changes to this component will *not* be applied to already-playing audio.
pub settings: PlaybackSettings,
}
impl<T: Decodable + Asset> Default for AudioSourceBundle<T> {
fn default() -> Self {
Self {
source: Default::default(),
settings: Default::default(),
}
}
}
/// Bundle for playing a sound with a 3D position.
///
/// Insert this bundle onto an entity to trigger a sound source to begin playing.
///
/// If the handle refers to an unavailable asset (such as if it has not finished loading yet),
/// the audio will not begin playing immediately. The audio will play when the asset is ready.
///
/// When Bevy begins the audio playback, a [`SpatialAudioSink`][crate::SpatialAudioSink]
/// component will be added to the entity. You can use that component to control the audio
/// settings during playback.
#[derive(Bundle)]
pub struct SpatialAudioSourceBundle<Source = AudioSource>
where
Source: Asset + Decodable,
{
/// Asset containing the audio data to play.
pub source: Handle<Source>,
/// Initial settings that the audio starts playing with.
/// If you would like to control the audio while it is playing,
/// query for the [`SpatialAudioSink`][crate::SpatialAudioSink] component.
/// Changes to this component will *not* be applied to already-playing audio.
pub settings: PlaybackSettings,
/// Spatial audio configuration. Specifies the positions of the source and listener.
pub spatial: SpatialSettings,
}

View File

@ -1,11 +1,11 @@
use crate::{
Audio, AudioSource, Decodable, GlobalVolume, SpatialAudioSink, SpatialSettings, Volume,
AudioSourceBundle, Decodable, GlobalVolume, PlaybackMode, PlaybackSettings, SpatialAudioSink,
SpatialAudioSourceBundle, SpatialSettings, Volume,
};
use bevy_asset::{Asset, Assets};
use bevy_ecs::system::{Res, ResMut, Resource};
use bevy_asset::{Asset, Assets, Handle};
use bevy_ecs::prelude::*;
use bevy_utils::tracing::warn;
use rodio::{OutputStream, OutputStreamHandle, Sink, Source, SpatialSink};
use std::marker::PhantomData;
use crate::AudioSink;
@ -22,157 +22,227 @@ use crate::AudioSink;
/// since the memory cost will be the same.
/// However, repeatedly inserting this resource into the app will **leak more memory**.
#[derive(Resource)]
pub struct AudioOutput<Source = AudioSource>
where
Source: Decodable,
{
pub(crate) struct AudioOutput {
stream_handle: Option<OutputStreamHandle>,
phantom: PhantomData<Source>,
}
impl<Source> Default for AudioOutput<Source>
where
Source: Decodable,
{
impl Default for AudioOutput {
fn default() -> Self {
if let Ok((stream, stream_handle)) = OutputStream::try_default() {
// We leak `OutputStream` to prevent the audio from stopping.
std::mem::forget(stream);
Self {
stream_handle: Some(stream_handle),
phantom: PhantomData,
}
} else {
warn!("No audio device found.");
Self {
stream_handle: None,
phantom: PhantomData,
}
}
}
}
impl<Source> AudioOutput<Source>
where
Source: Asset + Decodable,
/// Marker for internal use, to despawn entities when playback finishes.
#[derive(Component)]
pub struct PlaybackDespawnMarker;
/// Marker for internal use, to remove audio components when playback finishes.
#[derive(Component)]
pub struct PlaybackRemoveMarker;
/// Plays "queued" audio through the [`AudioOutput`] resource.
///
/// "Queued" audio is any audio entity (with the components from
/// [`AudioBundle`][crate::AudioBundle] or [`SpatialAudioBundle`][crate::SpatialAudioBundle])
/// that does not have an [`AudioSink`]/[`SpatialAudioSink`] component.
///
/// This system detects such entities, checks if their source asset
/// data is available, and creates/inserts the sink.
pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
audio_output: Res<AudioOutput>,
audio_sources: Res<Assets<Source>>,
global_volume: Res<GlobalVolume>,
query_nonplaying: Query<
(
Entity,
&Handle<Source>,
&PlaybackSettings,
Option<&SpatialSettings>,
),
(Without<AudioSink>, Without<SpatialAudioSink>),
>,
mut commands: Commands,
) where
f32: rodio::cpal::FromSample<Source::DecoderItem>,
{
fn play_source(&self, audio_source: &Source, repeat: bool) -> Option<Sink> {
self.stream_handle
.as_ref()
.and_then(|stream_handle| match Sink::try_new(stream_handle) {
Ok(sink) => {
if repeat {
sink.append(audio_source.decoder().repeat_infinite());
} else {
sink.append(audio_source.decoder());
}
Some(sink)
}
Err(err) => {
warn!("Error playing sound: {err:?}");
None
}
})
}
let Some(stream_handle) = audio_output.stream_handle.as_ref() else {
// audio output unavailable; cannot play sound
return;
};
fn play_spatial_source(
&self,
audio_source: &Source,
repeat: bool,
spatial: SpatialSettings,
) -> Option<SpatialSink> {
self.stream_handle.as_ref().and_then(|stream_handle| {
match SpatialSink::try_new(
stream_handle,
spatial.emitter,
spatial.left_ear,
spatial.right_ear,
) {
Ok(sink) => {
if repeat {
sink.append(audio_source.decoder().repeat_infinite());
} else {
sink.append(audio_source.decoder());
}
Some(sink)
}
Err(err) => {
warn!("Error playing spatial sound: {err:?}");
None
}
}
})
}
fn try_play_queued(
&self,
audio_sources: &Assets<Source>,
audio: &mut Audio<Source>,
sinks: &mut Assets<AudioSink>,
spatial_sinks: &mut Assets<SpatialAudioSink>,
global_volume: &GlobalVolume,
) {
let mut queue = audio.queue.write();
let len = queue.len();
let mut i = 0;
while i < len {
let config = queue.pop_front().unwrap();
if let Some(audio_source) = audio_sources.get(&config.source_handle) {
if let Some(spatial) = config.spatial {
if let Some(sink) =
self.play_spatial_source(audio_source, config.settings.repeat, spatial)
{
sink.set_speed(config.settings.speed);
match config.settings.volume {
for (entity, source_handle, settings, spatial) in &query_nonplaying {
if let Some(audio_source) = audio_sources.get(source_handle) {
// audio data is available (has loaded), begin playback and insert sink component
if let Some(spatial) = spatial {
match SpatialSink::try_new(
stream_handle,
spatial.emitter,
spatial.left_ear,
spatial.right_ear,
) {
Ok(sink) => {
sink.set_speed(settings.speed);
match settings.volume {
Volume::Relative(vol) => {
sink.set_volume(vol.0 * global_volume.volume.0);
}
Volume::Absolute(vol) => sink.set_volume(vol.0),
}
// don't keep the strong handle. there is no way to return it to the user here as it is async
let _ = spatial_sinks
.set(config.sink_handle, SpatialAudioSink { sink: Some(sink) });
if settings.paused {
sink.pause();
}
match settings.mode {
PlaybackMode::Loop => {
sink.append(audio_source.decoder().repeat_infinite());
commands
.entity(entity)
.insert(SpatialAudioSink { sink: Some(sink) });
}
PlaybackMode::Once => {
sink.append(audio_source.decoder());
commands
.entity(entity)
.insert(SpatialAudioSink { sink: Some(sink) });
}
PlaybackMode::Despawn => {
sink.append(audio_source.decoder());
commands
.entity(entity)
// PERF: insert as bundle to reduce archetype moves
.insert((
SpatialAudioSink { sink: Some(sink) },
PlaybackDespawnMarker,
));
}
PlaybackMode::Remove => {
sink.append(audio_source.decoder());
commands
.entity(entity)
// PERF: insert as bundle to reduce archetype moves
.insert((
SpatialAudioSink { sink: Some(sink) },
PlaybackRemoveMarker,
));
}
};
}
} else if let Some(sink) = self.play_source(audio_source, config.settings.repeat) {
sink.set_speed(config.settings.speed);
match config.settings.volume {
Volume::Relative(vol) => sink.set_volume(vol.0 * global_volume.volume.0),
Volume::Absolute(vol) => sink.set_volume(vol.0),
Err(err) => {
warn!("Error playing spatial sound: {err:?}");
}
// don't keep the strong handle. there is no way to return it to the user here as it is async
let _ = sinks.set(config.sink_handle, AudioSink { sink: Some(sink) });
}
} else {
// audio source hasn't loaded yet. add it back to the queue
queue.push_back(config);
match Sink::try_new(stream_handle) {
Ok(sink) => {
sink.set_speed(settings.speed);
match settings.volume {
Volume::Relative(vol) => {
sink.set_volume(vol.0 * global_volume.volume.0);
}
Volume::Absolute(vol) => sink.set_volume(vol.0),
}
if settings.paused {
sink.pause();
}
match settings.mode {
PlaybackMode::Loop => {
sink.append(audio_source.decoder().repeat_infinite());
commands
.entity(entity)
.insert(AudioSink { sink: Some(sink) });
}
PlaybackMode::Once => {
sink.append(audio_source.decoder());
commands
.entity(entity)
.insert(AudioSink { sink: Some(sink) });
}
PlaybackMode::Despawn => {
sink.append(audio_source.decoder());
commands
.entity(entity)
// PERF: insert as bundle to reduce archetype moves
.insert((
AudioSink { sink: Some(sink) },
PlaybackDespawnMarker,
));
}
PlaybackMode::Remove => {
sink.append(audio_source.decoder());
commands
.entity(entity)
// PERF: insert as bundle to reduce archetype moves
.insert((AudioSink { sink: Some(sink) }, PlaybackRemoveMarker));
}
};
}
Err(err) => {
warn!("Error playing sound: {err:?}");
}
}
}
i += 1;
}
}
}
/// Plays audio currently queued in the [`Audio`] resource through the [`AudioOutput`] resource
pub fn play_queued_audio_system<Source: Asset + Decodable>(
audio_output: Res<AudioOutput<Source>>,
audio_sources: Option<Res<Assets<Source>>>,
global_volume: Res<GlobalVolume>,
mut audio: ResMut<Audio<Source>>,
mut sinks: ResMut<Assets<AudioSink>>,
mut spatial_sinks: ResMut<Assets<SpatialAudioSink>>,
) where
f32: rodio::cpal::FromSample<Source::DecoderItem>,
{
if let Some(audio_sources) = audio_sources {
audio_output.try_play_queued(
&*audio_sources,
&mut *audio,
&mut sinks,
&mut spatial_sinks,
&global_volume,
);
};
pub(crate) fn cleanup_finished_audio<T: Decodable + Asset>(
mut commands: Commands,
query_nonspatial_despawn: Query<
(Entity, &AudioSink),
(With<PlaybackDespawnMarker>, With<Handle<T>>),
>,
query_spatial_despawn: Query<
(Entity, &SpatialAudioSink),
(With<PlaybackDespawnMarker>, With<Handle<T>>),
>,
query_nonspatial_remove: Query<
(Entity, &AudioSink),
(With<PlaybackRemoveMarker>, With<Handle<T>>),
>,
query_spatial_remove: Query<
(Entity, &SpatialAudioSink),
(With<PlaybackRemoveMarker>, With<Handle<T>>),
>,
) {
for (entity, sink) in &query_nonspatial_despawn {
if sink.sink.as_ref().unwrap().empty() {
commands.entity(entity).despawn();
}
}
for (entity, sink) in &query_spatial_despawn {
if sink.sink.as_ref().unwrap().empty() {
commands.entity(entity).despawn();
}
}
for (entity, sink) in &query_nonspatial_remove {
if sink.sink.as_ref().unwrap().empty() {
commands
.entity(entity)
.remove::<(AudioSourceBundle<T>, AudioSink, PlaybackRemoveMarker)>();
}
}
for (entity, sink) in &query_spatial_remove {
if sink.sink.as_ref().unwrap().empty() {
commands.entity(entity).remove::<(
SpatialAudioSourceBundle<T>,
SpatialAudioSink,
PlaybackRemoveMarker,
)>();
}
}
}
/// Run Condition to only play audio if the audio output is available
pub(crate) fn audio_output_available(audio_output: Res<AudioOutput>) -> bool {
audio_output.stream_handle.is_some()
}

View File

@ -1,8 +1,8 @@
//! Audio support for the game engine Bevy
//!
//! ```no_run
//! # use bevy_ecs::{system::Res, event::EventWriter};
//! # use bevy_audio::{Audio, AudioPlugin};
//! # use bevy_ecs::prelude::*;
//! # use bevy_audio::{AudioBundle, AudioPlugin, PlaybackSettings};
//! # use bevy_asset::{AssetPlugin, AssetServer};
//! # use bevy_app::{App, AppExit, NoopPluginGroup as MinimalPlugins, Startup};
//! fn main() {
@ -12,8 +12,11 @@
//! .run();
//! }
//!
//! fn play_background_audio(asset_server: Res<AssetServer>, audio: Res<Audio>) {
//! audio.play(asset_server.load("background_audio.ogg"));
//! fn play_background_audio(asset_server: Res<AssetServer>, mut commands: Commands) {
//! commands.spawn(AudioBundle {
//! source: asset_server.load("background_audio.ogg"),
//! settings: PlaybackSettings::LOOP,
//! });
//! }
//! ```
@ -30,13 +33,13 @@ mod sinks;
pub mod prelude {
#[doc(hidden)]
pub use crate::{
Audio, AudioOutput, AudioSink, AudioSinkPlayback, AudioSource, Decodable, GlobalVolume,
PlaybackSettings, SpatialAudioSink,
AudioBundle, AudioSink, AudioSinkPlayback, AudioSource, AudioSourceBundle, Decodable,
GlobalVolume, PlaybackSettings, SpatialAudioBundle, SpatialAudioSink,
SpatialAudioSourceBundle, SpatialSettings,
};
}
pub use audio::*;
pub use audio_output::*;
pub use audio_source::*;
pub use rodio::cpal::Sample as CpalSample;
@ -46,28 +49,34 @@ pub use sinks::*;
use bevy_app::prelude::*;
use bevy_asset::{AddAsset, Asset};
use bevy_ecs::prelude::*;
use audio_output::*;
/// Set for the audio playback systems, so they can share a run condition
#[derive(SystemSet, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
struct AudioPlaySet;
/// Adds support for audio playback to a Bevy Application
///
/// Use the [`Audio`] resource to play audio.
/// Insert an [`AudioBundle`] or [`SpatialAudioBundle`] onto your entities to play audio.
#[derive(Default)]
pub struct AudioPlugin {
/// The global volume for all audio sources with a [`Volume::Relative`] volume.
/// The global volume for all audio entities with a [`Volume::Relative`] volume.
pub global_volume: GlobalVolume,
}
impl Plugin for AudioPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<AudioOutput<AudioSource>>()
.add_asset::<AudioSource>()
.add_asset::<AudioSink>()
.add_asset::<SpatialAudioSink>()
.init_resource::<Audio<AudioSource>>()
.insert_resource(self.global_volume)
.add_systems(PostUpdate, play_queued_audio_system::<AudioSource>);
app.insert_resource(self.global_volume)
.configure_set(PostUpdate, AudioPlaySet.run_if(audio_output_available))
.init_resource::<AudioOutput>();
#[cfg(any(feature = "mp3", feature = "flac", feature = "wav", feature = "vorbis"))]
app.init_asset_loader::<AudioLoader>();
{
app.add_audio_source::<AudioSource>();
app.init_asset_loader::<AudioLoader>();
}
}
}
@ -77,9 +86,11 @@ impl AddAudioSource for App {
T: Decodable + Asset,
f32: rodio::cpal::FromSample<T::DecoderItem>,
{
self.add_asset::<T>()
.init_resource::<Audio<T>>()
.init_resource::<AudioOutput<T>>()
.add_systems(PostUpdate, play_queued_audio_system::<T>)
self.add_asset::<T>().add_systems(
PostUpdate,
play_queued_audio_system::<T>.in_set(AudioPlaySet),
);
self.add_systems(PostUpdate, cleanup_finished_audio::<T>.in_set(AudioPlaySet));
self
}
}

View File

@ -1,5 +1,5 @@
use bevy_ecs::component::Component;
use bevy_math::Vec3;
use bevy_reflect::{TypePath, TypeUuid};
use bevy_transform::prelude::Transform;
use rodio::{Sink, SpatialSink};
@ -65,30 +65,13 @@ pub trait AudioSinkPlayback {
fn empty(&self) -> bool;
}
/// Asset controlling the playback of a sound
/// Used to control audio during playback.
///
/// ```
/// # use bevy_ecs::system::{Local, Res};
/// # use bevy_asset::{Assets, Handle};
/// # use bevy_audio::{AudioSink, AudioSinkPlayback};
/// // Execution of this system should be controlled by a state or input,
/// // otherwise it would just toggle between play and pause every frame.
/// fn pause(
/// audio_sinks: Res<Assets<AudioSink>>,
/// music_controller: Local<Handle<AudioSink>>,
/// ) {
/// if let Some(sink) = audio_sinks.get(&*music_controller) {
/// if sink.is_paused() {
/// sink.play()
/// } else {
/// sink.pause()
/// }
/// }
/// }
/// ```
/// Bevy inserts this component onto your entities when it begins playing an audio source.
/// Use [`AudioBundle`][crate::AudioBundle] to trigger that to happen.
///
#[derive(TypePath, TypeUuid)]
#[uuid = "8BEE570C-57C2-4FC0-8CFB-983A22F7D981"]
/// You can use this component to modify the playback settings while the audio is playing.
#[derive(Component)]
pub struct AudioSink {
// This field is an Option in order to allow us to have a safe drop that will detach the sink.
// It will never be None during its life
@ -139,27 +122,13 @@ impl AudioSinkPlayback for AudioSink {
}
}
/// Asset controlling the playback of a sound, or the locations of its listener and emitter.
/// Used to control spatial audio during playback.
///
/// ```
/// # use bevy_ecs::system::{Local, Res};
/// # use bevy_asset::{Assets, Handle};
/// # use bevy_audio::SpatialAudioSink;
/// # use bevy_math::Vec3;
/// // Execution of this system should be controlled by a state or input,
/// // otherwise it would just trigger every frame.
/// fn pause(
/// spatial_audio_sinks: Res<Assets<SpatialAudioSink>>,
/// audio_controller: Local<Handle<SpatialAudioSink>>,
/// ) {
/// if let Some(spatial_sink) = spatial_audio_sinks.get(&*audio_controller) {
/// spatial_sink.set_emitter_position(Vec3::new(1.0, 0.5, 1.0));
/// }
/// }
/// ```
/// Bevy inserts this component onto your entities when it begins playing an audio source.
/// Use [`SpatialAudioBundle`][crate::SpatialAudioBundle] to trigger that to happen.
///
#[derive(TypePath, TypeUuid)]
#[uuid = "F3CA4C47-595E-453B-96A7-31C3DDF2A177"]
/// You can use this component to modify the playback settings while the audio is playing.
#[derive(Component)]
pub struct SpatialAudioSink {
// This field is an Option in order to allow us to have a safe drop that will detach the sink.
// It will never be None during its life

View File

@ -9,7 +9,9 @@ fn main() {
.run();
}
fn setup(asset_server: Res<AssetServer>, audio: Res<Audio>) {
let music = asset_server.load("sounds/Windless Slopes.ogg");
audio.play(music);
fn setup(asset_server: Res<AssetServer>, mut commands: Commands) {
commands.spawn(AudioBundle {
source: asset_server.load("sounds/Windless Slopes.ogg"),
..default()
});
}

View File

@ -10,48 +10,35 @@ fn main() {
.run();
}
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
audio: Res<Audio>,
audio_sinks: Res<Assets<AudioSink>>,
) {
let music = asset_server.load("sounds/Windless Slopes.ogg");
let handle = audio_sinks.get_handle(audio.play(music));
commands.insert_resource(MusicController(handle));
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
AudioBundle {
source: asset_server.load("sounds/Windless Slopes.ogg"),
..default()
},
MyMusic,
));
}
#[derive(Resource)]
struct MusicController(Handle<AudioSink>);
#[derive(Component)]
struct MyMusic;
fn update_speed(
audio_sinks: Res<Assets<AudioSink>>,
music_controller: Res<MusicController>,
time: Res<Time>,
) {
if let Some(sink) = audio_sinks.get(&music_controller.0) {
fn update_speed(music_controller: Query<&AudioSink, With<MyMusic>>, time: Res<Time>) {
if let Ok(sink) = music_controller.get_single() {
sink.set_speed(((time.elapsed_seconds() / 5.0).sin() + 1.0).max(0.1));
}
}
fn pause(
keyboard_input: Res<Input<KeyCode>>,
audio_sinks: Res<Assets<AudioSink>>,
music_controller: Res<MusicController>,
) {
fn pause(keyboard_input: Res<Input<KeyCode>>, music_controller: Query<&AudioSink, With<MyMusic>>) {
if keyboard_input.just_pressed(KeyCode::Space) {
if let Some(sink) = audio_sinks.get(&music_controller.0) {
if let Ok(sink) = music_controller.get_single() {
sink.toggle();
}
}
}
fn volume(
keyboard_input: Res<Input<KeyCode>>,
audio_sinks: Res<Assets<AudioSink>>,
music_controller: Res<MusicController>,
) {
if let Some(sink) = audio_sinks.get(&music_controller.0) {
fn volume(keyboard_input: Res<Input<KeyCode>>, music_controller: Query<&AudioSink, With<MyMusic>>) {
if let Ok(sink) = music_controller.get_single() {
if keyboard_input.just_pressed(KeyCode::Plus) {
sink.set_volume(sink.volume() + 0.1);
} else if keyboard_input.just_pressed(KeyCode::Minus) {

View File

@ -93,10 +93,13 @@ fn main() {
.run();
}
fn setup(mut assets: ResMut<Assets<SineAudio>>, audio: Res<Audio<SineAudio>>) {
fn setup(mut assets: ResMut<Assets<SineAudio>>, mut commands: Commands) {
// add a `SineAudio` to the asset server so that it can be played
let audio_handle = assets.add(SineAudio {
frequency: 440., //this is the frequency of A4
});
audio.play(audio_handle);
commands.spawn(AudioSourceBundle {
source: audio_handle,
..default()
});
}

View File

@ -18,21 +18,25 @@ fn setup(
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
asset_server: Res<AssetServer>,
audio: Res<Audio>,
audio_sinks: Res<Assets<SpatialAudioSink>>,
) {
// Space between the two ears
let gap = 400.0;
let music = asset_server.load("sounds/Windless Slopes.ogg");
let handle = audio_sinks.get_handle(audio.play_spatial_with_settings(
music,
PlaybackSettings::LOOP,
Transform::IDENTITY,
gap / AUDIO_SCALE,
Vec3::ZERO,
// sound emitter
commands.spawn((
MaterialMesh2dBundle {
mesh: meshes.add(shape::Circle::new(15.0).into()).into(),
material: materials.add(ColorMaterial::from(Color::BLUE)),
transform: Transform::from_translation(Vec3::new(0.0, 50.0, 0.0)),
..default()
},
Emitter,
SpatialAudioBundle {
source: asset_server.load("sounds/Windless Slopes.ogg"),
settings: PlaybackSettings::LOOP,
spatial: SpatialSettings::new(Transform::IDENTITY, gap / AUDIO_SCALE, Vec3::ZERO),
},
));
commands.insert_resource(AudioController(handle));
// left ear
commands.spawn(SpriteBundle {
@ -56,17 +60,6 @@ fn setup(
..default()
});
// sound emitter
commands.spawn((
MaterialMesh2dBundle {
mesh: meshes.add(shape::Circle::new(15.0).into()).into(),
material: materials.add(ColorMaterial::from(Color::BLUE)),
transform: Transform::from_translation(Vec3::new(0.0, 50.0, 0.0)),
..default()
},
Emitter,
));
// camera
commands.spawn(Camera2dBundle::default());
}
@ -74,18 +67,14 @@ fn setup(
#[derive(Component)]
struct Emitter;
#[derive(Resource)]
struct AudioController(Handle<SpatialAudioSink>);
fn update_positions(
audio_sinks: Res<Assets<SpatialAudioSink>>,
music_controller: Res<AudioController>,
time: Res<Time>,
mut emitter: Query<&mut Transform, With<Emitter>>,
mut emitters: Query<(&mut Transform, Option<&SpatialAudioSink>), With<Emitter>>,
) {
if let Some(sink) = audio_sinks.get(&music_controller.0) {
let mut emitter_transform = emitter.single_mut();
for (mut emitter_transform, sink) in emitters.iter_mut() {
emitter_transform.translation.x = time.elapsed_seconds().sin() * 500.0;
sink.set_emitter_position(emitter_transform.translation / AUDIO_SCALE);
if let Some(sink) = &sink {
sink.set_emitter_position(emitter_transform.translation / AUDIO_SCALE);
}
}
}

View File

@ -12,23 +12,30 @@ fn main() {
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
audio: Res<Audio>,
audio_sinks: Res<Assets<SpatialAudioSink>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Space between the two ears
let gap = 4.0;
let music = asset_server.load("sounds/Windless Slopes.ogg");
let handle = audio_sinks.get_handle(audio.play_spatial_with_settings(
music,
PlaybackSettings::LOOP,
Transform::IDENTITY,
gap,
Vec3::ZERO,
// sound emitter
commands.spawn((
PbrBundle {
mesh: meshes.add(Mesh::from(shape::UVSphere {
radius: 0.2,
..default()
})),
material: materials.add(Color::BLUE.into()),
transform: Transform::from_xyz(0.0, 0.0, 0.0),
..default()
},
Emitter,
SpatialAudioBundle {
source: asset_server.load("sounds/Windless Slopes.ogg"),
settings: PlaybackSettings::LOOP,
spatial: SpatialSettings::new(Transform::IDENTITY, gap, Vec3::ZERO),
},
));
commands.insert_resource(AudioController(handle));
// left ear
commands.spawn(PbrBundle {
@ -46,20 +53,6 @@ fn setup(
..default()
});
// sound emitter
commands.spawn((
PbrBundle {
mesh: meshes.add(Mesh::from(shape::UVSphere {
radius: 0.2,
..default()
})),
material: materials.add(Color::BLUE.into()),
transform: Transform::from_xyz(0.0, 0.0, 0.0),
..default()
},
Emitter,
));
// light
commands.spawn(PointLightBundle {
point_light: PointLight {
@ -80,19 +73,15 @@ fn setup(
#[derive(Component)]
struct Emitter;
#[derive(Resource)]
struct AudioController(Handle<SpatialAudioSink>);
fn update_positions(
audio_sinks: Res<Assets<SpatialAudioSink>>,
music_controller: Res<AudioController>,
time: Res<Time>,
mut emitter: Query<&mut Transform, With<Emitter>>,
mut emitters: Query<(&mut Transform, Option<&SpatialAudioSink>), With<Emitter>>,
) {
if let Some(sink) = audio_sinks.get(&music_controller.0) {
let mut emitter_transform = emitter.single_mut();
for (mut emitter_transform, sink) in emitters.iter_mut() {
emitter_transform.translation.x = time.elapsed_seconds().sin() * 3.0;
emitter_transform.translation.z = time.elapsed_seconds().cos() * 3.0;
sink.set_emitter_position(emitter_transform.translation);
if let Some(sink) = &sink {
sink.set_emitter_position(emitter_transform.translation);
}
}
}

View File

@ -404,14 +404,18 @@ fn check_for_collisions(
}
fn play_collision_sound(
mut commands: Commands,
mut collision_events: EventReader<CollisionEvent>,
audio: Res<Audio>,
sound: Res<CollisionSound>,
) {
// Play a sound once per frame if a collision occurred.
if !collision_events.is_empty() {
// This prevents events staying active on the next frame.
collision_events.clear();
audio.play(sound.0.clone());
commands.spawn(AudioBundle {
source: sound.0.clone(),
// auto-despawn the entity when playback finishes
settings: PlaybackSettings::DESPAWN,
});
}
}

View File

@ -152,7 +152,9 @@ fn button_handler(
}
}
fn setup_music(asset_server: Res<AssetServer>, audio: Res<Audio>) {
let music = asset_server.load("sounds/Windless Slopes.ogg");
audio.play(music);
fn setup_music(asset_server: Res<AssetServer>, mut commands: Commands) {
commands.spawn(AudioBundle {
source: asset_server.load("sounds/Windless Slopes.ogg"),
settings: PlaybackSettings::LOOP,
});
}