diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..db2d2b4561354d10c675fcd4b51495b1b9c33696 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.ttf filter=lfs diff=lfs merge=lfs -text diff --git a/Cargo.toml b/Cargo.toml index 8a2e85580ca953efb1b1b93aa0ee44d3c0d678e3..a2318e628ba62ff3bdde6314e64da3be196adab3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,21 @@ [package] name = "micro_musicbox" -version = "0.2.0-pre.1" +version = "0.3.0" edition = "2021" license = "Apache-2.0" authors = ["Louis Capitanchik <louis@microhacks.co.uk>"] repository = "https://lab.lcr.gr/microhacks/micro_bevy_musicbox" description = "Opinionated service interface for bevy_kira_audio" +[features] +default-features = [] +serde = ["dep:serde"] + [dependencies] bevy = { version = "0.8", default-features = false } -bevy_kira_audio = { version = "0.11", features = ["mp3"] } -serde = "1" +bevy_kira_audio = { version = "0.12", features = ["mp3"] } +serde = { version = "1", optional = true } + +[dev_dependencies] +bevy = "0.8" +log = "0.4" \ No newline at end of file diff --git a/README.md b/README.md index 1b26d21091c3c2dd6c15961dda9fd03234627b3e..fe9ea227c2266405a8359f18f41022dcfbac5f78 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,53 @@ -# Micro Bevy MusicBox +# Micro MusicBox Play some tunes ## What? -This library provides a convenience wrapper around bevy_kira_audio, handling all of the -setup for the common game audio scenario for you. +This library provides a convenience wrapper around bevy_kira_audio, handling all the +setup for the common game audio scenario. This includes channel management, giving you +control of the audio levels for your music, ambiance, sound effects and UI separately +from the start. -There are 4 types of audio channel that will be added to your game, with crossfade tracks -for 2 of those; this means a total of 6 `AudioChannel` resources are created. +### Channel Types -* `Music` - The main music for your game (includes A/B for crossfading) -* `Ambient` - Any long and/or looping background ambience that you might want to layer on top of music (includes A/B for crossfading) -* `Sfx` - Any short sound effects -* `UI Sfx` - Any sound effects that specifically relate to the UI, allowing for more fine-grained control of audio levels +Musicbox is configured with 4 channels, which are split into two types: -Track volume is controlled by a resource, and is calculated as the individual track's volume settings multiplied by the master volume -setting +- Main channels; Play a single looped audio track. Starting another track on this channel will stop the currently active one. Supports cross-fade between the current track and the next track. This includes the "Music" and "Ambiance" channels +- Background Channels; Plays any number of one-shot sounds. The sounds will not interfere with each other or the main channels. This includes the "SFX" and "UI SFX" channels + +### Volume + +Volume is calculated by multiplying a given channel's volume setting by the master volume setting. +This means you can let players adjust either the overall game volume, or each of the three types +individually. The 4 channels are assigned as such: + +- "music" -> music volume * master volume +- "ambiance" & "sfx" -> sfx volume * master volume +- "ui sfx" -> ui volume * master volume + +There are two types of channel: Singleton "main" channels, and multi-sound channels. Audio +played on a main channel will loop until stopped, and will replace any currently playing audio +on that same channel. multi-sound channels work exactly as they, well, sound - play one-off sounds +without worrying about coordinating them with other sources. + +Main channels also expose a cross-fade feature - either a basic cross-fade, in which the same +audio tween is applied to the outgoing and incoming music tracks, or an in-out fade where the two +tracks can have independent tweens. ## How? ### Quickstart -- Include the MusixBocPlugin plugin, or the CombinedAudioPlugins plugin group in your app -- Implement `SuppliesAudio` for a resource (or use the built in impl on `AssetServer`) +- Implement `SuppliesAudio` for a resource (or use the built-in impl on `AssetServer`) +- Include the MusixBocPlugin plugin, or the CombinedAudioPlugins plugin group in your app, providing your `SuppliesAudio` impl as the generic parameter - Use `MusicBox<T: SuppliesAudio>` as a parameter on a system - Call one of the `MusicBox::play_*` methods to play sounds ```rust fn main() { App::new() - .add_plugins(CombinedAudioPlugins) + .add_plugins(CombinedAudioPlugins::<AssetServer>::new()) .add_startup_system(|mut music_box: MusicBox<AssetServer>| { music_box.play_music("music/bing_bong.mp3"); }); @@ -104,4 +121,12 @@ pub fn play_sounds( music_box.play_effect_once(sound); } } -``` \ No newline at end of file +``` + +## Asset Licenses + +The examples in this repository use assets available under the following licenses: + +- The-Great-Madeja.mp3 by [Rolemusic](http://rolemusic.sawsquarenoise.com/) is licensed under a [CC-BY-4.0 Attribution License](https://creativecommons.org/licenses/by/4.0/). +- The-White-Kitty.mp3 by [Rolemusic](http://rolemusic.sawsquarenoise.com/) is licensed under a [CC-BY-4.0 Attribution License](https://creativecommons.org/licenses/by/4.0/). +- KenneyBlocks.ttf by [Kenney](https://www.kenney.nl) is licensed under a [CC-0 Public Domain License](http://creativecommons.org/publicdomain/zero/1.0/) diff --git a/assets/KenneyBlocks.ttf b/assets/KenneyBlocks.ttf new file mode 100644 index 0000000000000000000000000000000000000000..059ebf3c09541d6fbaf9d6d6f0ccda6431dcea6c --- /dev/null +++ b/assets/KenneyBlocks.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb2f9ba39f4fa7d8dbb3d3d1af05c2517b848f278bbe4f7ab53e518f2429bad5 +size 30508 diff --git a/assets/The-Great-Madeja.mp3 b/assets/The-Great-Madeja.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..3007ea4bcbe3741c758fbda157ee571a7de20223 --- /dev/null +++ b/assets/The-Great-Madeja.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46b627c7df1d95034b70bc4de7a9ed74be344bee132d41bfa7fb15a8356d3921 +size 1932180 diff --git a/assets/The-White-Kitty.mp3 b/assets/The-White-Kitty.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..a8424cb0ab99e3ffc9045cd9cdd004fa32af1071 --- /dev/null +++ b/assets/The-White-Kitty.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7804ea15d05840abfb0240455266d4ab1269dce99a12c8df846a97ed14131600 +size 1552452 diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000000000000000000000000000000000000..ee5f2950be03bf5a2b0437bfe30cd6389588fd75 --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,15 @@ +use bevy::prelude::*; +use micro_musicbox::prelude::*; +use micro_musicbox::CombinedAudioPlugins; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(CombinedAudioPlugins::<AssetServer>::new()) + .add_startup_system(play_audio) + .run(); +} + +pub fn play_audio(mut music_box: MusicBox<AssetServer>) { + music_box.play_music("The-Great-Madeja.mp3"); +} diff --git a/examples/kitchen_sink.rs b/examples/kitchen_sink.rs new file mode 100644 index 0000000000000000000000000000000000000000..cf4dff5e4784d569583d57eab6fcf1b0aec8ab8d --- /dev/null +++ b/examples/kitchen_sink.rs @@ -0,0 +1,124 @@ +// This example shows off a number of the features that micro_musicbox provides: +// - Basic music player +// - Channel based audio volume mixing w/ master volume +// - Cross fade audio + +use std::time::Duration; + +use bevy::ecs::schedule::ShouldRun; +use bevy::prelude::*; +use micro_musicbox::prelude::AudioSource; +use micro_musicbox::prelude::*; +use micro_musicbox::CombinedAudioPlugins; + +use crate::utilities::{AppState, DetailsMarker, TextMarker}; + +mod utilities; + +pub fn main() { + App::new() + .add_plugin(utilities::SetupPlugin) // Loads resources + .add_plugins(DefaultPlugins) + .add_plugins(CombinedAudioPlugins::<AssetServer>::new()) + .add_system_set(SystemSet::on_enter(AppState::Running).with_system(setup_audio).with_system(set_instructions)) + .add_system_set(SystemSet::on_update(AppState::Running).with_run_criteria(has_music_state).with_system(cross_fade_tracks)) + .run(); +} + +/// A resource that we'll use to keep track of which track is currently the active one, for fading +/// in and out +pub struct MusicState { + pub playing_first: bool, +} + +pub fn has_music_state(state: Option<Res<MusicState>>) -> ShouldRun { + state.is_some().into() +} + +pub fn set_instructions( + mut instructions: Query<&mut Text, (With<TextMarker>, Without<DetailsMarker>)>, + mut details: Query<&mut Text, (With<DetailsMarker>, Without<TextMarker>)>, +) { + for mut text in &mut instructions { + text.sections[0].value = String::from("Press Right Shift To Change Tracks") + } + for mut text in &mut details { + text.sections[0].value = String::from("Press 1-9 to change volume (10% - 90%)") + } +} + +pub fn setup_audio(mut commands: Commands, mut musicbox: MusicBox<AssetServer>) { + musicbox.play_music("The-White-Kitty.mp3"); + commands.insert_resource(MusicState { + playing_first: true, + }); +} + +// pub fn + +pub fn cross_fade_tracks( + input: Res<Input<KeyCode>>, + mut music_box: MusicBox<AssetServer>, + mut music_state: ResMut<MusicState>, +) { + if input.just_released(KeyCode::RShift) { + if music_state.playing_first { + music_box.cross_fade_music( + "The-Great-Madeja.mp3", + AudioTween::new(Duration::from_millis(500), AudioEasing::InOutPowf(0.6)), + ); + } else { + music_box.cross_fade_music( + "The-White-Kitty.mp3", + AudioTween::new(Duration::from_millis(500), AudioEasing::InOutPowf(0.6)), + ); + } + music_state.playing_first = !music_state.playing_first; + } + + if let Some(value) = map_key_values(&input) { + // + // You can set the volume of a channel with a convenience method + // + music_box.set_music_volume(value as f32 / 10.0); + } + + if input.just_released(KeyCode::Return) { + // + // You can also get a mutable ref for the settings, in case you + // need to do any maths with them. If you don't need `MusicBox` + // in a system, then you can directly access the settings with + // `Res<AudioSettings>`, or the ResMut equivalent + // + let mut settings = music_box.settings_mut(); + if settings.master_volume < 1.0 { + settings.master_volume = 1.0; + } else { + settings.master_volume = 0.5; + } + } +} + +fn map_key_values(input: &Res<Input<KeyCode>>) -> Option<usize> { + if input.just_released(KeyCode::Key1) { + Some(1) + } else if input.just_released(KeyCode::Key2) { + Some(2) + } else if input.just_released(KeyCode::Key3) { + Some(3) + } else if input.just_released(KeyCode::Key4) { + Some(4) + } else if input.just_released(KeyCode::Key5) { + Some(5) + } else if input.just_released(KeyCode::Key6) { + Some(6) + } else if input.just_released(KeyCode::Key7) { + Some(7) + } else if input.just_released(KeyCode::Key8) { + Some(8) + } else if input.just_released(KeyCode::Key9) { + Some(9) + } else { + None + } +} diff --git a/examples/utilities/mod.rs b/examples/utilities/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..554f5661e3673d3e539e7e57f943e971e0affe66 --- /dev/null +++ b/examples/utilities/mod.rs @@ -0,0 +1,128 @@ +use bevy::app::Plugin; +use bevy::asset::{Handle, LoadState}; +use bevy::ecs::schedule::ShouldRun; +use bevy::prelude::*; +use bevy_kira_audio::AudioSource; + +/// We store our asset handles in this to avoid Bevy from dropping the assets and reloading +/// when we switch tracks +pub struct AudioResources { + pub white_kitty: Handle<AudioSource>, + pub great_madeja: Handle<AudioSource>, +} + +#[derive(Default, Eq, PartialEq, Debug, Clone, Hash)] +pub enum AppState { + #[default] + Loading, + Running, +} + +pub fn load_resources(mut commands: Commands, assets: Res<AssetServer>) { + let white_kitty = assets.load("The-White-Kitty.mp3"); + let great_madeja = assets.load("The-Great-Madeja.mp3"); + + commands.insert_resource(AudioResources { + white_kitty, + great_madeja, + }) +} + +pub fn check_load_state( + assets: Res<AssetServer>, + resources: Res<AudioResources>, + mut appstate: ResMut<State<AppState>>, +) { + let load_state = + assets.get_group_load_state(vec![resources.white_kitty.id, resources.great_madeja.id]); + + match load_state { + LoadState::Loaded => { + appstate.set(AppState::Running); + } + LoadState::Loading => {} + _ => { + log::error!("The resources are in a bad state! This is a problem"); + } + } +} + +pub fn has_audio_resources(res: Option<Res<AudioResources>>) -> ShouldRun { + res.is_some().into() +} +pub fn is_state_loading(state: Res<AppState>) -> ShouldRun { + (*state == AppState::Loading).into() +} +pub fn is_state_running(state: Res<AppState>) -> ShouldRun { + (*state == AppState::Running).into() +} + +/// This component allows us to easily grab the on screen text +#[derive(Component)] +pub struct TextMarker; +/// This component allows us to easily grab the blank details text area +#[derive(Component)] +pub struct DetailsMarker; + +pub fn create_ui(mut commands: Commands, assets: Res<AssetServer>) { + commands.spawn_bundle(Camera2dBundle::default()); + commands + .spawn_bundle(NodeBundle { + style: Style { + size: Size::new(Val::Percent(100.0), Val::Percent(100.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + flex_direction: FlexDirection::Column, + ..Default::default() + }, + ..Default::default() + }) + .with_children(|children| { + children + .spawn_bundle(TextBundle { + text: Text::from_section( + "Loading Audio Tracks", + TextStyle { + color: Color::BLACK, + font_size: 48.0, + font: assets.load("KenneyBlocks.ttf"), + }, + ), + ..Default::default() + }) + .insert(TextMarker); + children + .spawn_bundle(TextBundle { + text: Text::from_section( + "...", + TextStyle { + color: Color::BLACK, + font_size: 32.0, + font: assets.load("KenneyBlocks.ttf"), + }, + ), + ..Default::default() + }) + .insert(DetailsMarker); + }); +} + +pub struct SetupPlugin; +impl Plugin for SetupPlugin { + fn build(&self, app: &mut App) { + app.add_state(AppState::Loading) + .insert_resource(WindowDescriptor { + width: 800.0, + height: 600.0, + title: String::from("Kitchen Sink Example"), + ..Default::default() + }) + .add_startup_system(load_resources) + .add_startup_system(create_ui) + .add_system_set( + SystemSet::on_update(AppState::Loading) + .with_run_criteria(has_audio_resources) + .with_system(check_load_state), + ); + } +} diff --git a/src/channels.rs b/src/channels.rs index dd056a2dbc33d1b5e3c0ebd6971c8ed34a9d64db..eb49fc74b93611a51c6f9af93ae153ff9be08a66 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -1,23 +1,29 @@ -/// The first channel to use for main music tracks. Combined with -/// `music audio channel b` to perform cross-fades -/// *Volume Type:* Music -pub struct MusicAudioChannelA; -/// The backup channel to use for main music tracks. Combined with -/// `music audio channel a` to perform cross-fades -/// *Volume Type:* Music -pub struct MusicAudioChannelB; -/// The first channel to use for background ambiance tracks. -/// Combined with `ambiance audio channel b` to perform cross-fades -/// *Volume Type:* SFX -pub struct AmbianceAudioChannelA; -/// The backup channel to use for background ambiance tracks. -/// Combined with `ambiance audio channel a` to perform cross-fades -/// *Volume Type:* SFX -pub struct AmbianceAudioChannelB; -/// The channel used for generic sound effects, such as spells, hits, -/// attacks, etc. -/// *Volume Type:* SFX +/// The channel used for playing main background music tracks. When a track is started in this +/// channel, the currently playing track (if it exist) will be stopped. Tweens can be applied to +/// control this transition +/// +/// - *Volume Type:* Music +/// - *Supports Concurrent Sounds:* No +/// - *Supports Transitions*: Yes +pub struct MusicAudioChannel; +/// The channel used for playing an ambient background track. An ambient background track can +/// be considered similar to a music track, but consisting of composite sound effects to create +/// a certain feeling, often boosting immersion. +/// +/// - *Volume Type:* SFX +/// - *Supports Concurrent Sounds:* No +/// - *Supports Transitions*: Yes +pub struct AmbianceAudioChannel; +/// The channel used for generic one shot sound effects, such as spells, hits, attacks, grunts, etc. +/// +/// - *Volume Type:* SFX +/// - *Supports Concurrent Sounds:* Yes +/// - *Supports Transitions:* Yes pub struct SfxAudioChannel; -/// The channel used for any UI related sound effects -/// *Volume Type:* UI SFX +/// The channel used for any UI related one-shot sound effects. The volume of ui sfx can be +/// controlled separately from game related sound effects +/// +/// - *Volume Type:* UI SFX +/// - *Supports Concurrent Sounds:* Yes +/// - *Supports Transitions:* Yes pub struct UiSfxAudioChannel; diff --git a/src/lib.rs b/src/lib.rs index 28a8637fa49144ea618d86300d2ea70c51deab5b..2a52e91f53d8132d9708af96d184dd0be81c2a75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,41 +1,81 @@ -use bevy::app::{App, Plugin, PluginGroup, PluginGroupBuilder}; +use std::marker::PhantomData; + +use bevy::app::{App, CoreStage, Plugin, PluginGroup, PluginGroupBuilder}; use bevy_kira_audio::{AudioApp, AudioPlugin}; use crate::channels::{ - AmbianceAudioChannelA, AmbianceAudioChannelB, MusicAudioChannelA, MusicAudioChannelB, - SfxAudioChannel, UiSfxAudioChannel, + AmbianceAudioChannel, MusicAudioChannel, SfxAudioChannel, UiSfxAudioChannel, }; -use crate::utilities::{AudioCrossFade, AudioSettings}; +use crate::music_box::MusicBoxState; +use crate::utilities::{AudioSettings, SuppliesAudio}; +/// Musicbox encapsulates the concept of different sound types in a number of preset channels, that +/// can be used in certain ways pub mod channels; pub mod music_box; pub mod utilities; pub mod prelude { + pub use bevy_kira_audio::{ + AudioChannel, AudioControl, AudioEasing, AudioInstance, AudioSettings as KiraAudioSettings, + AudioSource, AudioTween, + }; + pub use super::channels::*; pub use super::music_box::MusicBox; - pub use bevy_kira_audio::{AudioSource, AudioChannel}; + pub use super::utilities::AudioSettings; } -pub struct MusicBoxPlugin; +pub struct MusicBoxPlugin<T: SuppliesAudio> { + _t: PhantomData<T>, +} + +impl<T: SuppliesAudio> Default for MusicBoxPlugin<T> { + fn default() -> Self { + Self { + _t: PhantomData::default(), + } + } +} + +impl<T: SuppliesAudio> MusicBoxPlugin<T> { + pub fn new() -> MusicBoxPlugin<T> { + Default::default() + } +} -impl Plugin for MusicBoxPlugin { +impl<T: SuppliesAudio> Plugin for MusicBoxPlugin<T> { fn build(&self, app: &mut App) { - app.add_audio_channel::<MusicAudioChannelA>() - .add_audio_channel::<MusicAudioChannelB>() - .add_audio_channel::<AmbianceAudioChannelA>() - .add_audio_channel::<AmbianceAudioChannelB>() + app.add_audio_channel::<MusicAudioChannel>() + .add_audio_channel::<AmbianceAudioChannel>() .add_audio_channel::<SfxAudioChannel>() .add_audio_channel::<UiSfxAudioChannel>() .insert_resource(AudioSettings::default()) - .insert_resource(AudioCrossFade::default()); + .insert_resource(MusicBoxState::default()) + .add_system_to_stage(CoreStage::Last, utilities::sync_music_volume::<T>); } } -pub struct CombinedAudioPlugins; -impl PluginGroup for CombinedAudioPlugins { +pub struct CombinedAudioPlugins<T: SuppliesAudio> { + _t: PhantomData<T>, +} + +impl<T: SuppliesAudio> Default for CombinedAudioPlugins<T> { + fn default() -> Self { + Self { + _t: PhantomData::default(), + } + } +} + +impl<T: SuppliesAudio> CombinedAudioPlugins<T> { + pub fn new() -> CombinedAudioPlugins<T> { + Default::default() + } +} + +impl<T: SuppliesAudio> PluginGroup for CombinedAudioPlugins<T> { fn build(&mut self, group: &mut PluginGroupBuilder) { - group.add(AudioPlugin) - .add(MusicBoxPlugin); + group.add(AudioPlugin).add(MusicBoxPlugin::<T>::new()); } -} \ No newline at end of file +} diff --git a/src/music_box.rs b/src/music_box.rs index 7647ff1f8ef33015cabee05b0bb6d2992ed86566..7028bf29ee860dd41d263c5c570e63adf3339705 100644 --- a/src/music_box.rs +++ b/src/music_box.rs @@ -1,21 +1,16 @@ use std::marker::PhantomData; use bevy::ecs::system::SystemParam; -use bevy::prelude::*; -use bevy_kira_audio::{AudioChannel, InstanceHandle}; +use bevy::prelude::{Assets, Commands, Handle, Res, ResMut}; +use bevy_kira_audio::{AudioChannel, AudioControl, AudioInstance, AudioSource, AudioTween}; -use crate::utilities::{AudioSettings, CrossFadeTrack, SuppliesAudio, TrackType}; -use crate::{ - AmbianceAudioChannelA, AmbianceAudioChannelB, AudioCrossFade, MusicAudioChannelA, - MusicAudioChannelB, SfxAudioChannel, UiSfxAudioChannel, -}; +use crate::utilities::{AudioSettings, SuppliesAudio, TrackType}; +use crate::{AmbianceAudioChannel, MusicAudioChannel, SfxAudioChannel, UiSfxAudioChannel}; #[derive(SystemParam)] pub struct AudioChannels<'w, 's> { - pub music_channel_a: Res<'w, AudioChannel<MusicAudioChannelA>>, - pub music_channel_b: Res<'w, AudioChannel<MusicAudioChannelB>>, - pub ambiance_channel_a: Res<'w, AudioChannel<AmbianceAudioChannelA>>, - pub ambiance_channel_b: Res<'w, AudioChannel<AmbianceAudioChannelB>>, + pub music_channel: Res<'w, AudioChannel<MusicAudioChannel>>, + pub ambiance_channel: Res<'w, AudioChannel<AmbianceAudioChannel>>, pub sfx_channel: Res<'w, AudioChannel<SfxAudioChannel>>, pub ui_sfx_channel: Res<'w, AudioChannel<UiSfxAudioChannel>>, @@ -25,100 +20,211 @@ pub struct AudioChannels<'w, 's> { #[derive(SystemParam)] pub struct MusicBox<'w, 's, T: SuppliesAudio> { - pub commands: Commands<'w, 's>, - pub channels: AudioChannels<'w, 's>, - pub handles: Res<'w, T>, - pub settings: Res<'w, AudioSettings>, - pub fade_state: Res<'w, AudioCrossFade>, + channels: AudioChannels<'w, 's>, + handles: Res<'w, T>, + settings: ResMut<'w, AudioSettings>, + state: ResMut<'w, MusicBoxState>, + audio_instances: ResMut<'w, Assets<AudioInstance>>, } -pub enum MusicTrackState { - Pending, - Playing, - FadeOut { progress: f32 }, - FadeIn { progress: f32 }, - CrossFade { out_progress: f32, in_progress: f32 }, +/// Tracks the currently active audio instance singleton channels, to allow +/// for transitions +#[derive(Debug, Default)] +pub struct MusicBoxState { + pub active_music: Option<Handle<AudioInstance>>, + pub active_ambiance: Option<Handle<AudioInstance>>, } -impl Default for MusicTrackState { - fn default() -> Self { - Self::Pending +impl<'w, 's, T: SuppliesAudio> MusicBox<'w, 's, T> { + pub fn cross_fade_music<Name: ToString>( + &mut self, + name: Name, + fade: AudioTween, + ) -> Option<Handle<AudioInstance>> { + self.fade_out_music(fade.clone()); + self.fade_in_music(name, fade) } -} -impl<'w, 's, T: SuppliesAudio> MusicBox<'w, 's, T> { - pub fn play_looped_music<Name: ToString>(&self, name: Name) -> Option<InstanceHandle> { - self.channels.music_channel_a.stop(); - self.channels.music_channel_b.stop(); - - match self.resolve_track_name(name) { - TrackType::Single(track) => match self.fade_state.music.active { - CrossFadeTrack::A => Some( - self.channels - .music_channel_a - .play_looped(track.clone_weak()), - ), - CrossFadeTrack::B => Some( - self.channels - .music_channel_b - .play_looped(track.clone_weak()), - ), - }, - TrackType::WithIntro(intro, looped) => match self.fade_state.music.active { - CrossFadeTrack::A => Some( - self.channels - .music_channel_a - .play_looped_with_intro(intro.clone_weak(), looped.clone_weak()), - ), - CrossFadeTrack::B => Some( - self.channels - .music_channel_b - .play_looped_with_intro(intro.clone_weak(), looped.clone_weak()), - ), - }, + pub fn in_out_fade_music<Name: ToString>( + &mut self, + name: Name, + in_tween: AudioTween, + out_tween: AudioTween, + ) -> Option<Handle<AudioInstance>> { + self.fade_out_music(out_tween); + self.fade_in_music(name, in_tween) + } + + pub fn play_music<Name: ToString>(&mut self, name: Name) -> Option<Handle<AudioInstance>> { + self.stop_music(); + self.fade_in_music(name, AudioTween::default()) + } + + pub fn stop_music(&mut self) { + self.fade_out_music(AudioTween::default()); + } + + pub fn fade_out_music(&mut self, fade: AudioTween) { + let handle = std::mem::replace(&mut self.state.active_music, None) + .and_then(|handle| self.audio_instances.get_mut(&handle)); + if let Some(current) = handle { + current.stop(fade); + } + } + + pub fn fade_in_music<Name: ToString>( + &mut self, + name: Name, + fade: AudioTween, + ) -> Option<Handle<AudioInstance>> { + match self.map_tracks(name) { + TrackType::WithIntro(_, track) | TrackType::Single(track) => { + let next = self + .channels + .music_channel + .play(track) + .fade_in(fade) + .looped() + .handle(); + self.state.active_music = Some(next.clone()); + + Some(next) + } + TrackType::Missing => None, + } + } + + pub fn cross_fade_ambiance<Name: ToString>( + &mut self, + name: Name, + fade: AudioTween, + ) -> Option<Handle<AudioInstance>> { + self.fade_out_ambiance(fade.clone()); + self.fade_in_ambiance(name, fade) + } + + pub fn in_out_fade_ambiance<Name: ToString>( + &mut self, + name: Name, + in_tween: AudioTween, + out_tween: AudioTween, + ) -> Option<Handle<AudioInstance>> { + self.fade_out_ambiance(out_tween); + self.fade_in_ambiance(name, in_tween) + } + + pub fn play_ambiance<Name: ToString>(&mut self, name: Name) -> Option<Handle<AudioInstance>> { + self.stop_ambiance(); + self.fade_in_ambiance(name, AudioTween::default()) + } + + pub fn stop_ambiance(&mut self) { + self.fade_out_ambiance(AudioTween::default()); + } + + pub fn fade_out_ambiance(&mut self, fade: AudioTween) { + let handle = std::mem::replace(&mut self.state.active_ambiance, None) + .and_then(|handle| self.audio_instances.get_mut(&handle)); + if let Some(current) = handle { + current.stop(fade); + } + } + + pub fn fade_in_ambiance<Name: ToString>( + &mut self, + name: Name, + fade: AudioTween, + ) -> Option<Handle<AudioInstance>> { + match self.map_tracks(name) { + TrackType::WithIntro(_, track) | TrackType::Single(track) => { + let next = self + .channels + .ambiance_channel + .play(track) + .fade_in(fade) + .looped() + .handle(); + self.state.active_ambiance = Some(next.clone()); + + Some(next) + } TrackType::Missing => None, } } - pub fn play_effect_once<Name: ToString>(&mut self, name: Name) -> Option<InstanceHandle> { - let name = name.to_string(); + pub fn play_sfx<Name: ToString>(&mut self, name: Name) -> Option<Handle<AudioInstance>> { + match self.map_tracks(name) { + TrackType::WithIntro(_, track) | TrackType::Single(track) => { + let instance = self.channels.sfx_channel.play(track).handle(); + Some(instance) + } + TrackType::Missing => None, + } + } - match self.handles.get_audio_track(&name) { - Some(track) => Some(self.channels.sfx_channel.play(track)), - None => None, + pub fn play_ui_sfx<Name: ToString>(&mut self, name: Name) -> Option<Handle<AudioInstance>> { + match self.map_tracks(name) { + TrackType::WithIntro(_, track) | TrackType::Single(track) => { + let instance = self.channels.ui_sfx_channel.play(track).handle(); + Some(instance) + } + TrackType::Missing => None, } } - pub fn play_ambiance<Name: ToString>(&self, name: Name) -> Option<InstanceHandle> { - self.channels.ambiance_channel_a.stop(); - self.channels.ambiance_channel_b.stop(); - - match self.resolve_track_name(name) { - TrackType::Single(track) => match self.fade_state.ambiance.active { - CrossFadeTrack::A => Some( - self.channels - .ambiance_channel_a - .play_looped(track.clone_weak()), - ), - CrossFadeTrack::B => Some( - self.channels - .ambiance_channel_b - .play_looped(track.clone_weak()), - ), + pub fn sync_settings(&self) { + self.channels + .music_channel + .set_volume((self.settings.music_volume * self.settings.master_volume) as f64); + self.channels + .ambiance_channel + .set_volume((self.settings.ambiance_volume * self.settings.master_volume) as f64); + self.channels + .sfx_channel + .set_volume((self.settings.sfx_volume * self.settings.master_volume) as f64); + self.channels + .ui_sfx_channel + .set_volume((self.settings.ui_volume * self.settings.master_volume) as f64); + } + + pub fn settings(&self) -> &AudioSettings { + &self.settings + } + + pub fn settings_mut(&mut self) -> &mut AudioSettings { + &mut self.settings + } + + pub fn set_master_volume(&mut self, level: f32) { + self.settings.master_volume = level; + } + pub fn set_music_volume(&mut self, level: f32) { + self.settings.music_volume = level; + } + pub fn set_ambiance_volume(&mut self, level: f32) { + self.settings.ambiance_volume = level; + } + pub fn set_sfx_volume(&mut self, level: f32) { + self.settings.sfx_volume = level; + } + pub fn set_ui_sfx_volume(&mut self, level: f32) { + self.settings.ui_volume = level; + } + + fn map_tracks<Name: ToString>(&'w self, name: Name) -> TrackType<Handle<AudioSource>> { + match self.handles.resolve_track_name(name) { + TrackType::Single(name) => match self.handles.get_audio_track(name) { + Some(handle) => TrackType::Single(handle), + None => TrackType::Missing, }, - TrackType::WithIntro(intro, looped) => match self.fade_state.ambiance.active { - CrossFadeTrack::A => Some( - self.channels - .ambiance_channel_a - .play_looped_with_intro(intro.clone_weak(), looped.clone_weak()), - ), - CrossFadeTrack::B => Some( - self.channels - .ambiance_channel_b - .play_looped_with_intro(intro.clone_weak(), looped.clone_weak()), - ), + TrackType::WithIntro(intro, looper) => match ( + self.handles.get_audio_track(intro), + self.handles.get_audio_track(looper), + ) { + (Some(intro), Some(looper)) => TrackType::WithIntro(intro, looper), + _ => TrackType::Missing, }, - TrackType::Missing => None, + TrackType::Missing => TrackType::Missing, } } } diff --git a/src/utilities.rs b/src/utilities.rs index e41343106240795ce92bf0f05e73ba24e22abd31..1c02aa9404439422c19953095a4b73501bab1ed2 100644 --- a/src/utilities.rs +++ b/src/utilities.rs @@ -1,91 +1,52 @@ use bevy::ecs::system::Resource; use bevy::prelude::*; use bevy_kira_audio::AudioSource; -use serde::{Deserialize, Serialize}; use crate::music_box::MusicBox; pub trait SuppliesAudio: Resource { + fn resolve_track_name<T: ToString>(&self, name: T) -> TrackType<String>; fn get_audio_track<T: ToString>(&self, name: T) -> Option<Handle<AudioSource>>; } -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct AudioSettings { - master_volume: f32, - music_volume: f32, - sfx_volume: f32, - ui_volume: f32, + pub master_volume: f32, + pub music_volume: f32, + pub ambiance_volume: f32, + pub sfx_volume: f32, + pub ui_volume: f32, } impl Default for AudioSettings { fn default() -> Self { Self { master_volume: 1.0, - music_volume: 0.0, - sfx_volume: 0.0, - ui_volume: 0.0, + music_volume: 1.0, + ambiance_volume: 1.0, + sfx_volume: 1.0, + ui_volume: 1.0, } } } -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)] -pub enum CrossFadeTrack { - #[default] - A, - B, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] -pub struct CrossFadeState { - pub active: CrossFadeTrack, - pub next: CrossFadeTrack, - pub progress: f32, -} - -impl Default for CrossFadeState { - fn default() -> Self { - Self { - active: CrossFadeTrack::A, - next: CrossFadeTrack::B, - progress: 0.0, - } - } -} - -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)] -pub struct AudioCrossFade { - pub music: CrossFadeState, - pub ambiance: CrossFadeState, -} - -pub enum TrackType { - Single(Handle<AudioSource>), - WithIntro(Handle<AudioSource>, Handle<AudioSource>), +pub enum TrackType<T> { + Single(T), + WithIntro(T, T), Missing, } -impl<'w, 's, T: SuppliesAudio> MusicBox<'w, 's, T> { - pub fn resolve_track_name<Name: ToString>(&'w self, name: Name) -> TrackType { - let name = name.to_string(); - - if let (Some(intro), Some(looped)) = ( - self.handles.get_audio_track(format!("{}_intro", &name)), - self.handles.get_audio_track(format!("{}_loop", &name)), - ) { - TrackType::WithIntro(intro, looped) - } else if let Some(track) = self.handles.get_audio_track(name.clone()) { - TrackType::Single(track) - } else if let Some(track) = self.handles.get_audio_track(format!("{}_looped", name)) { - TrackType::Single(track) - } else { - TrackType::Missing - } +impl SuppliesAudio for AssetServer { + fn resolve_track_name<T: ToString>(&self, name: T) -> TrackType<String> { + TrackType::Single(name.to_string()) } -} - -impl SuppliesAudio for AssetServer { fn get_audio_track<T: ToString>(&self, name: T) -> Option<Handle<AudioSource>> { Some(self.load(&name.to_string())) } -} \ No newline at end of file +} + +pub fn sync_music_volume<T: SuppliesAudio>(music: MusicBox<T>) { + music.sync_settings(); +}