use crate::interop::{GodotAccess, GodotNodeHandle};
use crate::plugins::audio::{AudioTween, ChannelId};
use bevy_ecs::prelude::Resource;
use godot::classes::{AudioStreamPlayer, AudioStreamPlayer2D, AudioStreamPlayer3D, Node};
use godot::obj::Gd;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use tracing::trace;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SoundId(pub(crate) u32);
impl SoundId {
pub(crate) fn next() -> Self {
static NEXT_ID: AtomicU32 = AtomicU32::new(0);
Self(NEXT_ID.fetch_add(1, Ordering::Relaxed))
}
}
#[derive(Resource, Default)]
pub struct AudioOutput {
pub(crate) playing_sounds: HashMap<SoundId, GodotNodeHandle>,
pub(crate) sound_to_channel: HashMap<SoundId, ChannelId>,
pub(crate) current_volumes: HashMap<SoundId, f32>,
pub(crate) active_tweens: HashMap<SoundId, ActiveTween>,
}
#[derive(Debug, Clone)]
pub struct ActiveTween {
pub tween_type: TweenType,
pub start_value: f32,
pub target_value: f32,
pub duration: Duration,
pub elapsed: Duration,
pub easing: super::AudioEasing,
}
#[derive(Debug, Clone)]
pub enum TweenType {
Volume,
Pitch,
FadeOut, }
impl AudioOutput {
pub fn playing_count(&self) -> usize {
self.playing_sounds.len()
}
pub fn is_playing(&self, sound_id: SoundId) -> bool {
self.playing_sounds.contains_key(&sound_id)
}
pub fn sound_channel(&self, sound_id: SoundId) -> Option<ChannelId> {
self.sound_to_channel.get(&sound_id).copied()
}
pub fn set_sound_volume(&mut self, sound_id: SoundId, volume: f32, godot: &mut GodotAccess) {
let clamped_volume = volume.clamp(0.0, 1.0);
if let Some(handle) = self.playing_sounds.get(&sound_id).copied() {
set_audio_player_volume(godot, handle, clamped_volume);
self.current_volumes.insert(sound_id, clamped_volume);
trace!("Set volume to {} for sound: {:?}", clamped_volume, sound_id);
}
}
pub fn set_sound_pitch(&mut self, sound_id: SoundId, pitch: f32, godot: &mut GodotAccess) {
if let Some(handle) = self.playing_sounds.get(&sound_id).copied() {
set_audio_player_pitch(godot, handle, pitch.clamp(0.1, 4.0));
trace!("Set pitch to {} for sound: {:?}", pitch, sound_id);
}
}
pub fn pause_sound(&mut self, sound_id: SoundId, godot: &mut GodotAccess) {
if let Some(handle) = self.playing_sounds.get(&sound_id).copied() {
pause_audio_player(godot, handle);
trace!("Paused sound: {:?}", sound_id);
}
}
pub fn resume_sound(&mut self, sound_id: SoundId, godot: &mut GodotAccess) {
if let Some(handle) = self.playing_sounds.get(&sound_id).copied() {
resume_audio_player(godot, handle);
trace!("Resumed sound: {:?}", sound_id);
}
}
pub fn stop_sound(&mut self, sound_id: SoundId, godot: &mut GodotAccess) {
if let Some(handle) = self.playing_sounds.remove(&sound_id) {
stop_and_free_audio_player(godot, handle);
self.sound_to_channel.remove(&sound_id);
self.current_volumes.remove(&sound_id); trace!("Stopped sound: {:?}", sound_id);
}
}
}
pub(crate) enum AudioPlayer {
Stream(Gd<AudioStreamPlayer>),
Stream2D(Gd<AudioStreamPlayer2D>),
Stream3D(Gd<AudioStreamPlayer3D>),
}
impl AudioPlayer {
pub(crate) fn set_volume_db(&mut self, volume_db: f32) {
match self {
AudioPlayer::Stream(player) => player.set_volume_db(volume_db),
AudioPlayer::Stream2D(player) => player.set_volume_db(volume_db),
AudioPlayer::Stream3D(player) => player.set_volume_db(volume_db),
}
}
pub(crate) fn set_pitch_scale(&mut self, pitch: f32) {
match self {
AudioPlayer::Stream(player) => player.set_pitch_scale(pitch),
AudioPlayer::Stream2D(player) => player.set_pitch_scale(pitch),
AudioPlayer::Stream3D(player) => player.set_pitch_scale(pitch),
}
}
pub(crate) fn set_stream_paused(&mut self, paused: bool) {
match self {
AudioPlayer::Stream(player) => player.set_stream_paused(paused),
AudioPlayer::Stream2D(player) => player.set_stream_paused(paused),
AudioPlayer::Stream3D(player) => player.set_stream_paused(paused),
}
}
pub(crate) fn stop(&mut self) {
match self {
AudioPlayer::Stream(player) => player.stop(),
AudioPlayer::Stream2D(player) => player.stop(),
AudioPlayer::Stream3D(player) => player.stop(),
}
}
pub(crate) fn is_playing(&mut self) -> bool {
match self {
AudioPlayer::Stream(player) => player.is_playing(),
AudioPlayer::Stream2D(player) => player.is_playing(),
AudioPlayer::Stream3D(player) => player.is_playing(),
}
}
pub(crate) fn into_node(self) -> Gd<Node> {
match self {
AudioPlayer::Stream(player) => player.upcast(),
AudioPlayer::Stream2D(player) => player.upcast(),
AudioPlayer::Stream3D(player) => player.upcast(),
}
}
}
pub(crate) fn try_get_audio_player(
godot: &mut GodotAccess,
handle: GodotNodeHandle,
) -> Option<AudioPlayer> {
if let Some(player) = godot.try_get::<AudioStreamPlayer>(handle) {
Some(AudioPlayer::Stream(player))
} else if let Some(player) = godot.try_get::<AudioStreamPlayer2D>(handle) {
Some(AudioPlayer::Stream2D(player))
} else {
godot
.try_get::<AudioStreamPlayer3D>(handle)
.map(AudioPlayer::Stream3D)
}
}
fn volume_to_db(volume: f32) -> f32 {
if volume <= 0.0 {
-80.0 } else {
20.0 * volume.log10()
}
}
fn set_audio_player_volume(godot: &mut GodotAccess, handle: GodotNodeHandle, volume: f32) {
let volume_db = volume_to_db(volume);
if let Some(mut player) = try_get_audio_player(godot, handle) {
player.set_volume_db(volume_db);
}
}
fn set_audio_player_pitch(godot: &mut GodotAccess, handle: GodotNodeHandle, pitch: f32) {
if let Some(mut player) = try_get_audio_player(godot, handle) {
player.set_pitch_scale(pitch);
}
}
fn pause_audio_player(godot: &mut GodotAccess, handle: GodotNodeHandle) {
if let Some(mut player) = try_get_audio_player(godot, handle) {
player.set_stream_paused(true);
}
}
fn resume_audio_player(godot: &mut GodotAccess, handle: GodotNodeHandle) {
if let Some(mut player) = try_get_audio_player(godot, handle) {
player.set_stream_paused(false);
}
}
pub(crate) fn stop_and_free_audio_player(godot: &mut GodotAccess, handle: GodotNodeHandle) {
let Some(mut player) = try_get_audio_player(godot, handle) else {
return;
};
player.stop();
let mut node = player.into_node();
if let Some(mut parent) = node.get_parent() {
parent.remove_child(&node);
}
node.queue_free();
trace!("Removed and freed audio node from scene tree");
}
impl ActiveTween {
pub fn new_fade_in(target_volume: f32, tween: AudioTween) -> Self {
Self {
tween_type: TweenType::Volume,
start_value: 0.0,
target_value: target_volume,
duration: tween.duration,
elapsed: Duration::ZERO,
easing: tween.easing,
}
}
pub fn new_fade_out(current_volume: f32, tween: AudioTween) -> Self {
Self {
tween_type: TweenType::FadeOut,
start_value: current_volume,
target_value: 0.0,
duration: tween.duration,
elapsed: Duration::ZERO,
easing: tween.easing,
}
}
pub fn new_volume(start: f32, target: f32, tween: AudioTween) -> Self {
Self {
tween_type: TweenType::Volume,
start_value: start,
target_value: target,
duration: tween.duration,
elapsed: Duration::ZERO,
easing: tween.easing,
}
}
pub fn new_pitch(start: f32, target: f32, tween: AudioTween) -> Self {
Self {
tween_type: TweenType::Pitch,
start_value: start,
target_value: target,
duration: tween.duration,
elapsed: Duration::ZERO,
easing: tween.easing,
}
}
pub fn update(&mut self, delta: Duration) -> f32 {
self.elapsed += delta;
if self.duration.as_secs_f32() == 0.0 {
return self.target_value;
}
let progress = (self.elapsed.as_secs_f32() / self.duration.as_secs_f32()).clamp(0.0, 1.0);
let eased_progress = match self.easing {
super::AudioEasing::Linear => progress,
super::AudioEasing::EaseIn => progress * progress,
super::AudioEasing::EaseOut => 1.0 - (1.0 - progress) * (1.0 - progress),
super::AudioEasing::EaseInOut => {
if progress < 0.5 {
2.0 * progress * progress
} else {
1.0 - 2.0 * (1.0 - progress) * (1.0 - progress)
}
}
};
self.start_value + (self.target_value - self.start_value) * eased_progress
}
pub fn is_complete(&self) -> bool {
self.elapsed >= self.duration
}
}