use crate::plugins::assets::GodotResource;
use crate::plugins::audio::{
AudioCommand, AudioPlayerType, AudioSettings, AudioTween, PlayCommand, SoundId,
};
use bevy_asset::Handle;
use bevy_ecs::prelude::Resource;
use bevy_math::{Vec2, Vec3};
use parking_lot::RwLock;
use std::collections::VecDeque;
use std::marker::PhantomData;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ChannelId(pub &'static str);
#[derive(Debug, Clone)]
pub(crate) struct ChannelState {
#[allow(dead_code)]
pub volume: f32,
#[allow(dead_code)]
pub pitch: f32,
#[allow(dead_code)]
pub paused: bool,
#[allow(dead_code)]
pub panning: f32, }
impl Default for ChannelState {
fn default() -> Self {
Self {
volume: 1.0,
pitch: 1.0,
paused: false,
panning: 0.0,
}
}
}
pub trait AudioChannelMarker: Resource {
const CHANNEL_NAME: &'static str;
}
#[derive(Resource)]
pub struct AudioChannel<T: AudioChannelMarker> {
pub(crate) channel_id: ChannelId,
pub(crate) commands: RwLock<VecDeque<AudioCommand>>,
_marker: PhantomData<T>,
}
impl<T: AudioChannelMarker> AudioChannel<T> {
pub fn new(channel_id: ChannelId) -> Self {
Self {
channel_id,
commands: RwLock::new(VecDeque::new()),
_marker: PhantomData,
}
}
pub fn id(&self) -> &ChannelId {
&self.channel_id
}
fn queue_command(&self, command: AudioCommand) {
self.commands.write().push_back(command);
}
pub fn play(&self, handle: Handle<GodotResource>) -> PlayAudioCommand<'_, T> {
PlayAudioCommand::new(
self.channel_id,
handle,
AudioPlayerType::NonPositional,
self,
)
}
pub fn play_2d(
&self,
handle: Handle<GodotResource>,
position: Vec2,
) -> PlayAudioCommand<'_, T> {
PlayAudioCommand::new(
self.channel_id,
handle,
AudioPlayerType::Spatial2D { position },
self,
)
}
pub fn play_3d(
&self,
handle: Handle<GodotResource>,
position: Vec3,
) -> PlayAudioCommand<'_, T> {
PlayAudioCommand::new(
self.channel_id,
handle,
AudioPlayerType::Spatial3D { position },
self,
)
}
pub fn stop(&self) {
self.queue_command(AudioCommand::Stop(self.channel_id, None));
}
pub fn stop_with_fade(&self, fade_out: AudioTween) {
self.queue_command(AudioCommand::Stop(self.channel_id, Some(fade_out)));
}
pub fn pause(&self) {
self.queue_command(AudioCommand::Pause(self.channel_id, None));
}
pub fn resume(&self) {
self.queue_command(AudioCommand::Resume(self.channel_id, None));
}
pub fn set_volume(&self, volume: f32) {
self.queue_command(AudioCommand::SetVolume(
self.channel_id,
volume.clamp(0.0, 1.0),
None,
));
}
pub fn set_volume_with_fade(&self, volume: f32, tween: AudioTween) {
self.queue_command(AudioCommand::SetVolume(
self.channel_id,
volume.clamp(0.0, 1.0),
Some(tween),
));
}
pub fn set_pitch(&self, pitch: f32) {
self.queue_command(AudioCommand::SetPitch(
self.channel_id,
pitch.clamp(0.1, 4.0),
None,
));
}
pub fn set_panning(&self, panning: f32) {
self.queue_command(AudioCommand::SetPanning(
self.channel_id,
panning.clamp(-1.0, 1.0),
None,
));
}
}
pub struct PlayAudioCommand<'a, T: AudioChannelMarker> {
channel_id: ChannelId,
handle: Handle<GodotResource>,
player_type: AudioPlayerType,
settings: AudioSettings,
sound_id: SoundId,
channel: &'a AudioChannel<T>,
}
impl<'a, T: AudioChannelMarker> PlayAudioCommand<'a, T> {
pub(crate) fn new(
channel_id: ChannelId,
handle: Handle<GodotResource>,
player_type: AudioPlayerType,
channel: &'a AudioChannel<T>,
) -> Self {
let sound_id = SoundId::next();
Self {
channel_id,
handle,
player_type,
settings: AudioSettings::default(),
sound_id,
channel,
}
}
pub fn volume(mut self, volume: f32) -> Self {
self.settings.volume = volume.clamp(0.0, 1.0);
self
}
pub fn pitch(mut self, pitch: f32) -> Self {
self.settings.pitch = pitch.clamp(0.1, 4.0);
self
}
pub fn looped(mut self) -> Self {
self.settings.looping = true;
self
}
pub fn fade_in(mut self, duration: std::time::Duration) -> Self {
self.settings.fade_in = Some(AudioTween::linear(duration));
self
}
pub fn fade_in_with_easing(mut self, tween: AudioTween) -> Self {
self.settings.fade_in = Some(tween);
self
}
pub fn start_from(mut self, position: f32) -> Self {
self.settings.start_position = position.max(0.0);
self
}
pub fn panning(mut self, panning: f32) -> Self {
self.settings.panning = Some(panning.clamp(-1.0, 1.0));
self
}
}
impl<T: AudioChannelMarker> Drop for PlayAudioCommand<'_, T> {
fn drop(&mut self) {
let command = AudioCommand::Play(PlayCommand {
channel_id: self.channel_id,
handle: self.handle.clone(),
player_type: self.player_type.clone(),
settings: self.settings.clone(),
sound_id: self.sound_id,
});
self.channel.queue_command(command);
}
}
#[derive(Resource)]
pub struct MainAudioTrack;
impl AudioChannelMarker for MainAudioTrack {
const CHANNEL_NAME: &'static str = "main";
}
pub mod validation {
pub mod bounds {
pub const VOLUME_MIN: f32 = 0.0;
pub const VOLUME_MAX: f32 = 1.0;
pub const PITCH_MIN: f32 = 0.1;
pub const PITCH_MAX: f32 = 4.0;
pub const PANNING_MIN: f32 = -1.0;
pub const PANNING_MAX: f32 = 1.0;
}
pub fn clamp_volume(volume: f32) -> f32 {
volume.clamp(bounds::VOLUME_MIN, bounds::VOLUME_MAX)
}
pub fn clamp_pitch(pitch: f32) -> f32 {
pitch.clamp(bounds::PITCH_MIN, bounds::PITCH_MAX)
}
pub fn clamp_panning(panning: f32) -> f32 {
panning.clamp(bounds::PANNING_MIN, bounds::PANNING_MAX)
}
pub fn is_valid_volume(volume: f32) -> bool {
volume.is_finite() && (bounds::VOLUME_MIN..=bounds::VOLUME_MAX).contains(&volume)
}
pub fn is_valid_pitch(pitch: f32) -> bool {
pitch.is_finite() && (bounds::PITCH_MIN..=bounds::PITCH_MAX).contains(&pitch)
}
pub fn is_valid_panning(panning: f32) -> bool {
panning.is_finite() && (bounds::PANNING_MIN..=bounds::PANNING_MAX).contains(&panning)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clamp_volume() {
assert_eq!(clamp_volume(-0.5), 0.0);
assert_eq!(clamp_volume(0.5), 0.5);
assert_eq!(clamp_volume(1.5), 1.0);
assert_eq!(clamp_volume(0.0), 0.0);
assert_eq!(clamp_volume(1.0), 1.0);
}
#[test]
fn test_clamp_pitch() {
assert_eq!(clamp_pitch(0.05), 0.1);
assert_eq!(clamp_pitch(2.0), 2.0);
assert_eq!(clamp_pitch(5.0), 4.0);
assert_eq!(clamp_pitch(0.1), 0.1);
assert_eq!(clamp_pitch(4.0), 4.0);
}
#[test]
fn test_clamp_panning() {
assert_eq!(clamp_panning(-2.0), -1.0);
assert_eq!(clamp_panning(0.0), 0.0);
assert_eq!(clamp_panning(2.0), 1.0);
assert_eq!(clamp_panning(-1.0), -1.0);
assert_eq!(clamp_panning(1.0), 1.0);
}
#[test]
fn test_validation_functions() {
assert!(is_valid_volume(0.5));
assert!(!is_valid_volume(-0.1));
assert!(!is_valid_volume(1.1));
assert!(!is_valid_volume(f32::NAN));
assert!(is_valid_pitch(2.0));
assert!(!is_valid_pitch(0.05));
assert!(!is_valid_pitch(5.0));
assert!(is_valid_panning(0.0));
assert!(!is_valid_panning(-1.5));
assert!(!is_valid_panning(1.5));
}
}
}