1use crate::interop::{GodotAccess, GodotNodeHandle};
3use crate::plugins::assets::GodotResource;
4use crate::plugins::audio::output::{
5 AudioPlayer, stop_and_free_audio_player, try_get_audio_player,
6};
7use crate::plugins::audio::{
8 ActiveTween, AudioChannel, AudioChannelMarker, AudioCommand, AudioOutput, AudioPlayerType,
9 AudioSettings, ChannelId, ChannelState, MainAudioTrack, PlayCommand, SoundId, TweenType,
10};
11use crate::plugins::scene_tree::SceneTreeRef;
12use bevy_app::{App, Plugin, Update};
13use bevy_asset::Assets;
14use bevy_ecs::prelude::Resource;
15use bevy_ecs::schedule::{IntoScheduleConfigs, SystemSet};
16use bevy_ecs::system::{Res, ResMut};
17use bevy_math::{Vec2, Vec3};
18use bevy_time::Time;
19use godot::classes::{AudioStream, AudioStreamPlayer, AudioStreamPlayer2D, AudioStreamPlayer3D};
20use godot::obj::NewAlloc;
21use std::collections::{HashMap, VecDeque};
22use thiserror::Error;
23use tracing::{trace, warn};
24
25#[derive(Default)]
28pub struct GodotAudioPlugin;
29
30#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
31enum AudioSystemSet {
32 CollectCommands,
33 ProcessCommands,
34}
35
36impl Plugin for GodotAudioPlugin {
37 fn build(&self, app: &mut App) {
38 app.init_resource::<GodotAudioChannels>()
39 .init_resource::<AudioOutput>()
40 .add_audio_channel::<MainAudioTrack>()
41 .configure_sets(
42 Update,
43 AudioSystemSet::ProcessCommands.after(AudioSystemSet::CollectCommands),
44 )
45 .add_systems(
46 Update,
47 audio_main_thread.in_set(AudioSystemSet::ProcessCommands),
48 );
49 }
50}
51
52#[derive(Resource, Default)]
54pub struct GodotAudioChannels {
55 pub(crate) channels: HashMap<ChannelId, ChannelState>,
56 pub(crate) command_queue: VecDeque<AudioCommand>,
57}
58
59pub trait AudioApp {
61 fn add_audio_channel<T: AudioChannelMarker>(&mut self) -> &mut Self;
62}
63
64impl AudioApp for App {
65 fn add_audio_channel<T: AudioChannelMarker>(&mut self) -> &mut Self {
66 let channel_id = ChannelId(T::CHANNEL_NAME);
67
68 self.add_systems(
70 Update,
71 process_channel_commands::<T>.in_set(AudioSystemSet::CollectCommands),
72 );
73
74 self.insert_resource(AudioChannel::<T>::new(channel_id));
75
76 self.world_mut()
78 .resource_mut::<GodotAudioChannels>()
79 .channels
80 .insert(channel_id, ChannelState::default());
81
82 self
83 }
84}
85
86fn process_channel_commands<T: AudioChannelMarker>(
88 channel: Res<AudioChannel<T>>,
89 mut audio_channels: ResMut<GodotAudioChannels>,
90) {
91 let mut commands = channel.commands.write();
93 while let Some(command) = commands.pop_front() {
94 audio_channels.command_queue.push_back(command);
95 }
96}
97
98#[derive(Default)]
99struct PendingSoundOps {
100 volume: Option<f32>,
101 pitch: Option<f32>,
102 paused: Option<bool>,
103}
104
105fn audio_main_thread(
107 mut audio_channels: ResMut<GodotAudioChannels>,
108 mut audio_output: ResMut<AudioOutput>,
109 mut assets: ResMut<Assets<GodotResource>>,
110 mut scene_tree: SceneTreeRef,
111 time: Res<Time>,
112 mut godot: GodotAccess,
113) {
114 let mut pending_ops: HashMap<SoundId, PendingSoundOps> = HashMap::new();
115 let mut pending_stops: Vec<(SoundId, GodotNodeHandle)> = Vec::new();
116
117 while let Some(command) = audio_channels.command_queue.pop_front() {
118 match command {
119 AudioCommand::Play(play_cmd) => {
120 if process_play_command(
121 &play_cmd,
122 &mut assets,
123 &mut scene_tree,
124 &mut audio_output,
125 &mut godot,
126 )
127 .is_none()
128 {
129 audio_channels
131 .command_queue
132 .push_front(AudioCommand::Play(play_cmd));
133 warn!("Audio asset not ready, re-queued for next frame");
134 break; }
136 }
137 AudioCommand::Stop(channel_id, tween) => {
138 let sound_ids = collect_channel_sound_ids(&audio_output, channel_id);
139
140 if let Some(tween) = tween {
141 for sound_id in sound_ids {
143 let current_volume = audio_output
145 .current_volumes
146 .get(&sound_id)
147 .copied()
148 .unwrap_or(1.0);
149 let fade_out_tween =
150 ActiveTween::new_fade_out(current_volume, tween.clone());
151 audio_output.active_tweens.insert(sound_id, fade_out_tween);
152 trace!(
153 "Started fade-out from volume {} for sound: {:?}",
154 current_volume, sound_id
155 );
156 }
157 } else {
158 for sound_id in sound_ids {
160 schedule_stop_sound(
161 &mut audio_output,
162 &mut pending_ops,
163 &mut pending_stops,
164 sound_id,
165 );
166 }
167 }
168 trace!("Processed stop command for channel: {:?}", channel_id);
169 }
170 AudioCommand::Pause(channel_id, _tween) => {
171 let sound_ids = collect_channel_sound_ids(&audio_output, channel_id);
172 for sound_id in sound_ids {
173 pending_ops.entry(sound_id).or_default().paused = Some(true);
174 }
175 trace!("Paused channel: {:?}", channel_id);
176 }
177 AudioCommand::Resume(channel_id, _tween) => {
178 let sound_ids = collect_channel_sound_ids(&audio_output, channel_id);
179 for sound_id in sound_ids {
180 pending_ops.entry(sound_id).or_default().paused = Some(false);
181 }
182 trace!("Resumed channel: {:?}", channel_id);
183 }
184 AudioCommand::SetVolume(channel_id, volume, _tween) => {
185 let sound_ids = collect_channel_sound_ids(&audio_output, channel_id);
186 for sound_id in sound_ids {
187 audio_output.current_volumes.insert(sound_id, volume);
188 pending_ops.entry(sound_id).or_default().volume = Some(volume);
189 }
190 trace!("Set volume to {} for channel: {:?}", volume, channel_id);
191 }
192 AudioCommand::SetPitch(channel_id, pitch, _tween) => {
193 let sound_ids = collect_channel_sound_ids(&audio_output, channel_id);
194 for sound_id in sound_ids {
195 pending_ops.entry(sound_id).or_default().pitch = Some(pitch);
196 }
197 trace!("Set pitch to {} for channel: {:?}", pitch, channel_id);
198 }
199 AudioCommand::SetPanning(_channel_id, _panning, _tween) => {
200 warn!("Panning not yet implemented for individual sounds");
202 }
203 AudioCommand::StopSound(sound_id, _tween) => {
204 schedule_stop_sound(
205 &mut audio_output,
206 &mut pending_ops,
207 &mut pending_stops,
208 sound_id,
209 );
210 trace!("Stopped sound: {:?}", sound_id);
211 }
212 }
213 }
214
215 let delta = time.delta();
216 let mut completed_tweens = Vec::new();
217 let mut sounds_to_stop = Vec::new();
218 let mut volume_updates = Vec::new();
219 let mut pitch_updates = Vec::new();
220
221 for (&sound_id, tween) in audio_output.active_tweens.iter_mut() {
223 let current_value = tween.update(delta);
224
225 match tween.tween_type {
226 TweenType::Volume | TweenType::FadeOut => {
227 volume_updates.push((sound_id, current_value));
228 }
229 TweenType::Pitch => {
230 pitch_updates.push((sound_id, current_value));
231 }
232 }
233
234 if tween.is_complete() {
235 completed_tweens.push(sound_id);
236
237 if matches!(tween.tween_type, TweenType::FadeOut) {
239 sounds_to_stop.push(sound_id);
240 }
241 }
242 }
243
244 for (sound_id, volume) in volume_updates {
245 if audio_output.playing_sounds.contains_key(&sound_id) {
246 audio_output.current_volumes.insert(sound_id, volume);
247 pending_ops.entry(sound_id).or_default().volume = Some(volume);
248 }
249 }
250
251 for (sound_id, pitch) in pitch_updates {
252 if audio_output.playing_sounds.contains_key(&sound_id) {
253 pending_ops.entry(sound_id).or_default().pitch = Some(pitch);
254 }
255 }
256
257 for sound_id in completed_tweens {
258 audio_output.active_tweens.remove(&sound_id);
259 trace!("Completed tween for sound: {:?}", sound_id);
260 }
261
262 for sound_id in sounds_to_stop {
263 schedule_stop_sound(
264 &mut audio_output,
265 &mut pending_ops,
266 &mut pending_stops,
267 sound_id,
268 );
269 trace!("Stopped sound after fade-out: {:?}", sound_id);
270 }
271
272 for (sound_id, handle) in pending_stops {
273 stop_and_free_audio_player(&mut godot, handle);
274 trace!("Stopped sound: {:?}", sound_id);
275 }
276
277 let playing_sounds: Vec<(SoundId, GodotNodeHandle)> = audio_output
278 .playing_sounds
279 .iter()
280 .map(|(sound_id, handle)| (*sound_id, *handle))
281 .collect();
282
283 let mut finished_sounds = Vec::new();
284 for (sound_id, handle) in playing_sounds {
285 let Some(mut player) = try_get_audio_player(&mut godot, handle) else {
286 finished_sounds.push(sound_id);
287 continue;
288 };
289
290 if let Some(ops) = pending_ops.get(&sound_id) {
291 apply_pending_ops(&mut player, ops);
292 }
293
294 let is_playing = player.is_playing();
295 if !is_playing {
296 let mut node = player.into_node();
297 if let Some(mut parent) = node.get_parent() {
298 parent.remove_child(&node);
299 }
300 node.queue_free();
301 finished_sounds.push(sound_id);
302 }
303 }
304
305 for sound_id in finished_sounds {
306 audio_output.playing_sounds.remove(&sound_id);
307 audio_output.sound_to_channel.remove(&sound_id);
308 audio_output.active_tweens.remove(&sound_id);
309 audio_output.current_volumes.remove(&sound_id);
310 trace!("Cleaned up finished sound: {:?}", sound_id);
311 }
312}
313
314fn collect_channel_sound_ids(output: &AudioOutput, channel_id: ChannelId) -> Vec<SoundId> {
315 output
316 .sound_to_channel
317 .iter()
318 .filter(|(_, ch)| **ch == channel_id)
319 .map(|(sound_id, _)| *sound_id)
320 .collect()
321}
322
323fn schedule_stop_sound(
324 output: &mut AudioOutput,
325 pending_ops: &mut HashMap<SoundId, PendingSoundOps>,
326 pending_stops: &mut Vec<(SoundId, GodotNodeHandle)>,
327 sound_id: SoundId,
328) {
329 if let Some(handle) = output.playing_sounds.remove(&sound_id) {
330 output.sound_to_channel.remove(&sound_id);
331 output.current_volumes.remove(&sound_id);
332 pending_ops.remove(&sound_id);
333 pending_stops.push((sound_id, handle));
334 }
335}
336
337fn apply_pending_ops(player: &mut AudioPlayer, ops: &PendingSoundOps) {
338 if let Some(volume) = ops.volume {
339 player.set_volume_db(volume_to_db(volume));
340 }
341 if let Some(pitch) = ops.pitch {
342 player.set_pitch_scale(pitch);
343 }
344 if let Some(paused) = ops.paused {
345 player.set_stream_paused(paused);
346 }
347}
348
349fn process_play_command(
351 play_cmd: &PlayCommand,
352 assets: &mut Assets<GodotResource>,
353 scene_tree: &mut SceneTreeRef,
354 output: &mut AudioOutput,
355 godot: &mut GodotAccess,
356) -> Option<SoundId> {
357 let audio_stream = if let Some(asset) = assets.get_mut(&play_cmd.handle) {
358 asset.try_cast::<AudioStream>()
359 } else {
360 warn!("Audio asset not ready: {:?}", play_cmd.handle);
362 return None;
363 };
364
365 let Some(audio_stream) = audio_stream else {
366 warn!("Failed to cast to AudioStream: {:?}", play_cmd.handle);
367 return None;
368 };
369
370 let audio_stream = configure_looping(audio_stream, play_cmd.settings.looping);
372
373 let (initial_volume, fade_in_tween) = if let Some(fade_in) = &play_cmd.settings.fade_in {
375 (0.0, Some((play_cmd.settings.volume, fade_in.clone())))
376 } else {
377 (play_cmd.settings.volume, None)
378 };
379
380 let mut initial_settings = play_cmd.settings.clone();
382 initial_settings.volume = initial_volume;
383
384 let player_handle = match &play_cmd.player_type {
386 AudioPlayerType::NonPositional => create_audio_player(audio_stream, &initial_settings),
387 AudioPlayerType::Spatial2D { position } => {
388 create_audio_player_2d(audio_stream, &initial_settings, *position)
389 }
390 AudioPlayerType::Spatial3D { position } => {
391 create_audio_player_3d(audio_stream, &initial_settings, *position)
392 }
393 };
394
395 if let Some(handle) = player_handle {
396 if let Some(mut root) = scene_tree.get().get_root() {
397 let node = godot.get::<godot::classes::Node>(handle);
399 root.add_child(&node);
400 }
401
402 start_audio_playback(godot, handle);
404
405 output.playing_sounds.insert(play_cmd.sound_id, handle);
406 output
407 .sound_to_channel
408 .insert(play_cmd.sound_id, play_cmd.channel_id);
409
410 let initial_volume = if fade_in_tween.is_some() {
412 0.0
413 } else {
414 initial_settings.volume
415 };
416 output
417 .current_volumes
418 .insert(play_cmd.sound_id, initial_volume);
419
420 if let Some((target_volume, fade_in)) = fade_in_tween {
422 let tween = ActiveTween::new_fade_in(target_volume, fade_in);
423 output.active_tweens.insert(play_cmd.sound_id, tween);
424 trace!("Started fade-in for sound: {:?}", play_cmd.sound_id);
425 }
426
427 trace!(
428 "Started playing audio: {:?} in channel: {:?}",
429 play_cmd.sound_id, play_cmd.channel_id
430 );
431 Some(play_cmd.sound_id)
432 } else {
433 None
434 }
435}
436
437fn create_audio_player(
438 audio_stream: godot::obj::Gd<AudioStream>,
439 settings: &AudioSettings,
440) -> Option<GodotNodeHandle> {
441 let mut player = AudioStreamPlayer::new_alloc();
442 player.set_stream(&audio_stream);
443 player.set_volume_db(volume_to_db(settings.volume));
444 player.set_pitch_scale(settings.pitch);
445
446 if let Some(panning) = settings.panning {
447 let _godot_panning = (panning + 1.0) / 2.0;
449 let bus_name: godot::builtin::StringName = "Master".into();
450 player.set_bus(&bus_name);
451 }
452
453 Some(GodotNodeHandle::new(
455 player.upcast::<godot::classes::Node>(),
456 ))
457}
458
459fn create_audio_player_2d(
460 audio_stream: godot::obj::Gd<AudioStream>,
461 settings: &AudioSettings,
462 position: Vec2,
463) -> Option<GodotNodeHandle> {
464 let mut player = AudioStreamPlayer2D::new_alloc();
465 player.set_stream(&audio_stream);
466 player.set_volume_db(volume_to_db(settings.volume));
467 player.set_pitch_scale(settings.pitch);
468 player.set_position(godot::prelude::Vector2::new(position.x, position.y));
469
470 Some(GodotNodeHandle::new(
472 player.upcast::<godot::classes::Node>(),
473 ))
474}
475
476fn create_audio_player_3d(
477 audio_stream: godot::obj::Gd<AudioStream>,
478 settings: &AudioSettings,
479 position: Vec3,
480) -> Option<GodotNodeHandle> {
481 let mut player = AudioStreamPlayer3D::new_alloc();
482 player.set_stream(&audio_stream);
483 player.set_volume_db(volume_to_db(settings.volume));
484 player.set_pitch_scale(settings.pitch);
485 player.set_position(godot::prelude::Vector3::new(
486 position.x, position.y, position.z,
487 ));
488
489 Some(GodotNodeHandle::new(
491 player.upcast::<godot::classes::Node>(),
492 ))
493}
494
495fn configure_looping(
496 audio_stream: godot::obj::Gd<AudioStream>,
497 looping: bool,
498) -> godot::obj::Gd<AudioStream> {
499 if !looping {
500 return audio_stream;
501 }
502
503 if let Ok(mut ogg_stream) = audio_stream
505 .clone()
506 .try_cast::<godot::classes::AudioStreamOggVorbis>()
507 {
508 ogg_stream.set_loop(true);
509 ogg_stream.upcast()
510 } else if let Ok(mut wav_stream) = audio_stream
511 .clone()
512 .try_cast::<godot::classes::AudioStreamWav>()
513 {
514 wav_stream.set_loop_mode(godot::classes::audio_stream_wav::LoopMode::FORWARD);
515 wav_stream.upcast()
516 } else {
517 warn!("Audio stream type doesn't support runtime loop configuration");
518 audio_stream
519 }
520}
521
522fn start_audio_playback(godot: &mut GodotAccess, handle: GodotNodeHandle) {
523 if let Some(mut player) = godot.try_get::<AudioStreamPlayer>(handle) {
525 player.play();
526 } else if let Some(mut player) = godot.try_get::<AudioStreamPlayer2D>(handle) {
527 player.play();
528 } else if let Some(mut player) = godot.try_get::<AudioStreamPlayer3D>(handle) {
529 player.play();
530 }
531}
532
533fn volume_to_db(volume: f32) -> f32 {
535 if volume <= 0.0 {
536 -80.0 } else {
538 20.0 * volume.log10()
539 }
540}
541
542impl GodotAudioChannels {
544 pub fn stats(&self) -> (usize, usize) {
546 (self.command_queue.len(), self.channels.len())
547 }
548}
549
550#[derive(Debug, Error)]
552pub enum AudioError {
553 #[error("Sound not found: {0:?}")]
554 SoundNotFound(SoundId),
555 #[error("Channel not found: {0:?}")]
556 ChannelNotFound(ChannelId),
557}