diff --git a/Cargo.toml b/Cargo.toml index ddf82dbec7..e63ec377d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ [dependencies] # bevy +bevy_audio = { path = "crates/bevy_audio" } bevy_app = { path = "crates/bevy_app" } bevy_asset = { path = "crates/bevy_asset" } bevy_type_registry = { path = "crates/bevy_type_registry" } @@ -115,6 +116,10 @@ path = "examples/asset/hot_asset_reloading.rs" name = "asset_loading" path = "examples/asset/asset_loading.rs" +[[example]] +name = "audio" +path = "examples/audio/audio.rs" + [[example]] name = "custom_diagnostic" path = "examples/diagnostics/custom_diagnostic.rs" diff --git a/assets/sounds/Windless Slopes.mp3 b/assets/sounds/Windless Slopes.mp3 new file mode 100644 index 0000000000..8b8b76d7ab Binary files /dev/null and b/assets/sounds/Windless Slopes.mp3 differ diff --git a/crates/bevy_audio/Cargo.toml b/crates/bevy_audio/Cargo.toml new file mode 100644 index 0000000000..c7ae37cf54 --- /dev/null +++ b/crates/bevy_audio/Cargo.toml @@ -0,0 +1,14 @@ +[package] +authors = ["Carter Anderson "] +edition = "2018" +name = "bevy_audio" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bevy_app = {path = "../bevy_app"} +bevy_asset = {path = "../bevy_asset"} +bevy_ecs = {path = "../bevy_ecs"} +anyhow = "1.0" +rodio = {version = "0.11", default-features = false, features = ["mp3"]} diff --git a/crates/bevy_audio/src/audio_output.rs b/crates/bevy_audio/src/audio_output.rs new file mode 100644 index 0000000000..ea44ca4b32 --- /dev/null +++ b/crates/bevy_audio/src/audio_output.rs @@ -0,0 +1,54 @@ +use crate::AudioSource; +use bevy_asset::{Assets, Handle}; +use bevy_ecs::Res; +use rodio::{Decoder, Device, Sink}; +use std::{collections::VecDeque, io::Cursor, sync::RwLock}; + +pub struct AudioOutput { + device: Device, + queue: RwLock>>, +} + +impl Default for AudioOutput { + fn default() -> Self { + Self { + device: rodio::default_output_device().unwrap(), + queue: Default::default(), + } + } +} + +impl AudioOutput { + pub fn play(&self, audio_source: &AudioSource) { + let sink = Sink::new(&self.device); + sink.append(Decoder::new(Cursor::new(audio_source.clone())).unwrap()); + sink.detach(); + } + + pub fn queue_play(&self, audio_source: Handle) { + self.queue.write().unwrap().push_front(audio_source); + } + + pub fn try_play_queued(&self, audio_sources: &Assets) { + let mut queue = self.queue.write().unwrap(); + let len = queue.len(); + let mut i = 0; + while i < len { + let audio_source_handle = queue.pop_back().unwrap(); + if let Some(audio_source) = audio_sources.get(&audio_source_handle) { + self.play(audio_source); + } else { + // audio source hasn't loaded yet. add it back to the queue + queue.push_front(audio_source_handle); + } + i += 1; + } + } +} + +pub fn play_queued_audio_system( + audio_sources: Res>, + audio_output: Res, +) { + audio_output.try_play_queued(&audio_sources); +} diff --git a/crates/bevy_audio/src/audio_source.rs b/crates/bevy_audio/src/audio_source.rs new file mode 100644 index 0000000000..5e4a734d5b --- /dev/null +++ b/crates/bevy_audio/src/audio_source.rs @@ -0,0 +1,27 @@ +use anyhow::Result; +use bevy_asset::AssetLoader; +use std::{sync::Arc, path::Path}; + +#[derive(Clone)] +pub struct AudioSource { + pub bytes: Arc>, +} + +impl AsRef<[u8]> for AudioSource { + fn as_ref(&self) -> &[u8] { + &self.bytes + } +} + +#[derive(Default)] +pub struct Mp3Loader; + +impl AssetLoader for Mp3Loader { + fn from_bytes(&self, _asset_path: &Path, bytes: Vec) -> Result { + Ok(AudioSource { bytes: Arc::new(bytes) }) + } + fn extensions(&self) -> &[&str] { + static EXTENSIONS: &[&str] = &["mp3"]; + EXTENSIONS + } +} diff --git a/crates/bevy_audio/src/lib.rs b/crates/bevy_audio/src/lib.rs new file mode 100644 index 0000000000..055f628f24 --- /dev/null +++ b/crates/bevy_audio/src/lib.rs @@ -0,0 +1,21 @@ +mod audio_output; +mod audio_source; + +pub use audio_output::*; +pub use audio_source::*; + +use bevy_app::{stage, AppBuilder, AppPlugin}; +use bevy_asset::AddAsset; +use bevy_ecs::IntoQuerySystem; + +#[derive(Default)] +pub struct AudioPlugin; + +impl AppPlugin for AudioPlugin { + fn build(&self, app: &mut AppBuilder) { + app.init_resource::() + .add_asset::() + .add_asset_loader::() + .add_system_to_stage(stage::POST_UPDATE, play_queued_audio_system.system()); + } +} diff --git a/examples/audio/audio.rs b/examples/audio/audio.rs new file mode 100644 index 0000000000..20463ae5ec --- /dev/null +++ b/examples/audio/audio.rs @@ -0,0 +1,15 @@ +use bevy::prelude::*; + +fn main() { + App::build() + .add_default_plugins() + .add_startup_system(setup.system()) + .run(); +} + +fn setup(asset_server: Res, audio_output: Res) { + let music = asset_server + .load("assets/sounds/Windless Slopes.mp3") + .unwrap(); + audio_output.queue_play(music); +} diff --git a/src/add_default_plugins.rs b/src/add_default_plugins.rs index 20c3670f36..cd01ddd39d 100644 --- a/src/add_default_plugins.rs +++ b/src/add_default_plugins.rs @@ -19,6 +19,7 @@ impl AddDefaultPlugins for AppBuilder { self.add_plugin(bevy_ui::UiPlugin::default()); self.add_plugin(bevy_gltf::GltfPlugin::default()); self.add_plugin(bevy_text::TextPlugin::default()); + self.add_plugin(bevy_audio::AudioPlugin::default()); #[cfg(feature = "bevy_winit")] self.add_plugin(bevy_winit::WinitPlugin::default()); diff --git a/src/lib.rs b/src/lib.rs index ce06b23fa4..01513362dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,6 +46,7 @@ pub use bevy_app as app; pub use glam as math; pub use bevy_asset as asset; +pub use bevy_audio as audio; pub use bevy_core as core; pub use bevy_diagnostic as diagnostic; pub use bevy_ecs as ecs; diff --git a/src/prelude.rs b/src/prelude.rs index 560d2914f2..7bcecf8546 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -4,6 +4,7 @@ pub use crate::{ EventReader, Events, }, asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle}, + audio::{AudioOutput, AudioSource}, core::{ time::{Time, Timer}, transform::FaceToward,