mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
Basic spatial audio (#6028)
# Objective - Add basic spatial audio support to Bevy - this is what rodio supports, so no HRTF, just simple stereo channel manipulation - no "built-in" ECS support: `Emitter` and `Listener` should be components that would automatically update the positions This PR goal is to just expose rodio functionality, made possible with the recent update to rodio 0.16. A proper ECS integration opens a lot more questions, and would probably require an RFC Also updates rodio and fixes #6122
This commit is contained in:
parent
a69e6a1207
commit
ca1802b774
12 changed files with 662 additions and 144 deletions
20
Cargo.toml
20
Cargo.toml
|
@ -830,6 +830,26 @@ description = "Shows how to create and register a custom audio source by impleme
|
|||
category = "Audio"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "spatial_audio_2d"
|
||||
path = "examples/audio/spatial_audio_2d.rs"
|
||||
|
||||
[package.metadata.example.spatial_audio_2d]
|
||||
name = "Spatial Audio 2D"
|
||||
description = "Shows how to play spatial audio, and moving the emitter in 2D"
|
||||
category = "Audio"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "spatial_audio_3d"
|
||||
path = "examples/audio/spatial_audio_3d.rs"
|
||||
|
||||
[package.metadata.example.spatial_audio_3d]
|
||||
name = "Spatial Audio 3D"
|
||||
description = "Shows how to play spatial audio, and moving the emitter in 3D"
|
||||
category = "Audio"
|
||||
wasm = true
|
||||
|
||||
# Diagnostics
|
||||
[[example]]
|
||||
name = "log_diagnostics"
|
||||
|
|
|
@ -13,19 +13,21 @@ keywords = ["bevy"]
|
|||
bevy_app = { path = "../bevy_app", version = "0.9.0" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.9.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.9.0" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.9.0" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.9.0", features = ["bevy"] }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.9.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.9.0" }
|
||||
|
||||
# other
|
||||
anyhow = "1.0.4"
|
||||
rodio = { version = "0.16", default-features = false }
|
||||
rodio = { version = "0.17", default-features = false }
|
||||
parking_lot = "0.12.1"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
oboe = { version = "0.4", optional = true }
|
||||
oboe = { version = "0.5", optional = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
rodio = { version = "0.16", default-features = false, features = ["wasm-bindgen"] }
|
||||
rodio = { version = "0.17", default-features = false, features = ["wasm-bindgen"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use crate::{AudioSink, AudioSource, Decodable};
|
||||
use crate::{AudioSink, AudioSource, Decodable, SpatialAudioSink};
|
||||
use bevy_asset::{Asset, Handle, HandleId};
|
||||
use bevy_ecs::system::Resource;
|
||||
use bevy_math::Vec3;
|
||||
use bevy_transform::prelude::Transform;
|
||||
use parking_lot::RwLock;
|
||||
use std::{collections::VecDeque, fmt};
|
||||
|
||||
|
@ -60,7 +62,7 @@ where
|
|||
///
|
||||
/// 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 for control on the playback through the [`AudioSink`] asset.
|
||||
/// to a strong handle allows you to control the playback through the [`AudioSink`] asset.
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::system::Res;
|
||||
|
@ -83,6 +85,7 @@ where
|
|||
settings: PlaybackSettings::ONCE,
|
||||
sink_handle: id,
|
||||
source_handle: audio_source,
|
||||
spatial: None,
|
||||
};
|
||||
self.queue.write().push_back(config);
|
||||
Handle::<AudioSink>::weak(id)
|
||||
|
@ -115,14 +118,141 @@ where
|
|||
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;
|
||||
/// # 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(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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings to control playback from the start.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PlaybackSettings {
|
||||
/// Play in repeat
|
||||
pub repeat: bool,
|
||||
|
@ -166,6 +296,13 @@ impl PlaybackSettings {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) 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
|
||||
|
@ -174,6 +311,7 @@ where
|
|||
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>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
use crate::{Audio, AudioSource, Decodable};
|
||||
use crate::{Audio, AudioSource, Decodable, SpatialAudioSink, SpatialSettings};
|
||||
use bevy_asset::{Asset, Assets};
|
||||
use bevy_ecs::system::{Res, ResMut, Resource};
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_utils::tracing::warn;
|
||||
use rodio::{OutputStream, OutputStreamHandle, Sink, Source};
|
||||
use rodio::{OutputStream, OutputStreamHandle, Sink, Source, SpatialSink};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use crate::AudioSink;
|
||||
|
||||
/// Used internally to play audio on the current "audio device"
|
||||
///
|
||||
/// ## Note
|
||||
|
@ -52,16 +53,53 @@ where
|
|||
impl<Source> AudioOutput<Source>
|
||||
where
|
||||
Source: Asset + Decodable,
|
||||
f32: rodio::cpal::FromSample<Source::DecoderItem>,
|
||||
{
|
||||
fn play_source(&self, audio_source: &Source, repeat: bool) -> Option<Sink> {
|
||||
self.stream_handle.as_ref().map(|stream_handle| {
|
||||
let sink = Sink::try_new(stream_handle).unwrap();
|
||||
if repeat {
|
||||
sink.append(audio_source.decoder().repeat_infinite());
|
||||
} else {
|
||||
sink.append(audio_source.decoder());
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
sink
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -70,6 +108,7 @@ where
|
|||
audio_sources: &Assets<Source>,
|
||||
audio: &mut Audio<Source>,
|
||||
sinks: &mut Assets<AudioSink>,
|
||||
spatial_sinks: &mut Assets<SpatialAudioSink>,
|
||||
) {
|
||||
let mut queue = audio.queue.write();
|
||||
let len = queue.len();
|
||||
|
@ -77,7 +116,18 @@ where
|
|||
while i < len {
|
||||
let config = queue.pop_front().unwrap();
|
||||
if let Some(audio_source) = audio_sources.get(&config.source_handle) {
|
||||
if let Some(sink) = self.play_source(audio_source, config.settings.repeat) {
|
||||
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);
|
||||
sink.set_volume(config.settings.volume);
|
||||
|
||||
// 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) });
|
||||
}
|
||||
} else if let Some(sink) = self.play_source(audio_source, config.settings.repeat) {
|
||||
sink.set_speed(config.settings.speed);
|
||||
sink.set_volume(config.settings.volume);
|
||||
|
||||
|
@ -99,118 +149,11 @@ pub fn play_queued_audio_system<Source: Asset + Decodable>(
|
|||
audio_sources: Option<Res<Assets<Source>>>,
|
||||
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);
|
||||
audio_output.try_play_queued(&*audio_sources, &mut *audio, &mut sinks, &mut spatial_sinks);
|
||||
};
|
||||
}
|
||||
|
||||
/// Asset controlling the playback of a sound
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::system::{Local, Res};
|
||||
/// # use bevy_asset::{Assets, Handle};
|
||||
/// # use bevy_audio::AudioSink;
|
||||
/// // 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()
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
#[derive(TypeUuid)]
|
||||
#[uuid = "8BEE570C-57C2-4FC0-8CFB-983A22F7D981"]
|
||||
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
|
||||
sink: Option<Sink>,
|
||||
}
|
||||
|
||||
impl Drop for AudioSink {
|
||||
fn drop(&mut self) {
|
||||
self.sink.take().unwrap().detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioSink {
|
||||
/// Gets the volume of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0`
|
||||
/// will multiply each sample by this value.
|
||||
pub fn volume(&self) -> f32 {
|
||||
self.sink.as_ref().unwrap().volume()
|
||||
}
|
||||
|
||||
/// Changes the volume of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0`
|
||||
/// will multiply each sample by this value.
|
||||
pub fn set_volume(&self, volume: f32) {
|
||||
self.sink.as_ref().unwrap().set_volume(volume);
|
||||
}
|
||||
|
||||
/// Gets the speed of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" speed (unfiltered input). Any value other than `1.0`
|
||||
/// will change the play speed of the sound.
|
||||
pub fn speed(&self) -> f32 {
|
||||
self.sink.as_ref().unwrap().speed()
|
||||
}
|
||||
|
||||
/// Changes the speed of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" speed (unfiltered input). Any value other than `1.0`
|
||||
/// will change the play speed of the sound.
|
||||
pub fn set_speed(&self, speed: f32) {
|
||||
self.sink.as_ref().unwrap().set_speed(speed);
|
||||
}
|
||||
|
||||
/// Resumes playback of a paused sink.
|
||||
///
|
||||
/// No effect if not paused.
|
||||
pub fn play(&self) {
|
||||
self.sink.as_ref().unwrap().play();
|
||||
}
|
||||
|
||||
/// Pauses playback of this sink.
|
||||
///
|
||||
/// No effect if already paused.
|
||||
/// A paused sink can be resumed with [`play`](Self::play).
|
||||
pub fn pause(&self) {
|
||||
self.sink.as_ref().unwrap().pause();
|
||||
}
|
||||
|
||||
/// Toggles the playback of this sink.
|
||||
///
|
||||
/// Will pause if playing, and will be resumed if paused.
|
||||
pub fn toggle(&self) {
|
||||
if self.is_paused() {
|
||||
self.play();
|
||||
} else {
|
||||
self.pause();
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this sink paused?
|
||||
///
|
||||
/// Sinks can be paused and resumed using [`pause`](Self::pause), [`play`](Self::play), and [`toggle`](Self::toggle).
|
||||
pub fn is_paused(&self) -> bool {
|
||||
self.sink.as_ref().unwrap().is_paused()
|
||||
}
|
||||
|
||||
/// Stops the sink.
|
||||
///
|
||||
/// It won't be possible to restart it afterwards.
|
||||
pub fn stop(&self) {
|
||||
self.sink.as_ref().unwrap().stop();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,5 +104,6 @@ pub trait AddAudioSource {
|
|||
/// the [audio][super::AudioPlugin] and [asset][bevy_asset::AssetPlugin] plugins must be added first.
|
||||
fn add_audio_source<T>(&mut self) -> &mut Self
|
||||
where
|
||||
T: Decodable + Asset;
|
||||
T: Decodable + Asset,
|
||||
f32: rodio::cpal::FromSample<T::DecoderItem>;
|
||||
}
|
||||
|
|
|
@ -25,11 +25,15 @@
|
|||
mod audio;
|
||||
mod audio_output;
|
||||
mod audio_source;
|
||||
mod sinks;
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub mod prelude {
|
||||
#[doc(hidden)]
|
||||
pub use crate::{Audio, AudioOutput, AudioSource, Decodable, PlaybackSettings};
|
||||
pub use crate::{
|
||||
Audio, AudioOutput, AudioSink, AudioSinkPlayback, AudioSource, Decodable, PlaybackSettings,
|
||||
SpatialAudioSink,
|
||||
};
|
||||
}
|
||||
|
||||
pub use audio::*;
|
||||
|
@ -39,6 +43,7 @@ pub use audio_source::*;
|
|||
pub use rodio::cpal::Sample as CpalSample;
|
||||
pub use rodio::source::Source;
|
||||
pub use rodio::Sample;
|
||||
pub use sinks::*;
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{AddAsset, Asset};
|
||||
|
@ -55,6 +60,7 @@ impl Plugin for AudioPlugin {
|
|||
app.init_resource::<AudioOutput<AudioSource>>()
|
||||
.add_asset::<AudioSource>()
|
||||
.add_asset::<AudioSink>()
|
||||
.add_asset::<SpatialAudioSink>()
|
||||
.init_resource::<Audio<AudioSource>>()
|
||||
.add_system(play_queued_audio_system::<AudioSource>.in_base_set(CoreSet::PostUpdate));
|
||||
|
||||
|
@ -67,6 +73,7 @@ impl AddAudioSource for App {
|
|||
fn add_audio_source<T>(&mut self) -> &mut Self
|
||||
where
|
||||
T: Decodable + Asset,
|
||||
f32: rodio::cpal::FromSample<T::DecoderItem>,
|
||||
{
|
||||
self.add_asset::<T>()
|
||||
.init_resource::<Audio<T>>()
|
||||
|
|
225
crates/bevy_audio/src/sinks.rs
Normal file
225
crates/bevy_audio/src/sinks.rs
Normal file
|
@ -0,0 +1,225 @@
|
|||
use bevy_math::Vec3;
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_transform::prelude::Transform;
|
||||
use rodio::{Sink, SpatialSink};
|
||||
|
||||
/// Common interactions with an audio sink.
|
||||
pub trait AudioSinkPlayback {
|
||||
/// Gets the volume of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0`
|
||||
/// will multiply each sample by this value.
|
||||
fn volume(&self) -> f32;
|
||||
|
||||
/// Changes the volume of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0`
|
||||
/// will multiply each sample by this value.
|
||||
fn set_volume(&self, volume: f32);
|
||||
|
||||
/// Gets the speed of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" speed (unfiltered input). Any value other than `1.0`
|
||||
/// will change the play speed of the sound.
|
||||
fn speed(&self) -> f32;
|
||||
|
||||
/// Changes the speed of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" speed (unfiltered input). Any value other than `1.0`
|
||||
/// will change the play speed of the sound.
|
||||
fn set_speed(&self, speed: f32);
|
||||
|
||||
/// Resumes playback of a paused sink.
|
||||
///
|
||||
/// No effect if not paused.
|
||||
fn play(&self);
|
||||
|
||||
/// Pauses playback of this sink.
|
||||
///
|
||||
/// No effect if already paused.
|
||||
/// A paused sink can be resumed with [`play`](Self::play).
|
||||
fn pause(&self);
|
||||
|
||||
/// Toggles the playback of this sink.
|
||||
///
|
||||
/// Will pause if playing, and will be resumed if paused.
|
||||
fn toggle(&self) {
|
||||
if self.is_paused() {
|
||||
self.play();
|
||||
} else {
|
||||
self.pause();
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this sink paused?
|
||||
///
|
||||
/// Sinks can be paused and resumed using [`pause`](Self::pause) and [`play`](Self::play).
|
||||
fn is_paused(&self) -> bool;
|
||||
|
||||
/// Stops the sink.
|
||||
///
|
||||
/// It won't be possible to restart it afterwards.
|
||||
fn stop(&self);
|
||||
}
|
||||
|
||||
/// Asset controlling the playback of a sound
|
||||
///
|
||||
/// ```
|
||||
/// # 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()
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
#[derive(TypeUuid)]
|
||||
#[uuid = "8BEE570C-57C2-4FC0-8CFB-983A22F7D981"]
|
||||
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
|
||||
pub(crate) sink: Option<Sink>,
|
||||
}
|
||||
|
||||
impl Drop for AudioSink {
|
||||
fn drop(&mut self) {
|
||||
self.sink.take().unwrap().detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioSinkPlayback for AudioSink {
|
||||
fn volume(&self) -> f32 {
|
||||
self.sink.as_ref().unwrap().volume()
|
||||
}
|
||||
|
||||
fn set_volume(&self, volume: f32) {
|
||||
self.sink.as_ref().unwrap().set_volume(volume);
|
||||
}
|
||||
|
||||
fn speed(&self) -> f32 {
|
||||
self.sink.as_ref().unwrap().speed()
|
||||
}
|
||||
|
||||
fn set_speed(&self, speed: f32) {
|
||||
self.sink.as_ref().unwrap().set_speed(speed);
|
||||
}
|
||||
|
||||
fn play(&self) {
|
||||
self.sink.as_ref().unwrap().play();
|
||||
}
|
||||
|
||||
fn pause(&self) {
|
||||
self.sink.as_ref().unwrap().pause();
|
||||
}
|
||||
|
||||
fn is_paused(&self) -> bool {
|
||||
self.sink.as_ref().unwrap().is_paused()
|
||||
}
|
||||
|
||||
fn stop(&self) {
|
||||
self.sink.as_ref().unwrap().stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Asset controlling the playback of a sound, or the locations of its listener and emitter.
|
||||
///
|
||||
/// ```
|
||||
/// # 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));
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
#[derive(TypeUuid)]
|
||||
#[uuid = "F3CA4C47-595E-453B-96A7-31C3DDF2A177"]
|
||||
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
|
||||
pub(crate) sink: Option<SpatialSink>,
|
||||
}
|
||||
|
||||
impl Drop for SpatialAudioSink {
|
||||
fn drop(&mut self) {
|
||||
self.sink.take().unwrap().detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioSinkPlayback for SpatialAudioSink {
|
||||
fn volume(&self) -> f32 {
|
||||
self.sink.as_ref().unwrap().volume()
|
||||
}
|
||||
|
||||
fn set_volume(&self, volume: f32) {
|
||||
self.sink.as_ref().unwrap().set_volume(volume);
|
||||
}
|
||||
|
||||
fn speed(&self) -> f32 {
|
||||
self.sink.as_ref().unwrap().speed()
|
||||
}
|
||||
|
||||
fn set_speed(&self, speed: f32) {
|
||||
self.sink.as_ref().unwrap().set_speed(speed);
|
||||
}
|
||||
|
||||
fn play(&self) {
|
||||
self.sink.as_ref().unwrap().play();
|
||||
}
|
||||
|
||||
fn pause(&self) {
|
||||
self.sink.as_ref().unwrap().pause();
|
||||
}
|
||||
|
||||
fn is_paused(&self) -> bool {
|
||||
self.sink.as_ref().unwrap().is_paused()
|
||||
}
|
||||
|
||||
fn stop(&self) {
|
||||
self.sink.as_ref().unwrap().stop();
|
||||
}
|
||||
}
|
||||
|
||||
impl SpatialAudioSink {
|
||||
/// Set the two ears position.
|
||||
pub fn set_ears_position(&self, left_position: Vec3, right_position: Vec3) {
|
||||
let sink = self.sink.as_ref().unwrap();
|
||||
sink.set_left_ear_position(left_position.to_array());
|
||||
sink.set_right_ear_position(right_position.to_array());
|
||||
}
|
||||
|
||||
/// Set the listener position, with an ear on each side separated by `gap`.
|
||||
pub fn set_listener_position(&self, position: Transform, gap: f32) {
|
||||
self.set_ears_position(
|
||||
position.translation + position.left() * gap / 2.0,
|
||||
position.translation + position.right() * gap / 2.0,
|
||||
);
|
||||
}
|
||||
|
||||
/// Set the emitter position.
|
||||
pub fn set_emitter_position(&self, position: Vec3) {
|
||||
self.sink
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.set_emitter_position(position.to_array());
|
||||
}
|
||||
}
|
15
deny.toml
15
deny.toml
|
@ -6,7 +6,6 @@ unmaintained = "deny"
|
|||
yanked = "deny"
|
||||
notice = "deny"
|
||||
ignore = [
|
||||
"RUSTSEC-2020-0056", # from cpal v0.14.1 - unmaintained - https://github.com/koute/stdweb/issues/403
|
||||
"RUSTSEC-2022-0048", # from xml-rs 0.8.4 - unmaintained - it's used in a build script of winit
|
||||
]
|
||||
|
||||
|
@ -35,20 +34,12 @@ wildcards = "deny"
|
|||
highlight = "all"
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
{ name = "ndk-sys", version = "0.3" }, # from rodio v0.16.0
|
||||
{ name = "ndk", version = "0.6" }, # from rodio v0.16.0
|
||||
{ name = "nix", version = "0.23" }, # from cpal v0.14.1
|
||||
{ name = "core-foundation-sys", version = "0.6" }, # from cpal v0.15.0
|
||||
{ name = "jni", version = "0.19" }, # from cpal v0.15.0
|
||||
{ name = "nix", version = "0.24" }, # from cpal v0.15.0
|
||||
{ name = "redox_syscall", version = "0.2" }, # from notify v5.1.0
|
||||
{ name = "rustc_version", version = "0.2" }, # from postcard v1.0.2
|
||||
{ name = "semver", version = "0.9" }, # from postcard v1.0.2
|
||||
{ name = "windows-sys", version = "0.42" }, # from notify v5.1.0
|
||||
{ name = "windows", version = "0.43"}, # from gilrs v0.10.1
|
||||
{ name = "windows", version = "0.37" }, # from rodio v0.16.0
|
||||
{ name = "windows_aarch64_msvc", version = "0.37" }, # from rodio v0.16.0
|
||||
{ name = "windows_i686_gnu", version = "0.37" }, # from rodio v0.16.0
|
||||
{ name = "windows_i686_msvc", version = "0.37" }, # from rodio v0.16.0
|
||||
{ name = "windows_x86_64_gnu", version = "0.37" }, # from rodio v0.16.0
|
||||
{ name = "windows_x86_64_msvc", version = "0.37" }, # from rodio v0.16.0
|
||||
]
|
||||
|
||||
[sources]
|
||||
|
|
|
@ -183,6 +183,8 @@ Example | Description
|
|||
[Audio](../examples/audio/audio.rs) | Shows how to load and play an audio file
|
||||
[Audio Control](../examples/audio/audio_control.rs) | Shows how to load and play an audio file, and control how it's played
|
||||
[Decodable](../examples/audio/decodable.rs) | Shows how to create and register a custom audio source by implementing the `Decodable` type.
|
||||
[Spatial Audio 2D](../examples/audio/spatial_audio_2d.rs) | Shows how to play spatial audio, and moving the emitter in 2D
|
||||
[Spatial Audio 3D](../examples/audio/spatial_audio_3d.rs) | Shows how to play spatial audio, and moving the emitter in 3D
|
||||
|
||||
## Diagnostics
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//! This example illustrates how to load and play an audio file, and control how it's played.
|
||||
|
||||
use bevy::{audio::AudioSink, prelude::*};
|
||||
use bevy::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
|
|
91
examples/audio/spatial_audio_2d.rs
Normal file
91
examples/audio/spatial_audio_2d.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
//! This example illustrates how to load and play an audio file, and control where the sounds seems to come from.
|
||||
use bevy::{prelude::*, sprite::MaterialMesh2dBundle};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_system(update_positions)
|
||||
.run();
|
||||
}
|
||||
|
||||
/// Spatial audio uses the distance to attenuate the sound volume. In 2D with the default camera, 1 pixel is 1 unit of distance,
|
||||
/// so we use a scale so that 100 pixels is 1 unit of distance for audio.
|
||||
const AUDIO_SCALE: f32 = 100.0;
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
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,
|
||||
));
|
||||
commands.insert_resource(AudioController(handle));
|
||||
|
||||
// left ear
|
||||
commands.spawn(SpriteBundle {
|
||||
sprite: Sprite {
|
||||
color: Color::RED,
|
||||
custom_size: Some(Vec2::splat(20.0)),
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(-gap / 2.0, 0.0, 0.0),
|
||||
..default()
|
||||
});
|
||||
|
||||
// right ear
|
||||
commands.spawn(SpriteBundle {
|
||||
sprite: Sprite {
|
||||
color: Color::GREEN,
|
||||
custom_size: Some(Vec2::splat(20.0)),
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(gap / 2.0, 0.0, 0.0),
|
||||
..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());
|
||||
}
|
||||
|
||||
#[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>>,
|
||||
) {
|
||||
if let Some(sink) = audio_sinks.get(&music_controller.0) {
|
||||
let mut emitter_transform = emitter.single_mut();
|
||||
emitter_transform.translation.x = time.elapsed_seconds().sin() * 500.0;
|
||||
sink.set_emitter_position(emitter_transform.translation / AUDIO_SCALE);
|
||||
}
|
||||
}
|
98
examples/audio/spatial_audio_3d.rs
Normal file
98
examples/audio/spatial_audio_3d.rs
Normal file
|
@ -0,0 +1,98 @@
|
|||
//! This example illustrates how to load and play an audio file, and control where the sounds seems to come from.
|
||||
use bevy::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_system(update_positions)
|
||||
.run();
|
||||
}
|
||||
|
||||
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,
|
||||
));
|
||||
commands.insert_resource(AudioController(handle));
|
||||
|
||||
// left ear
|
||||
commands.spawn(PbrBundle {
|
||||
mesh: meshes.add(Mesh::from(shape::Cube { size: 0.2 })),
|
||||
material: materials.add(Color::RED.into()),
|
||||
transform: Transform::from_xyz(-gap / 2.0, 0.0, 0.0),
|
||||
..default()
|
||||
});
|
||||
|
||||
// right ear
|
||||
commands.spawn(PbrBundle {
|
||||
mesh: meshes.add(Mesh::from(shape::Cube { size: 0.2 })),
|
||||
material: materials.add(Color::GREEN.into()),
|
||||
transform: Transform::from_xyz(gap / 2.0, 0.0, 0.0),
|
||||
..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 {
|
||||
intensity: 1500.0,
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(4.0, 8.0, 4.0),
|
||||
..default()
|
||||
});
|
||||
// camera
|
||||
commands.spawn(Camera3dBundle {
|
||||
transform: Transform::from_xyz(0.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||
..default()
|
||||
});
|
||||
}
|
||||
|
||||
#[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>>,
|
||||
) {
|
||||
if let Some(sink) = audio_sinks.get(&music_controller.0) {
|
||||
let mut emitter_transform = emitter.single_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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue