Skip to content
Snippets Groups Projects
Verified Commit be3119ab authored by Louis's avatar Louis :fire:
Browse files

Update to bevy_kira_audio 0.12

parent 7f8625fa
No related branches found
No related tags found
No related merge requests found
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
[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
# 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/)
File added
File added
File added
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");
}
// 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
}
}
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),
);
}
}
/// 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;
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
}
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,
}
}
}
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();
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment