Skip to main content

goud_engine/assets/audio_manager/
mod.rs

1//! Audio manager resource for centralized audio playback control.
2//!
3//! The `AudioManager` is an ECS resource that provides a high-level interface
4//! for playing, pausing, stopping, and controlling audio in the game engine.
5//! It wraps the rodio audio backend and provides a clean API for audio operations.
6//!
7//! # Features
8//!
9//! - Play audio from loaded assets
10//! - Control global volume
11//! - Play, pause, stop, resume audio
12//! - Check playback state
13//! - Thread-safe for use in parallel systems
14//!
15//! # Usage
16//!
17//! ```no_run
18//! use goud_engine::ecs::{World, Resource};
19//! use goud_engine::assets::AudioManager;
20//!
21//! let mut world = World::new();
22//! let audio_manager = AudioManager::new().expect("Failed to initialize audio");
23//! world.insert_resource(audio_manager);
24//!
25//! // In a system:
26//! // fn play_sound_system(audio: ResMut<AudioManager>, assets: Res<AssetServer>) {
27//! //     let sound_handle = assets.load("sounds/jump.wav");
28//! //     audio.play(sound_handle);
29//! // }
30//! ```
31
32pub(super) mod spatial;
33
34mod controls;
35mod playback;
36mod spatial_playback;
37#[cfg(test)]
38mod tests;
39
40use crate::core::error::{GoudError, GoudResult};
41use rodio::{DeviceSinkBuilder, MixerDeviceSink, Player};
42use std::collections::HashMap;
43use std::sync::{Arc, Mutex};
44
45/// Central audio playback manager resource.
46///
47/// `AudioManager` is an ECS resource that manages audio playback using the
48/// rodio audio library. It maintains the audio output stream and provides
49/// methods for playing, pausing, stopping, and controlling audio.
50///
51/// # Thread Safety
52///
53/// The AudioManager is thread-safe (`Send + Sync`) and can be used in parallel
54/// systems. Internal state is protected by a Mutex.
55///
56/// # Example
57///
58/// ```no_run
59/// use goud_engine::assets::AudioManager;
60///
61/// // Create audio manager
62/// let mut audio_manager = AudioManager::new().expect("Failed to initialize audio");
63///
64/// // Set global volume (0.0 to 1.0)
65/// audio_manager.set_global_volume(0.5);
66///
67/// // Get global volume
68/// assert_eq!(audio_manager.global_volume(), 0.5);
69/// ```
70pub struct AudioManager {
71    /// Audio device sink (must be kept alive for playback).
72    pub(super) device_sink: MixerDeviceSink,
73
74    /// Global volume (0.0 to 1.0).
75    pub(super) global_volume: Arc<Mutex<f32>>,
76
77    /// Active audio players (for controlling playback).
78    pub(super) players: Arc<Mutex<HashMap<u64, Player>>>,
79
80    /// Next player ID for tracking.
81    pub(super) next_player_id: Arc<Mutex<u64>>,
82}
83
84impl AudioManager {
85    /// Creates a new AudioManager instance.
86    ///
87    /// Initializes the audio output stream. Returns an error if audio
88    /// output is not available (e.g., no audio device found).
89    ///
90    /// # Errors
91    ///
92    /// Returns `AudioInitFailed` if the audio system cannot be initialized.
93    ///
94    /// # Example
95    ///
96    /// ```no_run
97    /// use goud_engine::assets::AudioManager;
98    ///
99    /// let audio_manager = AudioManager::new().expect("Failed to initialize audio");
100    /// ```
101    pub fn new() -> GoudResult<Self> {
102        let mut device_sink = DeviceSinkBuilder::open_default_sink().map_err(|e| {
103            GoudError::AudioInitFailed(format!("Failed to create audio output stream: {}", e))
104        })?;
105        // Suppress the log message rodio prints when the device sink is dropped.
106        device_sink.log_on_drop(false);
107
108        Ok(Self {
109            device_sink,
110            global_volume: Arc::new(Mutex::new(1.0)),
111            players: Arc::new(Mutex::new(HashMap::new())),
112            next_player_id: Arc::new(Mutex::new(0)),
113        })
114    }
115}
116
117// SAFETY: AudioManager is Send + Sync because all internal state is protected by Mutex.
118// MixerDeviceSink contains a cpal::Stream which is Send but not Sync by default.
119// We ensure thread safety by only accessing it through our Mutex-protected methods.
120unsafe impl Send for AudioManager {}
121unsafe impl Sync for AudioManager {}
122
123impl std::fmt::Debug for AudioManager {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        f.debug_struct("AudioManager")
126            .field("global_volume", &self.global_volume())
127            .field("active_players", &self.active_count())
128            .finish()
129    }
130}