Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Basic spatial audio #6028

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,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"
Expand Down
8 changes: 5 additions & 3 deletions crates/bevy_audio/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
144 changes: 141 additions & 3 deletions crates/bevy_audio/src/audio.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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
mockersf marked this conversation as resolved.
Show resolved Hide resolved
/// 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(),
mockersf marked this conversation as resolved.
Show resolved Hide resolved
}),
};
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,
Expand Down Expand Up @@ -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
Expand All @@ -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>
Expand Down
Loading