From 0df67cdaae30becd35447c6767d5e30afeee17f1 Mon Sep 17 00:00:00 2001 From: dis-da-moe Date: Tue, 17 Jan 2023 22:42:00 +0000 Subject: [PATCH] Add `AddAudioSource` trait and improve `Decodable` docs (#6649) # Objective - Fixes #6361 - Fixes #6362 - Fixes #6364 ## Solution - Added an example for creating a custom `Decodable` type - Clarified the documentation on `Decodable` - Added an `AddAudioSource` trait and implemented it for `App` Co-authored-by: dis-da-moe <84386186+dis-da-moe@users.noreply.github.com> --- Cargo.toml | 10 +++ crates/bevy_audio/src/audio_source.rs | 36 ++++++++-- crates/bevy_audio/src/lib.rs | 15 +++- examples/README.md | 1 + examples/audio/decodable.rs | 100 ++++++++++++++++++++++++++ 5 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 examples/audio/decodable.rs diff --git a/Cargo.toml b/Cargo.toml index 898cd1f2e33ef..3e29eff47f2d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -763,6 +763,16 @@ description = "Shows how to load and play an audio file, and control how it's pl category = "Audio" wasm = true +[[example]] +name = "decodable" +path = "examples/audio/decodable.rs" + +[package.metadata.example.decodable] +name = "Decodable" +description = "Shows how to create and register a custom audio source by implementing the `Decodable` type." +category = "Audio" +wasm = true + # Diagnostics [[example]] name = "log_diagnostics" diff --git a/crates/bevy_audio/src/audio_source.rs b/crates/bevy_audio/src/audio_source.rs index 470eccd04f59e..dda555f13a4de 100644 --- a/crates/bevy_audio/src/audio_source.rs +++ b/crates/bevy_audio/src/audio_source.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use bevy_asset::{AssetLoader, LoadContext, LoadedAsset}; +use bevy_asset::{Asset, AssetLoader, LoadContext, LoadedAsset}; use bevy_reflect::TypeUuid; use bevy_utils::BoxedFuture; use std::{io::Cursor, sync::Arc}; @@ -63,14 +63,24 @@ impl AssetLoader for AudioLoader { } } -/// A type implementing this trait can be decoded as a rodio source +/// A type implementing this trait can be converted to a [`rodio::Source`] type. +/// It must be [`Send`] and [`Sync`], and usually implements [`Asset`] so needs to be [`TypeUuid`], +/// in order to be registered. +/// Types that implement this trait usually contain raw sound data that can be converted into an iterator of samples. +/// This trait is implemented for [`AudioSource`]. +/// Check the example `audio/decodable` for how to implement this trait on a custom type. pub trait Decodable: Send + Sync + 'static { - /// The decoder that can decode the implementing type - type Decoder: rodio::Source + Send + Iterator; - /// A single value given by the decoder + /// The type of the audio samples. + /// Usually a [`u16`], [`i16`] or [`f32`], as those implement [`rodio::Sample`]. + /// Other types can implement the [`rodio::Sample`] trait as well. type DecoderItem: rodio::Sample + Send + Sync; - /// Build and return a [`Self::Decoder`] for the implementing type + /// The type of the iterator of the audio samples, + /// which iterates over samples of type [`Self::DecoderItem`]. + /// Must be a [`rodio::Source`] so that it can provide information on the audio it is iterating over. + type Decoder: rodio::Source + Send + Iterator; + + /// Build and return a [`Self::Decoder`] of the implementing type fn decoder(&self) -> Self::Decoder; } @@ -82,3 +92,17 @@ impl Decodable for AudioSource { rodio::Decoder::new(Cursor::new(self.clone())).unwrap() } } + +/// A trait that allows adding a custom audio source to the object. +/// This is implemented for [`App`][bevy_app::App] to allow registering custom [`Decodable`] types. +pub trait AddAudioSource { + /// Registers an audio source. + /// The type must implement [`Decodable`], + /// so that it can be converted to a [`rodio::Source`] type, + /// and [`Asset`], so that it can be registered as an asset. + /// To use this method on [`App`][bevy_app::App], + /// the [audio][super::AudioPlugin] and [asset][bevy_asset::AssetPlugin] plugins must be added first. + fn add_audio_source(&mut self) -> &mut Self + where + T: Decodable + Asset; +} diff --git a/crates/bevy_audio/src/lib.rs b/crates/bevy_audio/src/lib.rs index 9fc4a5484b88c..f91c60a9b55ee 100644 --- a/crates/bevy_audio/src/lib.rs +++ b/crates/bevy_audio/src/lib.rs @@ -35,12 +35,13 @@ pub mod prelude { pub use audio::*; pub use audio_output::*; pub use audio_source::*; + pub use rodio::cpal::Sample as CpalSample; pub use rodio::source::Source; pub use rodio::Sample; use bevy_app::prelude::*; -use bevy_asset::AddAsset; +use bevy_asset::{AddAsset, Asset}; /// Adds support for audio playback to a Bevy Application /// @@ -63,3 +64,15 @@ impl Plugin for AudioPlugin { app.init_asset_loader::(); } } + +impl AddAudioSource for App { + fn add_audio_source(&mut self) -> &mut Self + where + T: Decodable + Asset, + { + self.add_asset::() + .init_resource::>() + .init_non_send_resource::>() + .add_system_to_stage(CoreStage::PostUpdate, play_queued_audio_system::) + } +} diff --git a/examples/README.md b/examples/README.md index 83f6c129044f1..f5ff72e6752ba 100644 --- a/examples/README.md +++ b/examples/README.md @@ -178,6 +178,7 @@ 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. ## Diagnostics diff --git a/examples/audio/decodable.rs b/examples/audio/decodable.rs new file mode 100644 index 0000000000000..1ac2fdd06db86 --- /dev/null +++ b/examples/audio/decodable.rs @@ -0,0 +1,100 @@ +//! Shows how to create a custom `Decodable` type by implementing a Sine wave. +//! ***WARNING THIS EXAMPLE IS VERY LOUD.*** Turn your volume down. +use bevy::audio::AddAudioSource; +use bevy::audio::Source; +use bevy::prelude::*; +use bevy::reflect::TypeUuid; +use bevy::utils::Duration; + +// This struct usually contains the data for the audio being played. +// This is where data read from an audio file would be stored, for example. +// Implementing `TypeUuid` will automatically implement `Asset`. +// This allows the type to be registered as an asset. +#[derive(TypeUuid)] +#[uuid = "c2090c23-78fd-44f1-8508-c89b1f3cec29"] +struct SineAudio { + frequency: f32, +} +// This decoder is responsible for playing the audio, +// and so stores data about the audio being played. +struct SineDecoder { + // how far along one period the wave is (between 0 and 1) + current_progress: f32, + // how much we move along the period every frame + progress_per_frame: f32, + // how long a period is + period: f32, + sample_rate: u32, +} + +impl SineDecoder { + fn new(frequency: f32) -> Self { + // standard sample rate for most recordings + let sample_rate = 44_100; + SineDecoder { + current_progress: 0., + progress_per_frame: frequency / sample_rate as f32, + period: std::f32::consts::PI * 2., + sample_rate, + } + } +} + +// The decoder must implement iterator so that it can implement `Decodable`. +impl Iterator for SineDecoder { + type Item = f32; + + fn next(&mut self) -> Option { + self.current_progress += self.progress_per_frame; + // we loop back round to 0 to avoid floating point inaccuracies + self.current_progress %= 1.; + Some(f32::sin(self.period * self.current_progress)) + } +} +// `Source` is what allows the audio source to be played by bevy. +// This trait provides information on the audio. +impl Source for SineDecoder { + fn current_frame_len(&self) -> Option { + None + } + + fn channels(&self) -> u16 { + 1 + } + + fn sample_rate(&self) -> u32 { + self.sample_rate + } + + fn total_duration(&self) -> Option { + None + } +} + +// Finally `Decodable` can be implemented for our `SineAudio`. +impl Decodable for SineAudio { + type Decoder = SineDecoder; + + type DecoderItem = ::Item; + + fn decoder(&self) -> Self::Decoder { + SineDecoder::new(self.frequency) + } +} + +fn main() { + let mut app = App::new(); + // register the audio source so that it can be used + app.add_plugins(DefaultPlugins) + .add_audio_source::() + .add_startup_system(setup) + .run(); +} + +fn setup(mut assets: ResMut>, audio: Res>) { + // 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); +}