use bevy::app::{App, Plugin, Update};
use bevy::asset::{Assets, Handle};
use bevy::ecs::system::ResMut;
use bevy::prelude::*;
use godot::classes::{AudioStream, AudioStreamPlayer};
use godot::obj::NewAlloc;
use std::collections::HashMap;
use thiserror::Error;
use super::assets::GodotResource;
use super::core::SceneTreeRef;
use crate::bridge::GodotNodeHandle;
pub struct GodotAudioPlugin;
impl Plugin for GodotAudioPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<AudioManager>().add_systems(
Update,
(process_sound_queue, cleanup_finished_sounds).chain(),
);
}
}
#[derive(Resource, Default)]
pub struct AudioManager {
playing_sounds: HashMap<SoundId, GodotNodeHandle>,
next_id: u32,
sound_queue: Vec<QueuedSound>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SoundId(u32);
#[derive(Debug)]
struct QueuedSound {
id: SoundId,
handle: Handle<GodotResource>,
settings: SoundSettings,
}
#[derive(Debug, Clone)]
pub struct SoundSettings {
pub volume: f32,
pub pitch: f32,
pub looping: bool,
}
impl Default for SoundSettings {
fn default() -> Self {
Self {
volume: 1.0,
pitch: 1.0,
looping: false,
}
}
}
impl SoundSettings {
pub fn new() -> Self {
Self::default()
}
pub fn volume(mut self, volume: f32) -> Self {
self.volume = volume.clamp(0.0, 1.0);
self
}
pub fn pitch(mut self, pitch: f32) -> Self {
self.pitch = pitch.clamp(0.1, 4.0);
self
}
pub fn looped(mut self) -> Self {
self.looping = true;
self
}
}
impl AudioManager {
pub fn play(&mut self, handle: Handle<GodotResource>) -> SoundId {
self.play_with_settings(handle, SoundSettings::default())
}
pub fn play_with_settings(
&mut self,
handle: Handle<GodotResource>,
settings: SoundSettings,
) -> SoundId {
let id = SoundId(self.next_id);
self.next_id += 1;
self.sound_queue.push(QueuedSound {
id,
handle,
settings,
});
id
}
pub fn stop(&mut self, id: SoundId) -> Result<(), AudioError> {
if let Some(mut handle) = self.playing_sounds.remove(&id) {
if let Some(mut player) = handle.try_get::<AudioStreamPlayer>() {
player.stop();
}
Ok(())
} else {
Err(AudioError::SoundNotFound(id))
}
}
pub fn stop_all(&mut self) {
for (_, mut handle) in self.playing_sounds.drain() {
if let Some(mut player) = handle.try_get::<AudioStreamPlayer>() {
player.stop();
}
}
}
pub fn is_playing(&mut self, id: SoundId) -> bool {
if let Some(handle) = self.playing_sounds.get_mut(&id) {
if let Some(player) = handle.try_get::<AudioStreamPlayer>() {
return player.is_playing();
}
}
false
}
pub fn playing_count(&self) -> usize {
self.playing_sounds.len()
}
pub fn stats(&self) -> (usize, usize) {
(self.sound_queue.len(), self.playing_sounds.len())
}
}
fn process_sound_queue(
mut audio_manager: ResMut<AudioManager>,
mut assets: ResMut<Assets<GodotResource>>,
mut scene_tree: SceneTreeRef,
) {
let queued_sounds = std::mem::take(&mut audio_manager.sound_queue);
for queued in queued_sounds {
let audio_stream = if let Some(asset) = assets.get_mut(&queued.handle) {
asset.try_cast::<AudioStream>()
} else {
audio_manager.sound_queue.push(queued);
continue;
};
if let Some(mut audio_stream) = audio_stream {
if queued.settings.looping {
if let Ok(mut ogg_stream) = audio_stream
.clone()
.try_cast::<godot::classes::AudioStreamOggVorbis>()
{
ogg_stream.set_loop(true);
audio_stream = ogg_stream.upcast();
} else if let Ok(mut wav_stream) = audio_stream
.clone()
.try_cast::<godot::classes::AudioStreamWav>()
{
wav_stream.set_loop_mode(godot::classes::audio_stream_wav::LoopMode::FORWARD);
audio_stream = wav_stream.upcast();
} else {
warn!(
"Audio stream type doesn't support runtime loop configuration for asset: {:?}",
queued.handle
);
}
}
let mut player = AudioStreamPlayer::new_alloc();
player.set_stream(&audio_stream);
player.set_volume_db(volume_to_db(queued.settings.volume));
player.set_pitch_scale(queued.settings.pitch);
if let Some(mut root) = scene_tree.get().get_root() {
root.add_child(&player);
}
player.play();
let handle = GodotNodeHandle::new(player);
audio_manager.playing_sounds.insert(queued.id, handle);
trace!("Started playing audio: {:?}", queued.id);
} else {
warn!(
"Failed to get audio stream for queued sound: {:?}",
queued.handle
);
}
}
}
fn cleanup_finished_sounds(mut audio_manager: ResMut<AudioManager>) {
let mut finished_sounds = Vec::new();
for (&sound_id, handle) in audio_manager.playing_sounds.iter_mut() {
if let Some(player) = handle.try_get::<AudioStreamPlayer>() {
if !player.is_playing() {
finished_sounds.push(sound_id);
}
} else {
finished_sounds.push(sound_id);
}
}
for sound_id in finished_sounds {
audio_manager.playing_sounds.remove(&sound_id);
trace!("Cleaned up finished sound: {:?}", sound_id);
}
}
fn volume_to_db(volume: f32) -> f32 {
if volume <= 0.0 {
-80.0 } else {
20.0 * volume.log10()
}
}
#[derive(Debug, Error)]
pub enum AudioError {
#[error("Sound not found: {0:?}")]
SoundNotFound(SoundId),
}
pub trait AudioManagerExt {
fn play_sound(&mut self, handle: Handle<GodotResource>) -> SoundId;
fn play_sound_with_volume(&mut self, handle: Handle<GodotResource>, volume: f32) -> SoundId;
fn play_looping_sound(&mut self, handle: Handle<GodotResource>) -> SoundId;
}
impl AudioManagerExt for AudioManager {
fn play_sound(&mut self, handle: Handle<GodotResource>) -> SoundId {
self.play(handle)
}
fn play_sound_with_volume(&mut self, handle: Handle<GodotResource>, volume: f32) -> SoundId {
self.play_with_settings(handle, SoundSettings::new().volume(volume))
}
fn play_looping_sound(&mut self, handle: Handle<GodotResource>) -> SoundId {
self.play_with_settings(handle, SoundSettings::new().looped())
}
}
pub use AudioManager as GodotAudio;