diff --git a/.gitignore b/.gitignore index 448668577b5a00ec14ce582567753b185de56bd8..a89933bfd13b47b74cf2639a2addb00f9b10e511 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +# IDE Files +.idea diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..b07f6a589cbecc175f7ab52feac49d709bd2e685 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "micro_bevy_musicbox" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bevy = { version = "0.7", default-features = false } +bevy_kira_audio = { version = "0.10", features = ["mp3"] } +serde = "1" +serde_json = "1" \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000000000000000000000000000000000000..271800cb2f3791b3adc24328e71c9e2550b439db --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000000000000000000000000000000000000..ddac46a818a71283d5b1d79d0d01761aaceb5b84 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +hard_tabs = true +group_imports = "StdExternalCrate" +use_field_init_shorthand = true +use_try_shorthand = true \ No newline at end of file diff --git a/src/channels.rs b/src/channels.rs new file mode 100644 index 0000000000000000000000000000000000000000..dd056a2dbc33d1b5e3c0ebd6971c8ed34a9d64db --- /dev/null +++ b/src/channels.rs @@ -0,0 +1,23 @@ +/// 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 +pub struct SfxAudioChannel; +/// The channel used for any UI related sound effects +/// *Volume Type:* UI SFX +pub struct UiSfxAudioChannel; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..28a8637fa49144ea618d86300d2ea70c51deab5b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,41 @@ +use bevy::app::{App, Plugin, PluginGroup, PluginGroupBuilder}; +use bevy_kira_audio::{AudioApp, AudioPlugin}; + +use crate::channels::{ + AmbianceAudioChannelA, AmbianceAudioChannelB, MusicAudioChannelA, MusicAudioChannelB, + SfxAudioChannel, UiSfxAudioChannel, +}; +use crate::utilities::{AudioCrossFade, AudioSettings}; + +pub mod channels; +pub mod music_box; +pub mod utilities; + +pub mod prelude { + pub use super::channels::*; + pub use super::music_box::MusicBox; + pub use bevy_kira_audio::{AudioSource, AudioChannel}; +} + +pub struct MusicBoxPlugin; + +impl Plugin for MusicBoxPlugin { + fn build(&self, app: &mut App) { + app.add_audio_channel::<MusicAudioChannelA>() + .add_audio_channel::<MusicAudioChannelB>() + .add_audio_channel::<AmbianceAudioChannelA>() + .add_audio_channel::<AmbianceAudioChannelB>() + .add_audio_channel::<SfxAudioChannel>() + .add_audio_channel::<UiSfxAudioChannel>() + .insert_resource(AudioSettings::default()) + .insert_resource(AudioCrossFade::default()); + } +} + +pub struct CombinedAudioPlugins; +impl PluginGroup for CombinedAudioPlugins { + fn build(&mut self, group: &mut PluginGroupBuilder) { + group.add(AudioPlugin) + .add(MusicBoxPlugin); + } +} \ No newline at end of file diff --git a/src/music_box.rs b/src/music_box.rs new file mode 100644 index 0000000000000000000000000000000000000000..7647ff1f8ef33015cabee05b0bb6d2992ed86566 --- /dev/null +++ b/src/music_box.rs @@ -0,0 +1,124 @@ +use std::marker::PhantomData; + +use bevy::ecs::system::SystemParam; +use bevy::prelude::*; +use bevy_kira_audio::{AudioChannel, InstanceHandle}; + +use crate::utilities::{AudioSettings, CrossFadeTrack, SuppliesAudio, TrackType}; +use crate::{ + AmbianceAudioChannelA, AmbianceAudioChannelB, AudioCrossFade, MusicAudioChannelA, + MusicAudioChannelB, 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 sfx_channel: Res<'w, AudioChannel<SfxAudioChannel>>, + pub ui_sfx_channel: Res<'w, AudioChannel<UiSfxAudioChannel>>, + + #[system_param(ignore)] + _p: PhantomData<&'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>, +} + +pub enum MusicTrackState { + Pending, + Playing, + FadeOut { progress: f32 }, + FadeIn { progress: f32 }, + CrossFade { out_progress: f32, in_progress: f32 }, +} + +impl Default for MusicTrackState { + fn default() -> Self { + Self::Pending + } +} + +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()), + ), + }, + TrackType::Missing => None, + } + } + + pub fn play_effect_once<Name: ToString>(&mut self, name: Name) -> Option<InstanceHandle> { + let name = name.to_string(); + + match self.handles.get_audio_track(&name) { + Some(track) => Some(self.channels.sfx_channel.play(track)), + None => 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()), + ), + }, + 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::Missing => None, + } + } +} diff --git a/src/utilities.rs b/src/utilities.rs new file mode 100644 index 0000000000000000000000000000000000000000..76f085a98404949d11febc549bab52272fe952bd --- /dev/null +++ b/src/utilities.rs @@ -0,0 +1,84 @@ +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 get_audio_track<T: ToString>(&self, name: T) -> Option<Handle<AudioSource>>; +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub struct AudioSettings { + master_volume: f32, + music_volume: f32, + sfx_volume: f32, + 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, + } + } +} + +#[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>), + 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 + } + } +}