Skip to content

Commit ca1802b

Browse files
committed
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
1 parent a69e6a1 commit ca1802b

File tree

12 files changed

+662
-144
lines changed

12 files changed

+662
-144
lines changed

Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,26 @@ description = "Shows how to create and register a custom audio source by impleme
830830
category = "Audio"
831831
wasm = true
832832

833+
[[example]]
834+
name = "spatial_audio_2d"
835+
path = "examples/audio/spatial_audio_2d.rs"
836+
837+
[package.metadata.example.spatial_audio_2d]
838+
name = "Spatial Audio 2D"
839+
description = "Shows how to play spatial audio, and moving the emitter in 2D"
840+
category = "Audio"
841+
wasm = true
842+
843+
[[example]]
844+
name = "spatial_audio_3d"
845+
path = "examples/audio/spatial_audio_3d.rs"
846+
847+
[package.metadata.example.spatial_audio_3d]
848+
name = "Spatial Audio 3D"
849+
description = "Shows how to play spatial audio, and moving the emitter in 3D"
850+
category = "Audio"
851+
wasm = true
852+
833853
# Diagnostics
834854
[[example]]
835855
name = "log_diagnostics"

crates/bevy_audio/Cargo.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,21 @@ keywords = ["bevy"]
1313
bevy_app = { path = "../bevy_app", version = "0.9.0" }
1414
bevy_asset = { path = "../bevy_asset", version = "0.9.0" }
1515
bevy_ecs = { path = "../bevy_ecs", version = "0.9.0" }
16+
bevy_math = { path = "../bevy_math", version = "0.9.0" }
1617
bevy_reflect = { path = "../bevy_reflect", version = "0.9.0", features = ["bevy"] }
18+
bevy_transform = { path = "../bevy_transform", version = "0.9.0" }
1719
bevy_utils = { path = "../bevy_utils", version = "0.9.0" }
1820

1921
# other
2022
anyhow = "1.0.4"
21-
rodio = { version = "0.16", default-features = false }
23+
rodio = { version = "0.17", default-features = false }
2224
parking_lot = "0.12.1"
2325

2426
[target.'cfg(target_os = "android")'.dependencies]
25-
oboe = { version = "0.4", optional = true }
27+
oboe = { version = "0.5", optional = true }
2628

2729
[target.'cfg(target_arch = "wasm32")'.dependencies]
28-
rodio = { version = "0.16", default-features = false, features = ["wasm-bindgen"] }
30+
rodio = { version = "0.17", default-features = false, features = ["wasm-bindgen"] }
2931

3032

3133
[features]

crates/bevy_audio/src/audio.rs

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
use crate::{AudioSink, AudioSource, Decodable};
1+
use crate::{AudioSink, AudioSource, Decodable, SpatialAudioSink};
22
use bevy_asset::{Asset, Handle, HandleId};
33
use bevy_ecs::system::Resource;
4+
use bevy_math::Vec3;
5+
use bevy_transform::prelude::Transform;
46
use parking_lot::RwLock;
57
use std::{collections::VecDeque, fmt};
68

