Skip to main content

godot_bevy/plugins/audio/
plugin.rs

1//! Main audio plugin and systems
2use 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/// Plugin that provides a comprehensive audio API using Godot's audio system.
26/// Supports 2D, 3D, and non-positional audio with channels, tweening, and spatial features.
27#[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/// Main audio manager for playing sounds and music across different channels.
53#[derive(Resource, Default)]
54pub struct GodotAudioChannels {
55    pub(crate) channels: HashMap<ChannelId, ChannelState>,
56    pub(crate) command_queue: VecDeque<AudioCommand>,
57}
58
59/// Extension trait for App to add audio channels with automatic system registration
60pub 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        // Auto-register a dedicated system for this channel type
69        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        // Initialize channel state in the global manager
77        self.world_mut()
78            .resource_mut::<GodotAudioChannels>()
79            .channels
80            .insert(channel_id, ChannelState::default());
81
82        self
83    }
84}
85
86/// Dedicated system for processing commands from a specific channel type
87fn process_channel_commands<T: AudioChannelMarker>(
88    channel: Res<AudioChannel<T>>,
89    mut audio_channels: ResMut<GodotAudioChannels>,
90) {
91    // Collect commands into the global queue so a single system can apply Godot calls.
92    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
105/// System that applies queued audio commands using Godot APIs.
106fn 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                    // Asset not ready, re-queue for next frame
130                    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; // Stop processing this frame to avoid infinite retry loop
135                }
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                    // Implement fade-out tweening with real current volumes
142                    for sound_id in sound_ids {
143                        // Get the actual current volume instead of assuming 1.0
144                        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                    // Immediate stop
159                    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                // TODO: Implement panning for individual sounds
201                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    // First pass: update tweens and collect parameter changes
222    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 this was a fade-out, mark sound for removal
238            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
349/// Process a play command and return the sound ID if successful
350fn 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        // Asset not ready yet, re-queue for next frame
361        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    // Configure looping if requested
371    let audio_stream = configure_looping(audio_stream, play_cmd.settings.looping);
372
373    // Check if fade-in is needed
374    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    // Create settings with initial volume for fade-in
381    let mut initial_settings = play_cmd.settings.clone();
382    initial_settings.volume = initial_volume;
383
384    // Create appropriate player based on type
385    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            // Get the node from the handle and add it to the scene tree
398            let node = godot.get::<godot::classes::Node>(handle);
399            root.add_child(&node);
400        }
401
402        // Now that the node is in the scene tree, start playback
403        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        // Track initial volume (either fade-in start volume or target volume)
411        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        // Set up fade-in tween if needed
421        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        // Convert from -1.0..1.0 to 0.0..1.0 for Godot
448        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    // Don't play yet - need to add to scene tree first
454    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    // Don't play yet - need to add to scene tree first
471    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    // Don't play yet - need to add to scene tree first
490    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    // Try to enable looping on different stream types
504    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    // Try each player type and start playback
524    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
533/// Convert linear volume (0.0-1.0) to decibels for Godot
534fn volume_to_db(volume: f32) -> f32 {
535    if volume <= 0.0 {
536        -80.0 // Silence
537    } else {
538        20.0 * volume.log10()
539    }
540}
541
542/// Simplified GodotAudioChannels - most functionality moved to per-channel systems
543impl GodotAudioChannels {
544    /// Get stats about the audio system
545    pub fn stats(&self) -> (usize, usize) {
546        (self.command_queue.len(), self.channels.len())
547    }
548}
549
550/// Possible errors that can be produced by the audio system
551#[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}