From 2ff5548da9dad9fe25405f23901327b55ddec243 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:01:23 -0400 Subject: [PATCH 01/44] chore: add usage to the readme and cleanup example --- Cargo.toml | 1 - README.md | 79 +++++++++++++++++++++++++++++++---------------- examples/basic.rs | 8 +++-- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5640073..e2f37dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ categories = ["game-development", "multimedia::audio"] readme = "./README.md" [lib] -path = "src/lib.rs" [dependencies] bevy = { version = "0.14", default-features = false, features = [ diff --git a/README.md b/README.md index 23cf251..3195536 100644 --- a/README.md +++ b/README.md @@ -9,33 +9,26 @@ The build script will automatically filter through your assets folder and determ Be sure to check out the examples to see how to use this plugin. -## Cargo Features - -### `default` - -None - -### `inspect` - -Adds additional reflection traits to the structs used by this plugin to make them available in `bevy-egui-inspector` - -**Requires that channel components must also derive `Reflect`** - -### `mp3` - -Enables support for MP3 audio files. - -### `ogg` - -Enables support for OGG audio files - -### `flac` - -Enables support for FLAC audio files - -### `wav` - -Enables support for WAV audio files +## Usage + +```rust +#[derive(Component, Default)] +struct SfxChannel; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + // Add the plugin to the app, since it is generic, you can add as many channels as you want + .add_plugins(AudioControllerPlugin::::default()) + .add_systems(Update, play_with_plugin) + .run(); +} + +fn play_with_plugin(mut ew: EventWriter) { + // even though this is called on every frame, it will only be played once the previous clip has finished + ew.send(SfxEvent::new("fire.ogg")); +} +``` ## Examples @@ -64,10 +57,42 @@ Demonstrates: - Further tweaking the `AudioSink` components after the plugin has spawned them - How the `inspect` feature can be used to show more information in bevy-egui-inspector +Inputs: + +- Space Bar: Force a sound to play and ignore the controller + ```sh cargo run --example advanced --all-features ``` +## Cargo Features + +### `default` + +None + +### `inspect` + +Adds additional reflection traits to the structs used by this plugin to make them available in `bevy-egui-inspector` + +**Requires that channel components must also derive `Reflect`** + +### `mp3` + +Enables support for MP3 audio files. + +### `ogg` + +Enables support for OGG audio files + +### `flac` + +Enables support for FLAC audio files + +### `wav` + +Enables support for WAV audio files + ## Bevy support table | bevy | bevy_audio_controller | diff --git a/examples/basic.rs b/examples/basic.rs index 605f4b1..10fa336 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -10,6 +10,9 @@ use bevy_audio_controller::prelude::{AudioControllerEvent, AudioControllerPlugin #[derive(Component, Default)] struct SfxChannel; +/// Type alias for the SFX audio event to minimize boilerplate +type SfxEvent = AudioControllerEvent; + fn main() { App::new() .add_plugins(DefaultPlugins.set(LogPlugin { @@ -59,8 +62,8 @@ fn setup(mut commands: Commands) { }); } -fn play_with_plugin(mut ew: EventWriter>) { - ew.send(AudioControllerEvent::::new("fire.ogg")); +fn play_with_plugin(mut ew: EventWriter) { + ew.send(SfxEvent::new("fire.ogg")); } fn play_without_plugin(mut commands: Commands, asset_server: Res) { @@ -74,6 +77,7 @@ fn play_without_plugin(mut commands: Commands, asset_server: Res) { }); } +/// Only relevant to this example to clean up the audio from the previous state fn despawn_on_change(mut commands: Commands, query: Query>) { for entity in query.iter() { commands.entity(entity).despawn_recursive(); From 3041e1887e21977f19e438e9e85e165c4936ec6a Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Sat, 2 Nov 2024 11:06:42 -0400 Subject: [PATCH 02/44] complete rewrite --- .vscode/settings.json | 10 ++ README.md | 3 + assets/spray.ogg | Bin 0 -> 14469 bytes build.rs | 292 ++++++++++++++++++++++++++++++++++++------ examples/advanced.rs | 71 ++++++---- examples/basic.rs | 4 +- src/cache.rs | 46 ------- src/channel.rs | 145 +++++++++++++++++++++ src/event.rs | 36 ------ src/events.rs | 95 ++++++++++++++ src/lib.rs | 16 ++- src/plugin.rs | 111 ++-------------- src/resources.rs | 57 +++++++++ 13 files changed, 639 insertions(+), 247 deletions(-) create mode 100644 assets/spray.ogg delete mode 100644 src/cache.rs create mode 100644 src/channel.rs delete mode 100644 src/event.rs create mode 100644 src/events.rs create mode 100644 src/resources.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 1729341..994a721 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,13 @@ { "editor.formatOnSave": true, + "rust-analyzer.cargo.buildScripts.overrideCommand": [ + "cargo", + "check", + "--all-features", + "--all-targets", + "--message-format=json", + "--quiet", + "--workspace", + "--keep-going" + ], } \ No newline at end of file diff --git a/README.md b/README.md index 3195536..88aeac9 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ Be sure to check out the examples to see how to use this plugin. ## Usage ```rust +use bevy::prelude::*; +use bevy_audio_controller::prelude::*; + #[derive(Component, Default)] struct SfxChannel; diff --git a/assets/spray.ogg b/assets/spray.ogg new file mode 100644 index 0000000000000000000000000000000000000000..b5f54f14758f58fbebab5a2e70b38972161de895 GIT binary patch literal 14469 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 A$p8QV literal 0 HcmV?d00001 diff --git a/build.rs b/build.rs index 6550323..e30020a 100644 --- a/build.rs +++ b/build.rs @@ -12,6 +12,7 @@ use symphonia::core::probe::Hint; use symphonia::default::get_probe; const ASSET_PATH_VAR: &str = "BEVY_ASSET_PATH"; +const OUTPUT_FILE_NAME: &str = "audio_controller.rs"; fn main() { cargo_emit::rerun_if_env_changed!(ASSET_PATH_VAR); @@ -70,61 +71,161 @@ fn main() { // cargo_emit::warning!("Asset folder found: {}", dir.to_string_lossy()); let out_dir = env::var_os("OUT_DIR").unwrap(); - let dest_path = Path::new(&out_dir).join("audio_lengths.rs"); - let mut file = File::create(dest_path).unwrap(); - - file.write_all( - "/// Function for getting the length of an audio file by its path. -fn get_audio_file_length(file_name: &str) -> Option {\n match file_name {\n" - .as_ref(), - ) - .unwrap(); + let mut marker_file = File::create(Path::new(&out_dir).join(OUTPUT_FILE_NAME)).unwrap(); + let mut files = Vec::new(); let building_for_wasm = std::env::var("CARGO_CFG_TARGET_ARCH") == Ok("wasm32".to_string()); visit_dirs(&dir) .iter() .map(|path| (path, path.strip_prefix(&dir).unwrap())) - .for_each(|(fullpath, path)| { + .for_each(|(full_path, path)| { let mut path = path.to_string_lossy().to_string(); if building_for_wasm { // building for wasm. replace paths with forward slash in case we're building from windows path = path.replace(std::path::MAIN_SEPARATOR, "/"); } - cargo_emit::rerun_if_changed!(fullpath.to_string_lossy()); - - if let Some(duration) = get_audio_duration(&fullpath) { - file.write_all( - format!( - r#" {:?} => Some({}), -"#, - path, duration - ) - .as_ref(), - ) - .unwrap(); - } else { - cargo_emit::warning!( - "Could not get duration for audio file: {}", - fullpath.to_string_lossy() - ); + cargo_emit::rerun_if_changed!(full_path.to_string_lossy()); + if let Some(duration) = get_audio_duration(&full_path) { + files.push(AudioFile { path, duration }); } }); - file.write_all(format!(" not_found => {{\n bevy::utils::tracing::warn!(\"Audio length not found for {{not_found}}\");\n None\n }}\n }}\n}}\n").as_ref()) + // Write the markers + marker_file + .write_all( + format!( + "pub mod markers {{\n{}\n}}\n", + files + .iter() + .map(|f| f.get_marker_struct()) + .collect::() + ) + .as_ref(), + ) .unwrap(); - } else if std::env::var("DOCS_RS").is_ok() { - let out_dir = env::var_os("OUT_DIR").unwrap(); - let dest_path = Path::new(&out_dir).join("audio_lengths.rs"); - let mut file = File::create(dest_path).unwrap(); - file.write_all( - "/// Generated function that will return the length of the input audio file. It does not panic if a file is missing but will log a warning. -fn include_all_assets(registry: impl EmbeddedRegistry){}" + // Write the insert_audio_track trait for entity commands so we can do some jank component inserts + marker_file + .write_all( + format!( + r#" +pub mod ac_traits {{ + pub trait InsertAudioTrack {{ + fn insert_audio_track(&mut self, id: &str) -> &mut Self; + }} + + impl<'a> InsertAudioTrack for bevy::ecs::system::EntityCommands<'a> {{ + fn insert_audio_track(&mut self, id: &str) -> &mut bevy::ecs::system::EntityCommands<'a> {{ + match id {{ + {} + _ => self, + }} + }} + }} +}} +"#, + files + .iter() + .map(|f| f.insert_audio_track_impl()) + .collect::>() + .join("\n ") + ) .as_ref(), - ) - .unwrap(); + ) + .unwrap(); + + // Write the enum for the audio files + marker_file + .write_all( + format!( + r#" +pub mod audio_files {{ + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] + pub enum AudioFiles {{ + #[default] + Unknown, + {} + }} + + impl ToString for AudioFiles {{ + fn to_string(&self) -> String {{ + match self {{ + {} + _ => "Unknown", + }} + .to_string() + }} + }} + + impl From<&String> for AudioFiles {{ + fn from(file_name: &String) -> Self {{ + match file_name {{ + {} + _ => AudioFiles::Unknown, + }} + }} + }} + + impl From for AudioFiles {{ + fn from(file_name: String) -> Self {{ + Self::from(&file_name) + }} + }} + + + impl From for &str {{ + fn from(file_name: AudioFiles) -> &str {{ + match file_name {{ + {} + _ => "Unknown", + }} + }} + }} + + impl From<&bevy::core::Name> for AudioFiles {{ + fn from(name: &bevy::core::Name) -> Self {{ + Self::from(&name.to_string()) + }} + }} +}} +"#, + files + .iter() + .map(|f| f.enum_creator()) + .collect::>() + .join("\n "), + files + .iter() + .map(|f| f.get_enum_file_match()) + .collect::>() + .join("\n "), + files + .iter() + .map(|f| f.get_enum_match()) + .collect::>() + .join("\n "), + files + .iter() + .map(|f| f.get_enum_file_match()) + .collect::>() + .join("\n ") + ) + .as_ref(), + ) + .unwrap(); + } else if std::env::var("DOCS_RS").is_ok() { + // let out_dir = env::var_os("OUT_DIR").unwrap(); + // let dest_path = Path::new(&out_dir).join("audio_lengths.rs"); + + // let mut file = File::create(dest_path).unwrap(); + // file.write_all( + // "/// Generated function that will return the length of the input audio file. It does not panic if a file is missing but will log a warning. + // fn include_all_assets(registry: impl EmbeddedRegistry){}" + // .as_ref(), + // ) + // .unwrap(); } else { cargo_emit::warning!( "Could not find asset folder, please specify its path with ${}", @@ -134,6 +235,123 @@ fn include_all_assets(registry: impl EmbeddedRegistry){}" } } +struct AudioFile { + path: String, + duration: f32, +} + +impl AudioFile { + // fn get_length_match(&self) -> String { + // format!( + // r#" {:?} => Some(markers::{}::DURATION),"#, + // self.path, + // self.pascal_case() + // ) + // } + + fn get_enum_match(&self) -> String { + let struct_name = self.pascal_case(); + format!( + "markers::{}::FILE_NAME => AudioFiles::{},", + struct_name, struct_name + ) + } + + fn get_enum_file_match(&self) -> String { + let struct_name = self.pascal_case(); + format!( + "AudioFiles::{} => markers::{}::FILE_NAME,", + struct_name, struct_name, + ) + } + + fn insert_audio_track_impl(&self) -> String { + format!( + r#"{:?} => self.insert(markers::{}::DURATION),"#, + self.path, + self.pascal_case() + ) + } + + fn enum_creator(&self) -> String { + format!("{},", self.pascal_case()) + } + + fn get_marker_struct(&self) -> String { + let struct_name = self.pascal_case(); + format!( + r#" + /// Marker for the audio file: {:?} + #[derive(Debug, bevy::ecs::component::Component)] + pub struct {}(f32); + + impl Default for {} {{ + fn default() -> Self {{ + Self(Self::DURATION) + }} + }} + + impl From for {} {{ + fn from(duration: f32) -> Self {{ + Self(duration) + }} + }} + + impl From<{}> for f32 {{ + fn from(marker: {}) -> f32 {{ + marker.0 + }} + }} + + impl {} {{ + pub const DURATION: f32 = {}; + pub const FILE_NAME: &'static str = {:?}; + + pub fn get(&self) -> f32 {{ + self.0 + }} + pub fn set(&mut self, duration: f32) {{ + self.0 = duration; + }} + pub fn reset(&mut self) {{ + self.0 = Self::DURATION; + }} + }} +"#, + self.path, + struct_name, + struct_name, + struct_name, + struct_name, + struct_name, + struct_name, + self.duration, + self.path, + ) + } + + fn pascal_case(&self) -> String { + let mut parts: Vec = self + .path + .split(|c: char| c.is_whitespace() || "-_.".contains(c)) + .filter(|s| !s.is_empty()) + .map(|s| { + let mut chars = s.chars(); + match chars.next() { + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + None => String::new(), + } + }) + .collect(); + + if let Some(extension) = parts.last_mut() { + *extension = extension.to_uppercase(); + } + + parts.concat() + } +} + fn visit_dirs(dir: &Path) -> Vec { let mut collected = vec![]; if dir.is_dir() { diff --git a/examples/advanced.rs b/examples/advanced.rs index 0674a26..393f78f 100644 --- a/examples/advanced.rs +++ b/examples/advanced.rs @@ -1,11 +1,14 @@ use bevy::{ - input::common_conditions::{input_just_pressed, input_toggle_active}, - log::LogPlugin, - prelude::*, + input::common_conditions::input_just_pressed, log::LogPlugin, prelude::*, + time::common_conditions::on_timer, utils::Duration, }; use bevy_inspector_egui::quick::WorldInspectorPlugin; -use bevy_audio_controller::prelude::{AudioControllerEvent, AudioControllerPlugin}; +use bevy_audio_controller::{ + markers::FireOGG, + prelude::{AudioControllerPlugin, PlayEvent}, + AudioFiles, +}; #[derive(Component, Default, Reflect)] struct MusicChannel; @@ -24,31 +27,24 @@ fn main() { })) .add_plugins(WorldInspectorPlugin::new()) .add_plugins(( - AudioControllerPlugin::::new( - 0.5, - PlaybackSettings { - mode: bevy::audio::PlaybackMode::Loop, - ..Default::default() - }, - ), - AudioControllerPlugin::::default() - .with_fallback_delay_time(0.25) - .with_settings(PlaybackSettings::REMOVE), + AudioControllerPlugin::::default(), + AudioControllerPlugin::::default(), )) .add_systems(Startup, setup) .add_systems( Update, ( - play_fire_sound, + play_sfx, + // play_sfx.run_if(on_timer(Duration::from_secs_f32(1.0))), force_play.run_if(input_just_pressed(KeyCode::Space)), ), ) - .add_systems(PostUpdate, do_something_with_sfx) + // .add_systems(PostUpdate, (do_something_with_fire)) .run(); } -fn setup(mut commands: Commands, mut ew: EventWriter>) { - ew.send(AudioControllerEvent::::new("background.ogg")); +fn setup(mut commands: Commands, mut ew: EventWriter>) { + ew.send(PlayEvent::::new("background.ogg")); commands.spawn((Name::new("SFX Container"), SfxParent)); commands.spawn(Camera2dBundle::default()); commands @@ -78,15 +74,16 @@ fn setup(mut commands: Commands, mut ew: EventWriter>, - mut ew: EventWriter>, + mut ew: EventWriter>, ) { if parent_query.is_empty() { return; } let parent_entity = parent_query.single(); - ew.send(AudioControllerEvent::::new("fire.ogg").with_parent(parent_entity)); + ew.send(PlayEvent::::from(AudioFiles::FireOGG).with_parent(parent_entity)); + // ew.send(AudioControllerEvent::::new("spray.ogg").with_parent(parent_entity)); } fn do_something_with_sfx( @@ -103,6 +100,34 @@ fn do_something_with_sfx( } } -fn force_play(mut ew: EventWriter>) { - ew.send(AudioControllerEvent::::new("fire.ogg").with_force()); +// fn do_something_with_fire( +// sfx_query: Query<(Entity, &Name, &AudioSink), (Added, With)>, +// ) { +// for (entity, name, sink) in sfx_query.iter() { +// sink.set_speed(1.5); +// info!( +// "Fire: {} ({}) is playing at speed {}", +// name, +// entity, +// sink.speed() +// ); +// } +// } + +fn do_something_with_fire(sfx_query: Query<(Entity, &Name, &AudioSink), (Changed)>) { + for (entity, name, sink) in sfx_query.iter() { + // sink.set_speed(1.5); + + info!( + "SFX: {} ({}) is playing at speed {} - {}", + name, + entity, + sink.speed(), + sink.is_paused(), + ); + } +} + +fn force_play(mut ew: EventWriter>) { + ew.send(PlayEvent::::new("fire.ogg").with_force()); } diff --git a/examples/basic.rs b/examples/basic.rs index 10fa336..014f5ea 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -5,13 +5,13 @@ use bevy::{ }; use bevy_inspector_egui::quick::WorldInspectorPlugin; -use bevy_audio_controller::prelude::{AudioControllerEvent, AudioControllerPlugin}; +use bevy_audio_controller::prelude::*; #[derive(Component, Default)] struct SfxChannel; /// Type alias for the SFX audio event to minimize boilerplate -type SfxEvent = AudioControllerEvent; +type SfxEvent = PlayEvent; fn main() { App::new() diff --git a/src/cache.rs b/src/cache.rs deleted file mode 100644 index c94c17b..0000000 --- a/src/cache.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::marker::PhantomData; - -#[cfg(feature = "inspect")] -use bevy::{ecs::reflect::ReflectResource, reflect::Reflect}; -use bevy::{ - ecs::system::Resource, - time::{Timer, TimerMode}, - utils::hashbrown::HashMap, -}; - -use crate::bounds::Bounds; - -include!(concat!(env!("OUT_DIR"), "/audio_lengths.rs")); - -#[derive(Default, Resource)] -#[cfg_attr(feature = "inspect", derive(Reflect))] -#[cfg_attr(feature = "inspect", reflect(Resource))] -pub(super) struct AudioCache { - pub(super) default_time: f32, - pub(super) map: HashMap, - #[cfg_attr(feature = "inspect", reflect(ignore))] - _marker: PhantomData, -} - -impl AudioCache { - pub(super) fn new(default_time: f32) -> Self { - Self { - default_time, - ..Default::default() - } - } - - pub(super) fn can_play_sfx(&self, id: &str) -> bool { - self.map - .get(&id.to_string()) - .map_or(true, |timer| timer.finished()) - } - - pub(super) fn reset_entry(&mut self, id: String) { - let duration = get_audio_file_length(&id).unwrap_or(self.default_time); - self.map - .entry(id) - .or_insert(Timer::from_seconds(duration, TimerMode::Once)) - .reset() - } -} diff --git a/src/channel.rs b/src/channel.rs new file mode 100644 index 0000000..ed2b2d3 --- /dev/null +++ b/src/channel.rs @@ -0,0 +1,145 @@ +use std::ops::Deref; + +use bevy::{ + app::Update, + asset::AssetServer, + audio::{AudioBundle, AudioSink, AudioSinkPlayback}, + core::Name, + ecs::{ + event::EventReader, + query::{Added, With}, + system::{Commands, Query, Res, ResMut}, + }, + hierarchy::BuildChildren, + prelude::{on_event, resource_changed, IntoSystemConfigs}, + utils::hashbrown::HashSet, +}; + +use crate::{ + ac_traits::InsertAudioTrack, + audio_files::AudioFiles, + bounds::Bounds, + events::{PlayEvent, TrackEvent, VolumeEvent}, + plugin::GlobalAudioChannel, + resources::{ChannelSettings, TrackSettings}, +}; + +pub trait ChannelRegistration { + fn register_audio_channel(&mut self); +} + +impl ChannelRegistration for bevy::app::App { + fn register_audio_channel(&mut self) { + self.add_event::>() + .add_event::>() + .init_resource::>() + .init_resource::>() + .add_systems( + Update, + ( + update_volume_on_insert::, + play_event_reader::.run_if(on_event::>()), + volume_event_reader::.run_if(on_event::>()), + track_event_reader::.run_if(on_event::>()), + update_track_volumes:: + .run_if(resource_changed::>), + ), + ); + } +} + +fn update_track_volumes( + channel: Res>, + global: Res>, + track_query: Query<&AudioSink, With>, +) { + let volume = get_normalized_volume(channel, global); + for sink in track_query.iter() { + sink.set_volume(volume); + } +} + +fn update_volume_on_insert( + channel: Res>, + global: Res>, + sink_query: Query<&AudioSink, (Added, With)>, +) { + let volume = get_normalized_volume(channel, global); + for sink in sink_query.iter() { + sink.set_volume(sink.volume() * volume); + } +} + +fn volume_event_reader( + channel_settings: Res>, + mut events: EventReader>, +) { + for event in events.read() { + channel_settings.set_volume(event.volume); + } + channel_settings.deref(); +} + +fn track_event_reader( + mut track_settings: ResMut>, + mut events: EventReader>, +) { + for event in events.read() { + if let Some(id) = event.id { + track_settings.set(id, event.settings); + } else { + track_settings.set_all(event.settings); + } + } +} + +fn play_event_reader( + mut commands: Commands, + asset_server: Res, + mut events: EventReader>, + query: Query<(&Name, &AudioSink), With>, + track_settings: Res>, +) { + let is_playing = query + .iter() + .filter_map(|(name, sink)| { + if sink.is_paused() { + None + } else { + Some(name.into()) + } + }) + .collect::>(); + + for event in events.read() { + let id = event.id.to_string(); + if event.force || !is_playing.contains(&event.id) { + let settings = if let Some(event_settings) = event.settings { + event_settings + } else { + track_settings.get_track_setting(&event.id) + }; + let child = commands + .spawn(( + AudioBundle { + settings, + source: asset_server.load(&id), + }, + Channel::default(), + )) + .insert_audio_track(&id) + .insert(Name::new(id)) + .id(); + if let Some(entity) = event.parent { + commands.entity(entity).add_child(child); + } + } + } +} + +fn get_normalized_volume( + channel: Res>, + global: Res>, +) -> f32 { + channel.get_volume() * global.get_volume() +} diff --git a/src/event.rs b/src/event.rs deleted file mode 100644 index a6c24c4..0000000 --- a/src/event.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::marker::PhantomData; - -use bevy::ecs::{entity::Entity, event::Event}; - -use crate::bounds::Bounds; - -#[derive(Event)] -pub struct AudioControllerEvent { - pub(super) id: String, - pub(super) entity: Option, - pub(super) force: bool, - _marker: PhantomData, -} - -impl AudioControllerEvent { - pub fn new(id: U) -> Self { - Self { - id: id.to_string(), - entity: None, - force: false, - _marker: PhantomData::, - } - } - - pub fn with_parent(self, entity: Entity) -> Self { - Self { - entity: Some(entity), - ..self - } - } - - pub fn with_force(mut self) -> Self { - self.force = true; - self - } -} diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..89a55a7 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,95 @@ +use std::marker::PhantomData; + +use bevy::{ + audio::PlaybackSettings, + ecs::{entity::Entity, event::Event}, +}; + +use super::{audio_files::AudioFiles, bounds::Bounds}; + +#[derive(Event)] +pub struct PlayEvent { + pub(super) id: AudioFiles, + pub(super) parent: Option, + pub(super) settings: Option, + pub(super) force: bool, + _marker: PhantomData, +} + +impl PlayEvent { + pub fn new(id: U) -> Self { + Self { + id: id.to_string().into(), + parent: None, + settings: None, + force: false, + _marker: PhantomData::, + } + } + + pub fn with_parent(self, entity: Entity) -> Self { + Self { + parent: Some(entity), + ..self + } + } + + pub fn with_force(mut self) -> Self { + self.force = true; + self + } + + pub fn with_settings(mut self, settings: PlaybackSettings) -> Self { + self.settings = Some(settings); + self + } +} + +impl From for PlayEvent { + fn from(id: AudioFiles) -> Self { + Self { + id, + parent: None, + settings: None, + force: false, + _marker: PhantomData::, + } + } +} + +#[derive(Event)] +pub struct VolumeEvent { + pub(super) volume: f32, + _marker: PhantomData, +} + +impl VolumeEvent { + pub fn new(volume: f32) -> Self { + Self { + volume, + _marker: PhantomData::, + } + } +} + +#[derive(Event)] +pub struct TrackEvent { + pub(super) id: Option, + pub(super) settings: PlaybackSettings, + _marker: PhantomData, +} + +impl TrackEvent { + pub fn new(settings: PlaybackSettings) -> Self { + Self { + id: None, + settings, + _marker: PhantomData::, + } + } + + pub fn with_track(mut self, id: AudioFiles) -> Self { + self.id = Some(id); + self + } +} diff --git a/src/lib.rs b/src/lib.rs index da49181..e65a124 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,17 @@ mod bounds; -mod cache; -pub mod event; +pub mod channel; +pub mod events; pub mod plugin; +pub mod resources; + +include!(concat!(env!("OUT_DIR"), "/audio_controller.rs")); pub mod prelude { - pub use super::event::AudioControllerEvent; - pub use super::plugin::AudioControllerPlugin; + pub use super::ac_traits::InsertAudioTrack; + pub use super::audio_files::AudioFiles; + pub use super::channel::*; + pub use super::events::*; + pub use super::markers::*; + pub use super::plugin::*; + pub use super::resources::*; } diff --git a/src/plugin.rs b/src/plugin.rs index ea620eb..172270b 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,108 +1,21 @@ -use std::marker::PhantomData; - +#[cfg(feature = "inspect")] +use bevy::reflect::{FromReflect, GetTypeRegistration, TypePath}; use bevy::{ - app::{App, Plugin, Update}, - asset::AssetServer, - audio::{AudioBundle, PlaybackSettings}, - core::Name, - ecs::{ - event::EventReader, - system::{Commands, Res, ResMut}, - }, - hierarchy::BuildChildren, - time::Time, + app::{App, Plugin}, + ecs::component::Component, }; -use crate::{bounds::Bounds, cache::AudioCache, event::AudioControllerEvent}; +use super::channel::ChannelRegistration; -pub struct AudioControllerPlugin { - settings: PlaybackSettings, - fallback_delay_time: f32, - _marker: PhantomData, -} +pub struct AudioControllerPlugin; -impl Plugin for AudioControllerPlugin { +impl Plugin for AudioControllerPlugin { fn build(&self, app: &mut App) { - app.insert_resource(AudioCache::::new(self.fallback_delay_time)) - .add_event::>() - .add_systems(Update, self.build_event_reader()); - - #[cfg(feature = "inspect")] - app.register_type::>(); - } -} - -impl Default for AudioControllerPlugin { - fn default() -> Self { - Self { - settings: PlaybackSettings::DESPAWN, - fallback_delay_time: 0.25, - _marker: PhantomData::, - } + app.register_audio_channel::(); } } -impl AudioControllerPlugin { - pub fn new(fallback_delay_time: f32, settings: PlaybackSettings) -> Self { - Self { - fallback_delay_time, - settings, - _marker: PhantomData::, - } - } - - pub fn with_settings(mut self, settings: PlaybackSettings) -> Self { - self.settings = settings; - self - } - - pub fn with_fallback_delay_time(mut self, fallback_delay_time: f32) -> Self { - self.fallback_delay_time = fallback_delay_time; - self - } - - fn build_event_reader( - &self, - ) -> impl FnMut( - Commands, - ResMut>, - Res, - Res