Skip to main content

game_gem/
audio.rs

1//! Audio system (feature-gated behind `audio` feature).
2//!
3//! Provides:
4//! - **Spatial audio** — 2D positional sound with distance falloff
5//! - **Music & SFX channels** — separate volume controls
6//! - **Fade in/out** — smooth volume transitions
7//! - **Sound pooling** — avoid allocating duplicate sounds
8//!
9//! Built on `rodio` for cross-platform audio output.
10
11#![cfg_attr(not(feature = "audio"), allow(dead_code))]
12
13use crate::math::{Vec2, Vec2Ext, FloatExt};
14use std::collections::HashMap;
15
16// ─────────────────────────────────────────────
17// Sound handle
18// ─────────────────────────────────────────────
19
20/// A loaded sound effect.
21#[derive(Debug, Clone)]
22pub struct Sound {
23    /// Name identifier.
24    pub name: String,
25    /// Whether this is a music track (streamed) vs SFX (loaded into memory).
26    pub is_music: bool,
27    /// Base volume (0.0–1.0).
28    pub volume: f32,
29    /// Pitch multiplier.
30    pub pitch: f32,
31    /// Whether to loop.
32    pub looping: bool,
33    /// Minimum distance for spatial audio (within this, no attenuation).
34    pub spatial_min_distance: f32,
35    /// Maximum distance for spatial audio (beyond this, inaudible).
36    pub spatial_max_distance: f32,
37    /// 2D position for spatial audio.
38    pub position: Option<Vec2>,
39}
40
41impl Sound {
42    /// Create a new sound configuration.
43    pub fn new(name: &str) -> Self {
44        Self {
45            name: name.to_string(),
46            is_music: false,
47            volume: 1.0,
48            pitch: 1.0,
49            looping: false,
50            spatial_min_distance: 50.0,
51            spatial_max_distance: 500.0,
52            position: None,
53        }
54    }
55
56    /// Builder: set as music (streamed).
57    pub fn as_music(mut self) -> Self {
58        self.is_music = true;
59        self
60    }
61
62    /// Builder: set volume.
63    pub fn with_volume(mut self, volume: f32) -> Self {
64        self.volume = volume.clamp(0.0, 1.0);
65        self
66    }
67
68    /// Builder: set pitch.
69    pub fn with_pitch(mut self, pitch: f32) -> Self {
70        self.pitch = pitch.clamp(0.1, 10.0);
71        self
72    }
73
74    /// Builder: set looping.
75    pub fn with_looping(mut self, looping: bool) -> Self {
76        self.looping = looping;
77        self
78    }
79
80    /// Builder: set 2D position for spatial audio.
81    pub fn with_position(mut self, pos: Vec2) -> Self {
82        self.position = Some(pos);
83        self
84    }
85
86    /// Builder: set spatial distance range.
87    pub fn with_spatial_range(mut self, min: f32, max: f32) -> Self {
88        self.spatial_min_distance = min;
89        self.spatial_max_distance = max;
90        self
91    }
92}
93
94// ─────────────────────────────────────────────
95// Audio Manager
96// ─────────────────────────────────────────────
97
98/// Master volume channels.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100pub enum AudioChannel {
101    /// Sound effects channel.
102    Sfx,
103    /// Music channel.
104    Music,
105    /// UI sounds channel.
106    Ui,
107    /// Ambient / environment channel.
108    Ambient,
109}
110
111/// The audio manager controls playback, volume, and spatial audio.
112///
113/// # Example
114/// ```
115/// let mut audio = AudioManager::new();
116/// audio.load("jump", "sounds/jump.ogg");
117/// audio.load("bgm", "music/theme.ogg");
118///
119/// // Play a sound effect
120/// audio.play("jump");
121///
122/// // Play music with fade-in
123/// audio.play_music("bgm", Fade::In(2.0));
124///
125/// // Update spatial audio
126/// audio.update_listener_position(listener_pos);
127/// audio.update(dt);
128/// ```
129pub struct AudioManager {
130    /// Master volume (0.0–1.0).
131    master_volume: f32,
132    /// Per-channel volumes.
133    channel_volumes: HashMap<AudioChannel, f32>,
134    /// Loaded sounds.
135    sounds: HashMap<String, Sound>,
136    /// Currently playing instances.
137    instances: Vec<PlayingInstance>,
138    /// Listener position for spatial audio.
139    listener_position: Vec2,
140}
141
142/// A currently-playing sound instance.
143#[derive(Debug, Clone)]
144struct PlayingInstance {
145    sound_name: String,
146    channel: AudioChannel,
147    /// Current volume (after all multipliers).
148    current_volume: f32,
149    /// Fade state.
150    fade: Option<FadeState>,
151    /// Whether this instance is still alive.
152    alive: bool,
153}
154
155/// Fade in/out state.
156#[derive(Debug, Clone, Copy)]
157struct FadeState {
158    kind: FadeKind,
159    duration: f32,
160    elapsed: f32,
161    start_volume: f32,
162    target_volume: f32,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq)]
166enum FadeKind {
167    In,
168    Out,
169}
170
171/// Fade configuration for play commands.
172#[derive(Debug, Clone, Copy)]
173pub enum Fade {
174    /// Fade in over N seconds.
175    In(f32),
176    /// Fade out over N seconds, then stop.
177    Out(f32),
178    /// No fade.
179    None,
180}
181
182impl Default for Fade {
183    fn default() -> Self {
184        Fade::None
185    }
186}
187
188impl AudioManager {
189    /// Create a new audio manager.
190    pub fn new() -> Self {
191        let mut channel_volumes = HashMap::new();
192        channel_volumes.insert(AudioChannel::Sfx, 1.0);
193        channel_volumes.insert(AudioChannel::Music, 0.8);
194        channel_volumes.insert(AudioChannel::Ui, 0.7);
195        channel_volumes.insert(AudioChannel::Ambient, 0.6);
196
197        Self {
198            master_volume: 1.0,
199            channel_volumes,
200            sounds: HashMap::new(),
201            instances: Vec::new(),
202            listener_position: Vec2::ZERO,
203        }
204    }
205
206    /// Register a sound (in a real impl, this loads the audio file).
207    pub fn load(&mut self, name: &str, _path: &str) -> Result<(), String> {
208        let sound = Sound::new(name);
209        self.sounds.insert(name.to_string(), sound);
210        Ok(())
211    }
212
213    /// Register a sound with custom configuration.
214    pub fn load_with_config(&mut self, sound: Sound) {
215        let name = sound.name.clone();
216        self.sounds.insert(name, sound);
217    }
218
219    /// Play a sound effect.
220    pub fn play(&mut self, name: &str) {
221        self.play_with_fade(name, AudioChannel::Sfx, Fade::None);
222    }
223
224    /// Play a sound on a specific channel.
225    pub fn play_on_channel(&mut self, name: &str, channel: AudioChannel) {
226        self.play_with_fade(name, channel, Fade::None);
227    }
228
229    /// Play music with optional fade.
230    pub fn play_music(&mut self, name: &str, fade: Fade) {
231        // Stop current music
232        self.stop_channel(AudioChannel::Music);
233        self.play_with_fade(name, AudioChannel::Music, fade);
234    }
235
236    /// Internal: play with fade.
237    fn play_with_fade(&mut self, name: &str, channel: AudioChannel, fade: Fade) {
238        let sound = match self.sounds.get(name) {
239            Some(s) => s.clone(),
240            None => return,
241        };
242
243        let base_volume = sound.volume
244            * self.master_volume
245            * self.channel_volumes.get(&channel).copied().unwrap_or(1.0);
246
247        // Spatial attenuation
248        let spatial_volume = if let Some(sound_pos) = sound.position {
249            let dist = sound_pos.distance_to(self.listener_position);
250            if dist <= sound.spatial_min_distance {
251                1.0
252            } else if dist >= sound.spatial_max_distance {
253                0.0
254            } else {
255                1.0 - (dist - sound.spatial_min_distance)
256                    / (sound.spatial_max_distance - sound.spatial_min_distance)
257            }
258        } else {
259            1.0
260        };
261
262        let start_volume = match fade {
263            Fade::In(duration) => {
264                Some(FadeState {
265                    kind: FadeKind::In,
266                    duration,
267                    elapsed: 0.0,
268                    start_volume: 0.0,
269                    target_volume: base_volume * spatial_volume,
270                })
271            }
272            Fade::Out(duration) => {
273                Some(FadeState {
274                    kind: FadeKind::Out,
275                    duration,
276                    elapsed: 0.0,
277                    start_volume: base_volume * spatial_volume,
278                    target_volume: 0.0,
279                })
280            }
281            Fade::None => None,
282        };
283
284        let current_volume = start_volume
285            .as_ref()
286            .map(|f| f.start_volume)
287            .unwrap_or(base_volume * spatial_volume);
288
289        self.instances.push(PlayingInstance {
290            sound_name: name.to_string(),
291            channel,
292            current_volume,
293            fade: start_volume,
294            alive: true,
295        });
296    }
297
298    /// Stop all instances of a named sound.
299    pub fn stop(&mut self, name: &str) {
300        for instance in &mut self.instances {
301            if instance.sound_name == name {
302                instance.alive = false;
303            }
304        }
305    }
306
307    /// Stop all instances on a channel.
308    pub fn stop_channel(&mut self, channel: AudioChannel) {
309        for instance in &mut self.instances {
310            if instance.channel == channel {
311                instance.alive = false;
312            }
313        }
314    }
315
316    /// Stop all sounds.
317    pub fn stop_all(&mut self) {
318        for instance in &mut self.instances {
319            instance.alive = false;
320        }
321    }
322
323    /// Set the master volume (0.0–1.0).
324    pub fn set_master_volume(&mut self, volume: f32) {
325        self.master_volume = volume.clamp(0.0, 1.0);
326    }
327
328    /// Set a channel's volume (0.0–1.0).
329    pub fn set_channel_volume(&mut self, channel: AudioChannel, volume: f32) {
330        self.channel_volumes.insert(channel, volume.clamp(0.0, 1.0));
331    }
332
333    /// Set the listener position for spatial audio.
334    pub fn set_listener_position(&mut self, pos: Vec2) {
335        self.listener_position = pos;
336    }
337
338    /// Pause all audio.
339    pub fn pause_all(&mut self) {
340        // In a real implementation, this would pause the audio output
341    }
342
343    /// Resume all audio.
344    pub fn resume_all(&mut self) {
345        // In a real implementation, this would resume the audio output
346    }
347
348    /// Check if a named sound is currently playing.
349    pub fn is_playing(&self, name: &str) -> bool {
350        self.instances.iter().any(|i| i.sound_name == name && i.alive)
351    }
352
353    /// Update the audio system (call once per frame).
354    pub fn update(&mut self, dt: f32) {
355        for instance in &mut self.instances {
356            if !instance.alive {
357                continue;
358            }
359
360            // Update fade
361            if let Some(fade) = &mut instance.fade {
362                fade.elapsed += dt;
363                let t = (fade.elapsed / fade.duration).clamp(0.0, 1.0);
364                instance.current_volume = fade.start_volume.lerp(fade.target_volume, t);
365
366                if t >= 1.0 {
367                    match fade.kind {
368                        FadeKind::Out => instance.alive = false,
369                        FadeKind::In => instance.fade = None,
370                    }
371                }
372            }
373        }
374
375        // Remove dead instances
376        self.instances.retain(|i| i.alive);
377    }
378
379    /// Number of currently playing instances.
380    pub fn playing_count(&self) -> usize {
381        self.instances.iter().filter(|i| i.alive).count()
382    }
383}
384
385impl Default for AudioManager {
386    fn default() -> Self {
387        Self::new()
388    }
389}