Skip to content
Snippets Groups Projects
music_box.rs 10.1 KiB
Newer Older
use std::marker::PhantomData;

use bevy::ecs::system::SystemParam;
Louis's avatar
Louis committed
use bevy::prelude::{Assets, Commands, Handle, Res, ResMut};
use bevy_kira_audio::{AudioChannel, AudioControl, AudioInstance, AudioSource, AudioTween};
Louis's avatar
Louis committed
use crate::utilities::{AudioSettings, SuppliesAudio, TrackType};
use crate::{AmbianceAudioChannel, MusicAudioChannel, SfxAudioChannel, UiSfxAudioChannel};
Louis's avatar
Louis committed
/// A wrapper for each of the audio channels created and controlled by MusicBox
#[derive(SystemParam)]
pub struct AudioChannels<'w, 's> {
Louis's avatar
Louis committed
	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>>,

	#[system_param(ignore)]
	_p: PhantomData<&'s ()>,
}

Louis's avatar
Louis committed
/// The service object used to control your game's audio
///
/// ## `T: SuppliesAudio`
///
/// The particular implementation of `SuppliesAudio` will be used to verify that a music track exists,
/// and then to retrieve the associated `AudioSource`.
#[derive(SystemParam)]
pub struct MusicBox<'w, 's, T: SuppliesAudio> {
Louis's avatar
Louis committed
	channels: AudioChannels<'w, 's>,
	handles: Res<'w, T>,
	settings: ResMut<'w, AudioSettings>,
	state: ResMut<'w, MusicBoxState>,
	audio_instances: ResMut<'w, Assets<AudioInstance>>,
Louis's avatar
Louis committed
/// 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>>,
Louis's avatar
Louis committed
impl<'w, 's, T: SuppliesAudio> MusicBox<'w, 's, T> {
Louis's avatar
Louis committed
	/// Start playing a new audio track on the Music channel. The provided tween will be used
	/// to fade in the new track, and to fade out the old track
	///
	/// # Returns
	///
	/// A handle for the newly started audio instance, or `None` if the track was not found
Louis's avatar
Louis committed
	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)
Louis's avatar
Louis committed
	/// Start playing a new audio track on the Music channel. The "in_tween" will be used to fade
	/// in the new audio track, while the "out_tween" will be used to fade out the old audio
	/// track
	///
	/// # Returns
	///
	/// A handle for the newly started audio instance, or `None` if the track was not found
Louis's avatar
Louis committed
	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)
	}

Louis's avatar
Louis committed
	/// Start playing a new audio track on the Music channel. If another track is playing, it will
	/// be immediately stopped
	///
	/// # Returns
	///
	/// A handle for the newly started audio instance, or `None` if the track was not found
Louis's avatar
Louis committed
	pub fn play_music<Name: ToString>(&mut self, name: Name) -> Option<Handle<AudioInstance>> {
		self.stop_music();
		self.fade_in_music(name, AudioTween::default())
	}

Louis's avatar
Louis committed
	/// Stop playing music on the Music channel
Louis's avatar
Louis committed
	pub fn stop_music(&mut self) {
		self.fade_out_music(AudioTween::default());
	}

Louis's avatar
Louis committed
	/// Stop playing music on the music channel. The supplied tween will be used to fade out the track
	/// before it ends
Louis's avatar
Louis committed
	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);
		}
	}

Louis's avatar
Louis committed
	/// Start playing a new track on the music channel. If another track is playing, it will be
	/// immediately stopped. The provided tween will be used to fade in the new track
	///
	/// # Returns
	///
	/// A handle for the newly started audio instance, or `None` if the track was not found
Louis's avatar
Louis committed
	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,
		}
	}

Louis's avatar
Louis committed
	/// Start playing a new audio track on the Music channel. The provided tween will be used
	/// to fade in the new track, and to fade out the old track
	///
	/// # Returns
	///
	/// A handle for the newly started audio instance, or `None` if the track was not found
Louis's avatar
Louis committed
	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)
	}

