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

use bevy::ecs::system::SystemParam;
Louis's avatar
Louis committed
use bevy::prelude::{Assets, Commands, DetectChanges, Handle, Res, ResMut, Resource};
Louis's avatar
Louis committed
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
Louis's avatar
Louis committed
#[derive(Debug, Default, Resource)]
Louis's avatar
Louis committed
pub struct MusicBoxState {
	pub active_music: Option<Handle<AudioInstance>>,
	/// The name used to start the currently active music track
	pub active_music_name: Option<String>,
Louis's avatar
Louis committed
	pub active_ambiance: Option<Handle<AudioInstance>>,
	/// The name used to start the currently active ambiance track
	pub active_ambiance_name: Option<String>,
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>> {
		if self.state.active_music_name == Some(name.to_string()) {
			return self.state.active_music.as_ref().map(|f| f.clone_weak());
		}

Louis's avatar
Louis committed
		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>> {
		if self.state.active_music_name == Some(name.to_string()) {
			return self.state.active_music.as_ref().map(|f| f.clone_weak());
		}

Louis's avatar
Louis committed
		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>> {
		if self.state.active_music_name == Some(name.to_string()) {
			return self.state.active_music.as_ref().map(|f| f.clone_weak());
		}

Louis's avatar
Louis committed
		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) {
		self.state.active_music_name = None;

		let handle = self
			.state
			.active_music
			.take()
Louis's avatar
Louis committed
			.and_then(|handle| self.audio_instances.get_mut(&handle));
Louis's avatar
Louis committed
		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>> {
		let name = name.to_string();

		if self.state.active_music_name.as_ref() == Some(&name) {
			return self.state.active_music.as_ref().map(|f| f.clone_weak());
		}

		match self.map_tracks(&name) {
Louis's avatar
Louis committed
			TrackType::WithIntro(_, track) | TrackType::Single(track) => {
				let next = self
					.channels
					.music_channel
					.play(track)
					.fade_in(fade)
					.looped()
					.handle();

				self.state.active_music_name = Some(name.clone());
Louis's avatar
Louis committed
				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 = self
			.state
			.active_ambiance
			.take()
Louis's avatar
Louis committed
			.and_then(|handle| self.audio_instances.get_mut(&handle));
Louis's avatar
Louis committed
		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,
		}
	}

	/// Play a new sound effect on the SFX channel in a loop. Ideal for ambient spatial sounds
	/// like a crackling fireplace
	///
	/// # Returns
	///
	/// A handle for the newly started audio instance, or `None` if the track was not found
	pub fn start_looped_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).looped().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) {
Louis's avatar
Louis committed
		if self.settings.is_changed() {
			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;
	}

	/// Stop an audio instance handle from playing immediately
	pub fn stop_handle(&mut self, handle: &Handle<AudioInstance>) {
		if let Some(instance) = self.audio_instances.get_mut(handle) {
			instance.stop(AudioTween::default());
		}
	}
	/// Stop an audio instance handle from playing with a given tween
	pub fn stop_handle_with_tween(&mut self, handle: &Handle<AudioInstance>, tween: AudioTween) {
		if let Some(instance) = self.audio_instances.get_mut(handle) {
			instance.stop(tween);
		}
	}
	/// Pause an audio instance handle immediately, in a way that can be resumed later
	pub fn pause_handle(&mut self, handle: &Handle<AudioInstance>) {
		if let Some(instance) = self.audio_instances.get_mut(handle) {
			instance.pause(AudioTween::default());
		}
	}
	/// Pause an audio instance with a given tween in a way that can be resumed later
	pub fn pause_handle_with_tween(&mut self, handle: &Handle<AudioInstance>, tween: AudioTween) {
		if let Some(instance) = self.audio_instances.get_mut(handle) {
			instance.pause(tween);
		}
	}
	/// Stop an audio instance handle from playing immediately
	pub fn resume_handle(&mut self, handle: &Handle<AudioInstance>) {
		if let Some(instance) = self.audio_instances.get_mut(handle) {
			instance.resume(AudioTween::default());
		}
	}
	/// Stop an audio instance handle from playing with a given tween
	pub fn resume_handle_with_tween(&mut self, handle: &Handle<AudioInstance>, tween: AudioTween) {
		if let Some(instance) = self.audio_instances.get_mut(handle) {
			instance.resume(tween);
		}
	}

Louis's avatar
Louis committed
	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,