From 9c96f79e9ad636171de0378ab84a60a79cc17dd0 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 6 May 2024 17:11:29 +0200 Subject: [PATCH 1/5] examples: add `audio.rs` example --- examples/audio.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/audio.rs diff --git a/examples/audio.rs b/examples/audio.rs new file mode 100644 index 0000000..9ae1855 --- /dev/null +++ b/examples/audio.rs @@ -0,0 +1,17 @@ +use bevy::prelude::*; + +use bevy_kira_components::prelude::*; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, AudioPlugin)) + .add_systems(Startup, setup) + .run(); +} + +fn setup(asset_server: Res, mut commands: Commands) { + commands.spawn(AudioFileBundle { + source: asset_server.load("Windless Slopes.ogg"), + ..default() + }); +} From fe12e2a6103c4f554dec45ca70f3e50bf5012d73 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 6 May 2024 17:58:26 +0200 Subject: [PATCH 2/5] examples: add `custom_sound.rs` example --- Cargo.lock | 11 +--- Cargo.toml | 32 +++++++++- .../src/sine_wave.rs => custom_sound.rs} | 53 +++++++++------- examples/custom_sound/Cargo.toml | 47 -------------- examples/custom_sound/src/main.rs | 62 ------------------- 5 files changed, 62 insertions(+), 143 deletions(-) rename examples/{custom_sound/src/sine_wave.rs => custom_sound.rs} (84%) delete mode 100644 examples/custom_sound/Cargo.toml delete mode 100644 examples/custom_sound/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 64f1ce5..3455e8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,6 +306,7 @@ dependencies = [ "bevy_math", "cpal", "kira", + "ringbuf", "serde", "thiserror", ] @@ -1508,16 +1509,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" -[[package]] -name = "custom_sound" -version = "0.1.0" -dependencies = [ - "bevy", - "bevy-kira-components", - "diagnostics-ui", - "ringbuf", -] - [[package]] name = "d3d12" version = "0.19.0" diff --git a/Cargo.toml b/Cargo.toml index 961f847..414d3c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,37 @@ default-features = false features = ["bevy_asset"] [dev-dependencies] -bevy = "0.13" +ringbuf = "0.3.3" + +[dev-dependencies.bevy] +version = "0.13.0" +default-features = false +features = [ + # Copied from bevy with "bevy_audio" removed + "animation", + "bevy_asset", + "bevy_gilrs", + "bevy_scene", + "bevy_winit", + "bevy_core_pipeline", + "bevy_pbr", + "bevy_gltf", + "bevy_render", + "bevy_sprite", + "bevy_text", + "bevy_ui", + "multi-threaded", + "png", + "hdr", + "vorbis", + "x11", + "bevy_gizmos", + "android_shared_stdcxx", + "tonemapping_luts", + "default_font", + "webgl2", + "bevy_debug_stepping", +] [features] default = [] diff --git a/examples/custom_sound/src/sine_wave.rs b/examples/custom_sound.rs similarity index 84% rename from examples/custom_sound/src/sine_wave.rs rename to examples/custom_sound.rs index 15b3096..b2e3fe5 100644 --- a/examples/custom_sound/src/sine_wave.rs +++ b/examples/custom_sound.rs @@ -1,27 +1,34 @@ -use std::{convert::Infallible, f32::consts::TAU}; +use std::convert::Infallible; +use std::f32::consts::TAU; use bevy::prelude::*; -use bevy_kira_components::sources::AudioSource; -use bevy_kira_components::{ - kira::{ - self, - manager::error::PlaySoundError, - sound::{Sound, SoundData}, - tween::{Parameter, Tween, Value}, - }, - prelude::*, - sources::AudioSourcePlugin, -}; +use kira::manager::error::PlaySoundError; +use kira::sound::{Sound, SoundData}; +use kira::tween::{Parameter, Tween, Value}; use ringbuf::{HeapConsumer, HeapProducer, HeapRb}; -pub struct SineWavePlugin; - -impl Plugin for SineWavePlugin { - fn build(&self, app: &mut App) { - app.add_plugins(AudioSourcePlugin::::default()); - } +use bevy_kira_components::prelude::*; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + AudioPlugin, + AudioSourcePlugin::::default(), + )) + .add_systems(Startup, setup) + .run(); } +fn setup(mut commands: Commands, mut assets: ResMut>) { + use bevy_kira_components::sources::AudioBundle; + let handle = assets.add(SineWave); + commands.spawn(AudioBundle { + source: handle, + settings: SineWaveSettings { frequency: 440.0 }, + ..default() + }); + /// Enum for commands the Handle (controlled within Bevy systems) can send to the sound (in the /// audio thread). /// @@ -35,7 +42,7 @@ impl Plugin for SineWavePlugin { /// audio thread nor the sending thread has to wait on each other. enum SineWaveCommand { /// Set the frequency to a new value. It will use the provided `Tween` to transition from the - /// old value to this one. + /// old value to t:his one. SetFrequency(Value, Tween), } @@ -64,7 +71,7 @@ impl Sound for SineWaveSound { dt: f64, clock_info_provider: &kira::clock::clock_info::ClockInfoProvider, modulator_value_provider: &kira::modulator::value_provider::ModulatorValueProvider, - ) -> kira::Frame { + ) -> kira::dsp::Frame { // Receive and perform commands while let Some(command) = self.commands.pop() { match command { @@ -80,11 +87,11 @@ impl Sound for SineWaveSound { if self.phase > 1. { self.phase -= 1.; } - // 24 dB reduction to not blast the user's speakers (and ears) + // 24 dB = 8x reduction to not blast the user's speakers (and ears) let sample = 0.125 * f32::sin(TAU * self.phase); // Return the new stereo sample - kira::Frame { + kira::dsp::Frame { left: sample, right: sample, } @@ -119,7 +126,7 @@ pub struct SineWaveHandle { impl SineWaveHandle { pub fn set_frequency(&mut self, frequency: impl Into>, tween: Tween) { if self.commands.is_full() { - error!("Cannot send command: command queue is full"); + error!("maximum number of in-flight commands reached, cannot add any more"); return; } assert!(self diff --git a/examples/custom_sound/Cargo.toml b/examples/custom_sound/Cargo.toml deleted file mode 100644 index b30b768..0000000 --- a/examples/custom_sound/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "custom_sound" -version = "0.1.0" -edition = "2021" -publish = false - -[dependencies] -bevy-kira-components = { path = "../..", features = ["diagnostics"] } -diagnostics-ui = { path = "../diagnostics-ui" } -ringbuf = "0.3.3" - -[dependencies.bevy] -version = "0.13.0" -default-features = false -features = [ - # Copied from bevy with "bevy_audio" removed - "animation", - "bevy_asset", - "bevy_gilrs", - "bevy_scene", - "bevy_winit", - "bevy_core_pipeline", - "bevy_pbr", - "bevy_gltf", - "bevy_render", - "bevy_sprite", - "bevy_text", - "bevy_ui", - "multi-threaded", - "png", - "hdr", - "vorbis", - "x11", - "bevy_gizmos", - "android_shared_stdcxx", - "tonemapping_luts", - "default_font", - "webgl2", - "bevy_debug_stepping", -] - -[features] -default = ["dev"] -dev = [ - "bevy/dynamic_linking", -] -tracing = ["bevy/trace_chrome"] diff --git a/examples/custom_sound/src/main.rs b/examples/custom_sound/src/main.rs deleted file mode 100644 index 4ba8a87..0000000 --- a/examples/custom_sound/src/main.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::time::Duration; - -use bevy::prelude::*; -use bevy_kira_components::sources::AudioBundle; -use bevy_kira_components::{kira::tween::Tween, prelude::*}; -use diagnostics_ui::DiagnosticsUiPlugin; -use sine_wave::{SineWave, SineWaveHandle, SineWavePlugin, SineWaveSettings}; - -mod sine_wave; - -fn main() { - App::new() - .add_plugins(( - DefaultPlugins, - AudioPlugin, - SineWavePlugin, - DiagnosticsUiPlugin, - )) - .add_systems(Startup, setup_ui) - .add_systems(Startup, add_sounds) - .add_systems(PostUpdate, control_sounds) - .run(); -} - -fn setup_ui(mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); -} - -#[derive(Component)] -struct MySine; - -fn add_sounds(mut commands: Commands, mut sine_waves: ResMut>) { - info!("Spawning entity with sine wave bundle"); - commands.spawn(( - MySine, - AudioBundle { - source: sine_waves.add(SineWave), - settings: SineWaveSettings { frequency: 440.0 }, - ..default() - }, - )); -} - -#[allow(clippy::type_complexity)] -fn control_sounds( - mut q: Query< - &mut AudioHandle, - (With, Added>), - >, -) { - let Ok(mut handle) = q.get_single_mut() else { - return; - }; - info!("Sending command to sine wave sound"); - handle.set_frequency( - 1000.0, - Tween { - duration: Duration::from_secs(1), - ..default() - }, - ); -} From cba3f58461a60eaa69c8c1513636dbfdb5181a01 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 16 May 2024 21:10:45 +0200 Subject: [PATCH 3/5] examples: fix compilation on `custom_sound` --- examples/custom_sound.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/custom_sound.rs b/examples/custom_sound.rs index b2e3fe5..33724e1 100644 --- a/examples/custom_sound.rs +++ b/examples/custom_sound.rs @@ -8,6 +8,10 @@ use kira::tween::{Parameter, Tween, Value}; use ringbuf::{HeapConsumer, HeapProducer, HeapRb}; use bevy_kira_components::prelude::*; +// TODO: ambiguity is with bevy which still exposes `struct AudioSource` even when the `bevy_audio` +// feature is off. We'll have to break the monolithic `bevy` import anyway, so this will be solved +// then. +use bevy_kira_components::prelude::AudioSource; fn main() { App::new() @@ -28,6 +32,7 @@ fn setup(mut commands: Commands, mut assets: ResMut>) { settings: SineWaveSettings { frequency: 440.0 }, ..default() }); +} /// Enum for commands the Handle (controlled within Bevy systems) can send to the sound (in the /// audio thread). @@ -71,7 +76,7 @@ impl Sound for SineWaveSound { dt: f64, clock_info_provider: &kira::clock::clock_info::ClockInfoProvider, modulator_value_provider: &kira::modulator::value_provider::ModulatorValueProvider, - ) -> kira::dsp::Frame { + ) -> kira::Frame { // Receive and perform commands while let Some(command) = self.commands.pop() { match command { @@ -91,7 +96,7 @@ impl Sound for SineWaveSound { let sample = 0.125 * f32::sin(TAU * self.phase); // Return the new stereo sample - kira::dsp::Frame { + kira::Frame { left: sample, right: sample, } From 8f3087cf9a1094cc13c472c82ec7c77fda191cbc Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 16 May 2024 23:06:36 +0200 Subject: [PATCH 4/5] wip: custom sound documentation in example --- examples/custom_sound.rs | 104 ++++++++++++++++++++++++++++++++++++++- src/sources/mod.rs | 23 ++++++--- 2 files changed, 117 insertions(+), 10 deletions(-) diff --git a/examples/custom_sound.rs b/examples/custom_sound.rs index 33724e1..94f8b36 100644 --- a/examples/custom_sound.rs +++ b/examples/custom_sound.rs @@ -1,3 +1,98 @@ +//! # Custom audio sources +//! +//! ## A solemn forewarning +//! +//! Hark, ye who tread the path of the auditory arts, heed this scroll's solemn words, for they +//! unveil the treacherous pitfalls lurking within the realm of audio programming. As you embark on +//! your quest to weave procedural sound effects into the tapestry of existence, beware the siren +//! song of complexity that ensnares the unwary programmer. Beneath the guise of enchanting harmonies +//! lies the labyrinthine maze of real-time programming, where even the most seasoned minstrels may +//! lose themselves amidst the tangled webs of treacherous ways. +//! +//! Venture forth with vigilance, for in the realm of audio programming lies the challenge of +//! minimizing allocations. Beware the temptation to squander precious resources in the pursuit of +//! convenience, for each unnecessary allocation is a shackle binding thy creations to the +//! dreaded behemoth of thread scheduling. Strive instead to compose thy code with the elegance +//! of a virtuoso, orchestrating symphonies that sing with the clarity of unburdened thread +//! execution. +//! +//! As you seek to craft your incantations that transcend the limitations of earthly constraints, +//! embrace the art of wait-free and lock-free programming. Let not thy magic be marred by the +//! discord of contention and delay, but instead, harmonize thy algorithms with the rhythm of +//! seamless execution. With patience and precision, mayhaps ye shall unlock the secrets of audio +//! programming's true potential, weaving melodies that echo through the ages. +//! +//! Finally, let not thy pride blind thee to the wisdom of collaboration, for in unity lies strength. +//! Though the solitary path may seem enticing, remember that no bard is an island unto themselves. +//! Seek counsel from fellow travelers versed in the arcane arts of memory management and concurrency +//! control, and together, mayhaps ye shall compose masterpieces that resound with the echoes of +//! eternity. +//! +//! Behold, for a tome of further wisdom awaits your perusal [here]. Inscribe these words upon your +//! heart, and let them kindle the fires of resilience within. Tread this path at your own rhythm, +//! for it is not the speed of the journey, but the steadfastness of purpose that shall empower thee. +//! +//! [here]: http://www.rossbencina.com/code/real-time-audio-programming-101-time-waits-for-nothing +//! +//! # Implementing a custom sound +//! +//! ## Audio generation +//! +//! The most basic custom audio source implementation is to simply implement [`Sound`], which +//! is where the actual audio generation will happen. Every time this function gets called, a new +//! pair of stereo audio samples (called a `Frame` in Kira) is generated, which corresponds to +//! the position of the speaker cone at that instant. Much like physics, for example, sound is +//! computed at regular intervals, though much faster than physics, since it usually runs at +//! around 44-48 kHz. Each sample, then, only represent a few microseconds of time. +//! +//! ## Control +//! +//! To be useful in the vast majority of cases, you'll want your sound generation to be +//! controllable from the outside world (i.e. the ECS). This is where things get tricky, because a +//! lot of solutions that exist for inter-thread communications (i.e. channels, mutexes, etc.) are +//! invalid within the realm of real-time programming. Instead, the most common solution is to +//! send events or commands through a fixed-size ring buffer. +//! +//! This is the solution used within Kira, and here below. the various setter methods that you +//! will implement will not directly change the value in the [`Sound`] implementation, but +//! instead create a command indicating the change, to be sent to the audio thread through the +//! ring-buffer. +//! +//! Then, on the audio side, within the [`Sound`] implementation, the ring-buffer is consumed for +//! any sent commands to be applied. You can easily send raw values, as well as Kira modulators +//! and tweens, and Kira provides a [`Parameter`] struct which seamlessly integrates with those, +//! in such a way that you only have to consume the events and pass the values to the parameter. +//! +//! To clearly mark the separation of concerns, as well as the ownership of each half by +//! different threads, the audio processing-side type (here [`SineWaveSound`]) and the ECS-side +//! type (here [`SineWaveHandle`]) are separate, and only require access to their half of the +//! ring-buffer (which, here, is seen as a single-producer, single-consumer queue). The handle +//! pushes commands (here of type [`SineWaveCommand`]) into the ring-buffer, and the sound pulls +//! from it and applies the commands. +//! +//! Note that there is nothing to implement for the handle, as it is entirely dependent on the +//! applicable parameters and +//! +//! Most of the code of the handle is to provide a nice user-facing API, which hides the +//! ring-buffer as an implementation detail. +//! +//! ## Data +//! +//! In order to be constructed, you'll most probably require your user to provide initial +//! value, data (such as audio data from samples), as well as settings related to routing the audio +//! to a track or to a spatial scene. This "settings type" should implement [`SoundData`], whose +//! only job is to construct the [`Sound`] and its associated handle, so that Kira knows how to +//! work with your custom sound implementation. This type is not going to be used by end-users, +//! it's only the middle step in bridging the ECS and the audio engine. +//! +//! ## Settings +//! +//! TODO: Writing this triggered some existential crisis over the API 😵‍💫 +//! +//! ## Usage in the ECS +//! +//! TODO: Writing this triggered some existential crisis over the API 😵‍💫 + use std::convert::Infallible; use std::f32::consts::TAU; @@ -8,16 +103,18 @@ use kira::tween::{Parameter, Tween, Value}; use ringbuf::{HeapConsumer, HeapProducer, HeapRb}; use bevy_kira_components::prelude::*; -// TODO: ambiguity is with bevy which still exposes `struct AudioSource` even when the `bevy_audio` +// TODO: ambiguity is with bevy which still exposes `struct AudioSource` even when the `bevy_audio` // feature is off. We'll have to break the monolithic `bevy` import anyway, so this will be solved // then. -use bevy_kira_components::prelude::AudioSource; +use bevy_kira_components::prelude::AudioSource; fn main() { App::new() .add_plugins(( DefaultPlugins, AudioPlugin, + // The audio source plugin is generic over audio sources; use it to register systems + // that will manage your custom audio source for you. AudioSourcePlugin::::default(), )) .add_systems(Startup, setup) @@ -27,6 +124,9 @@ fn main() { fn setup(mut commands: Commands, mut assets: ResMut>) { use bevy_kira_components::sources::AudioBundle; let handle = assets.add(SineWave); + + // The `AudioBundle` is also generic over the sound source. You're probably already using it + // through the `AudioFileBundle` alias for audio files. commands.spawn(AudioBundle { source: handle, settings: SineWaveSettings { frequency: 440.0 }, diff --git a/src/sources/mod.rs b/src/sources/mod.rs index b7bae17..1cc4a40 100644 --- a/src/sources/mod.rs +++ b/src/sources/mod.rs @@ -1,20 +1,23 @@ //! Implementations of different audio sources. -pub mod audio_file; - -use crate::backend::AudioBackend; -use crate::spatial::SpatialEmitterHandle; +use std::fmt; +use std::marker::PhantomData; -use crate::{AudioPlaybackSet, AudioSourceSetup, AudioWorld, InternalAudioMarker}; use bevy::prelude::*; use kira::manager::AudioManager; -use std::fmt; -use std::marker::PhantomData; +use crate::{AudioPlaybackSet, AudioSourceSetup, AudioWorld, InternalAudioMarker}; +use crate::backend::AudioBackend; +use crate::spatial::SpatialEmitterHandle; + +pub mod audio_file; #[doc(hidden)] pub mod prelude { + pub use super::{ + AudioBundle, AudioHandle, AudioSource, AudioSourcePlugin, NoAudioSettings, + OutputDestination, + }; pub use super::audio_file::prelude::*; - pub use super::{AudioBundle, AudioHandle, AudioSource, AudioSourcePlugin, OutputDestination}; } /// Trait for implementing an audio source to play in the audio engine. @@ -47,6 +50,10 @@ pub trait AudioSource: Asset { ) -> Result; } +/// Dummy struct for cases where the audio source has no settings. +#[derive(Debug, Default, Component)] +pub struct NoAudioSettings; + /// Component holding a handle to an [`AudioSource`]. Access this component from your systems to /// control the parameters of the sound from Bevy. #[derive(Debug, Deref, DerefMut, Component)] From fd0f1abb37698fb9f5bba94370378fa3083c17c2 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Tue, 21 May 2024 20:30:16 +0200 Subject: [PATCH 5/5] chore: formatting --- src/sources/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sources/mod.rs b/src/sources/mod.rs index 1cc4a40..d988554 100644 --- a/src/sources/mod.rs +++ b/src/sources/mod.rs @@ -5,19 +5,19 @@ use std::marker::PhantomData; use bevy::prelude::*; use kira::manager::AudioManager; -use crate::{AudioPlaybackSet, AudioSourceSetup, AudioWorld, InternalAudioMarker}; use crate::backend::AudioBackend; use crate::spatial::SpatialEmitterHandle; +use crate::{AudioPlaybackSet, AudioSourceSetup, AudioWorld, InternalAudioMarker}; pub mod audio_file; #[doc(hidden)] pub mod prelude { + pub use super::audio_file::prelude::*; pub use super::{ AudioBundle, AudioHandle, AudioSource, AudioSourcePlugin, NoAudioSettings, OutputDestination, }; - pub use super::audio_file::prelude::*; } /// Trait for implementing an audio source to play in the audio engine.