Louis's avatar
Louis committed
	/// Start playing a new audio track on the Music channel. The "in_tween" will be used to fade
	/// in the new audio track, while the "out_tween" will be used to fade out the old audio
	/// track
	///
	/// # Returns
	///
	/// A handle for the newly started audio instance, or `None` if the track was not found
Louis's avatar
Louis committed
	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)
	}

Louis's avatar
Louis committed
	/// Start playing a new audio track on the Music channel. If another track is playing, it will
	/// be immediately stopped
	///
	/// # Returns
	///
	/// A handle for the newly started audio instance, or `None` if the track was not found
Louis's avatar
Louis committed
	pub fn play_ambiance<Name: ToString>(&mut self, name: Name) -> Option<Handle<AudioInstance>> {
		self.stop_ambiance();
		self.fade_in_ambiance(name, AudioTween::default())
	}

Louis's avatar
Louis committed
	/// Stop playing ambiance on the Music channel
Louis's avatar
Louis committed
	pub fn stop_ambiance(&mut self) {
		self.fade_out_ambiance(AudioTween::default());
	}

Louis's avatar
Louis committed
	/// Stop playing ambiance on the ambiance channel. The supplied tween will be used to fade out the track
	/// before it ends
Louis's avatar
Louis committed
	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);
		}
	}

Louis's avatar
Louis committed
	/// Start playing a new track on the ambiance channel. If another track is playing, it will be
	/// immediately stopped. The provided tween will be used to fade in the new track
	///
	/// # Returns
	///
	/// A handle for the newly started audio instance, or `None` if the track was not found
Louis's avatar
Louis committed
	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,
		}
	}

Louis's avatar
Louis committed
	/// Play a new sound effect on the SFX channel
	///
	/// # Returns
	///
	/// A handle for the newly started audio instance, or `None` if the track was not found
Louis's avatar
Louis committed
	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,
		}
	}
Louis's avatar
Louis committed
	/// Play a new sound effect on the UI SFX channel
	///
	/// # Returns
	///
	/// A handle for the newly started audio instance, or `None` if the track was not found
Louis's avatar
Louis committed
	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,
Louis's avatar
Louis committed
	/// Sync the actual volumes of each track to their values defined in the `AudioSettings` resource,
	/// modulated by the master volume setting
Louis's avatar
Louis committed
	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);
	}

Louis's avatar
Louis committed
	/// Get a reference to the settings object
Louis's avatar
Louis committed
	pub fn settings(&self) -> &AudioSettings {
		&self.settings
	}

Louis's avatar
Louis committed
	/// Get a mutable reference to the settings object
Louis's avatar
Louis committed
	pub fn settings_mut(&mut self) -> &mut AudioSettings {
		&mut self.settings
	}

Louis's avatar
Louis committed
	/// Sets the master volume level, as a percentage between 0-1.
	/// Master volume is used to adjust all of the other volume levels
Louis's avatar
Louis committed
	pub fn set_master_volume(&mut self, level: f32) {
		self.settings.master_volume = level;
	}
Louis's avatar
Louis committed

	/// Sets the music volume level, as a percentage between 0-1.
Louis's avatar
Louis committed
	pub fn set_music_volume(&mut self, level: f32) {
		self.settings.music_volume = level;
	}
Louis's avatar
Louis committed

	/// Sets the ambiance volume level, as a percentage between 0-1.
Louis's avatar
Louis committed
	pub fn set_ambiance_volume(&mut self, level: f32) {
		self.settings.ambiance_volume = level;
	}
Louis's avatar
Louis committed

	/// Sets the sfx volume level, as a percentage between 0-1.
Louis's avatar
Louis committed
	pub fn set_sfx_volume(&mut self, level: f32) {
		self.settings.sfx_volume = level;
	}
Louis's avatar
Louis committed

	/// Sets the ui sfx volume level, as a percentage between 0-1.
Louis's avatar
Louis committed
	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,
Louis's avatar
Louis committed
			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,
Louis's avatar
Louis committed
			TrackType::Missing => TrackType::Missing,