@@ -60,7 +62,7 @@ where
6062
///
6163
/// Returns a weak [`Handle`] to the [`AudioSink`]. If this handle isn't changed to a
6264
/// strong one, the sink will be detached and the sound will continue playing. Changing it
63-
/// to a strong handle allows for control on the playback through the [`AudioSink`] asset.
65+
/// to a strong handle allows you to control the playback through the [`AudioSink`] asset.
6466
///
6567
/// ```
6668
/// # use bevy_ecs::system::Res;
@@ -83,6 +85,7 @@ where
8385
settings: PlaybackSettings::ONCE,
8486
sink_handle: id,
8587
source_handle: audio_source,
88+
spatial: None,
8689
};
8790
self.queue.write().push_back(config);
8891
Handle::<AudioSink>::weak(id)
@@ -115,14 +118,141 @@ where
115118
settings,
116119
sink_handle: id,
117120
source_handle: audio_source,
121+
spatial: None,
118122
};
119123
self.queue.write().push_back(config);
120124
Handle::<AudioSink>::weak(id)
121125
}
126+
127+
/// Play audio from a [`Handle`] to the audio source, placing the listener at the given
128+
/// transform, an ear on each side separated by `gap`. The audio emitter will placed at
129+
/// `emitter`.
130+
///
131+
/// `bevy_audio` is not using HRTF for spatial audio, but is transforming the sound to a mono
132+
/// track, and then changing the level of each stereo channel according to the distance between
133+
/// the emitter and each ear by amplifying the difference between what the two ears hear.
134+
///
135+
/// ```
136+
/// # use bevy_ecs::system::Res;
137+
/// # use bevy_asset::AssetServer;
138+
/// # use bevy_audio::Audio;
139+
/// # use bevy_math::Vec3;
140+
/// # use bevy_transform::prelude::Transform;
141+
/// fn play_spatial_audio_system(asset_server: Res<AssetServer>, audio: Res<Audio>) {
142+
/// // Sound will be to the left and behind the listener
143+
/// audio.play_spatial(
144+
/// asset_server.load("my_sound.ogg"),
145+
/// Transform::IDENTITY,
146+
/// 1.0,
147+
/// Vec3::new(-2.0, 0.0, 1.0),
148+
/// );
149+
/// }
150+
/// ```
151+
///
152+
/// Returns a weak [`Handle`] to the [`SpatialAudioSink`]. If this handle isn't changed to a
153+
/// strong one, the sink will be detached and the sound will continue playing. Changing it
154+
/// to a strong handle allows you to control the playback, or move the listener and emitter
155+
/// through the [`SpatialAudioSink`] asset.
156+
///
157+
/// ```
158+
/// # use bevy_ecs::system::Res;
159+
/// # use bevy_asset::{AssetServer, Assets};
160+
/// # use bevy_audio::{Audio, SpatialAudioSink};
161+
/// # use bevy_math::Vec3;
162+
/// # use bevy_transform::prelude::Transform;
163+
/// fn play_spatial_audio_system(
164+
/// asset_server: Res<AssetServer>,
165+
/// audio: Res<Audio>,
166+
/// spatial_audio_sinks: Res<Assets<SpatialAudioSink>>,
167+
/// ) {
168+
/// // This is a weak handle, and can't be used to control playback.
169+
/// let weak_handle = audio.play_spatial(
170+
/// asset_server.load("my_sound.ogg"),
171+
/// Transform::IDENTITY,
172+
/// 1.0,
173+
/// Vec3::new(-2.0, 0.0, 1.0),
174+
/// );
175+
/// // This is now a strong handle, and can be used to control playback, or move the emitter.
176+
/// let strong_handle = spatial_audio_sinks.get_handle(weak_handle);
177+
/// }
178+
/// ```
179+
pub fn play_spatial(
180+
&self,
181+
audio_source: Handle<Source>,
182+
listener: Transform,
183+
gap: f32,
184+
emitter: Vec3,
185+
) -> Handle<SpatialAudioSink> {
186+
let id = HandleId::random::<SpatialAudioSink>();
187+
let config = AudioToPlay {
188+
settings: PlaybackSettings::ONCE,
189+
sink_handle: id,
190+
source_handle: audio_source,
191+
spatial: Some(SpatialSettings {
192+
left_ear: (listener.translation + listener.left() * gap / 2.0).to_array(),
193+
right_ear: (listener.translation + listener.right() * gap / 2.0).to_array(),
194+
emitter: emitter.to_array(),
195+
}),
196+
};
197+
self.queue.write().push_back(config);
198+
Handle::<SpatialAudioSink>::weak(id)
199+
}
200+
201+
/// Play spatial audio from a [`Handle`] to the audio source with [`PlaybackSettings`] that
202+
/// allows looping or changing volume from the start. The listener is placed at the given
203+
/// transform, an ear on each side separated by `gap`. The audio emitter is placed at
204+
/// `emitter`.
205+
///
206+
/// `bevy_audio` is not using HRTF for spatial audio, but is transforming the sound to a mono
207+
/// track, and then changing the level of each stereo channel according to the distance between
208+
/// the emitter and each ear by amplifying the difference between what the two ears hear.
209+
///
210+
/// ```
211+
/// # use bevy_ecs::system::Res;
212+
/// # use bevy_asset::AssetServer;
213+
/// # use bevy_audio::Audio;
214+
/// # use bevy_audio::PlaybackSettings;
215+
/// # use bevy_math::Vec3;
216+
/// # use bevy_transform::prelude::Transform;
217+
/// fn play_spatial_audio_system(asset_server: Res<AssetServer>, audio: Res<Audio>) {
218+
/// audio.play_spatial_with_settings(
219+
/// asset_server.load("my_sound.ogg"),
220+
/// PlaybackSettings::LOOP.with_volume(0.75),
221+
/// Transform::IDENTITY,
222+
/// 1.0,
223+
/// Vec3::new(-2.0, 0.0, 1.0),
224+
/// );
225+
/// }
226+
/// ```
227+
///
228+
/// See [`Self::play_spatial`] on how to control playback once it's started, or how to move
229+
/// the listener or the emitter.
230+
pub fn play_spatial_with_settings(
231+
&self,
232+
audio_source: Handle<Source>,
233+
settings: PlaybackSettings,
234+
listener: Transform,
235+
gap: f32,
236+
emitter: Vec3,
237+
) -> Handle<SpatialAudioSink> {
238+
let id = HandleId::random::<SpatialAudioSink>();
239+
let config = AudioToPlay {
240+
settings,
241+
sink_handle: id,
242+
source_handle: audio_source,
243+
spatial: Some(SpatialSettings {
244+
left_ear: (listener.translation + listener.left() * gap / 2.0).to_array(),
245+
right_ear: (listener.translation + listener.right() * gap / 2.0).to_array(),
246+
emitter: emitter.to_array(),
247+
}),
248+
};
249+
self.queue.write().push_back(config);
250+
Handle::<SpatialAudioSink>::weak(id)
251+
}
122252
}
123253

124254
/// Settings to control playback from the start.
125-
#[derive(Clone, Debug)]
255+
#[derive(Clone, Copy, Debug)]
126256
pub struct PlaybackSettings {
127257
/// Play in repeat
128258
pub repeat: bool,
@@ -166,6 +296,13 @@ impl PlaybackSettings {
166296
}
167297
}
168298

299+
#[derive(Clone)]
300+
pub(crate) struct SpatialSettings {
301+
pub(crate) left_ear: [f32; 3],
302+
pub(crate) right_ear: [f32; 3],
303+
pub(crate) emitter: [f32; 3],
304+
}
305+
169306
#[derive(Clone)]
170307
pub(crate) struct AudioToPlay<Source>
171308
where
@@ -174,6 +311,7 @@ where
174311
pub(crate) sink_handle: HandleId,
175312
pub(crate) source_handle: Handle<Source>,
176313
pub(crate) settings: PlaybackSettings,
314+
pub(crate) spatial: Option<SpatialSettings>,
177315
}
178316

179317
impl<Source> fmt::Debug for AudioToPlay<Source>

0 commit comments

Comments
 (0)