1use glam::Vec3;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum BusId {
18 Music,
19 Sfx,
20 Ambient,
21 Ui,
22 Reverb,
23 Master,
24}
25
26#[derive(Debug, Clone)]
28pub struct Bus {
29 pub id: BusId,
30 pub volume: f32, pub muted: bool,
32 pub effective: f32,
34 fade_target: f32,
36 fade_duration: f32,
38 fade_elapsed: f32,
40 pre_duck: f32,
42 pub ducked: bool,
43}
44
45impl Bus {
46 pub fn new(id: BusId, volume: f32) -> Self {
47 Self {
48 id,
49 volume,
50 muted: false,
51 effective: volume,
52 fade_target: volume,
53 fade_duration: 0.0,
54 fade_elapsed: 0.0,
55 pre_duck: volume,
56 ducked: false,
57 }
58 }
59
60 pub fn tick(&mut self, dt: f32) {
62 if self.fade_duration > 0.0 {
63 self.fade_elapsed += dt;
64 let t = (self.fade_elapsed / self.fade_duration).min(1.0);
65 self.volume = self.pre_duck + (self.fade_target - self.pre_duck) * smooth_step(t);
66 if t >= 1.0 {
67 self.volume = self.fade_target;
68 self.fade_duration = 0.0;
69 self.fade_elapsed = 0.0;
70 }
71 }
72 self.effective = if self.muted { 0.0 } else { self.volume };
73 }
74
75 pub fn fade_to(&mut self, target: f32, duration: f32) {
77 self.fade_target = target.clamp(0.0, 1.0);
78 self.fade_duration = duration.max(0.001);
79 self.fade_elapsed = 0.0;
80 self.pre_duck = self.volume;
81 }
82
83 pub fn duck(&mut self, reduced_volume: f32, attack_s: f32) {
85 if !self.ducked {
86 self.pre_duck = self.volume;
87 self.ducked = true;
88 }
89 self.fade_to(reduced_volume, attack_s);
90 }
91
92 pub fn unduck(&mut self, release_s: f32) {
94 if self.ducked {
95 let target = self.pre_duck;
96 self.fade_to(target, release_s);
97 self.ducked = false;
98 }
99 }
100}
101
102#[derive(Clone, Copy, Debug, Default)]
106pub struct StereoFrame {
107 pub left: f32,
108 pub right: f32,
109}
110
111impl StereoFrame {
112 pub fn mono(sample: f32) -> Self { Self { left: sample, right: sample } }
113
114 pub fn panned(sample: f32, pan: f32) -> Self {
115 let p = pan.clamp(-1.0, 1.0);
117 let angle = (p + 1.0) * std::f32::consts::FRAC_PI_4;
118 Self {
119 left: sample * angle.cos(),
120 right: sample * angle.sin(),
121 }
122 }
123
124 pub fn scaled(self, gain: f32) -> Self {
125 Self { left: self.left * gain, right: self.right * gain }
126 }
127}
128
129impl std::ops::Add for StereoFrame {
130 type Output = Self;
131 fn add(self, rhs: Self) -> Self {
132 Self { left: self.left + rhs.left, right: self.right + rhs.right }
133 }
134}
135
136impl std::ops::AddAssign for StereoFrame {
137 fn add_assign(&mut self, rhs: Self) {
138 self.left += rhs.left;
139 self.right += rhs.right;
140 }
141}
142
143#[derive(Debug, Clone, Copy)]
147pub enum AttenuationModel {
148 Linear,
150 Inverse,
152 InverseSquare,
154 Logarithmic,
156}
157
158pub fn attenuate(dist: f32, min_dist: f32, max_dist: f32, model: AttenuationModel) -> f32 {
160 if dist <= min_dist { return 1.0; }
161 if dist >= max_dist { return 0.0; }
162 let t = (dist - min_dist) / (max_dist - min_dist).max(0.001);
163 match model {
164 AttenuationModel::Linear => 1.0 - t,
165 AttenuationModel::Inverse => min_dist / dist.max(0.001),
166 AttenuationModel::InverseSquare => (min_dist / dist.max(0.001)).powi(2),
167 AttenuationModel::Logarithmic => 1.0 - t.ln().max(-10.0) / (-10.0),
168 }
169}
170
171pub fn spatial_weight(listener: Vec3, source: Vec3, max_distance: f32) -> f32 {
173 let dist = (source - listener).length();
174 if dist >= max_distance { return 0.0; }
175 1.0 - dist / max_distance
176}
177
178pub fn stereo_pan(listener: Vec3, source: Vec3) -> (f32, f32) {
181 let delta = source - listener;
182 let pan = (delta.x / (delta.length().max(0.001))).clamp(-1.0, 1.0);
183 let left = ((1.0 - pan) * 0.5).sqrt();
184 let right = ((1.0 + pan) * 0.5).sqrt();
185 (left, right)
186}
187
188#[derive(Debug, Clone)]
192pub struct ChannelStrip {
193 pub id: u64,
194 pub position: Vec3,
195 pub volume: f32,
196 pub bus: BusId,
197 pub looping: bool,
198 pub attenuation: AttenuationModel,
199 pub min_dist: f32,
200 pub max_dist: f32,
201 pub reverb_send: f32,
202 pub pitch_shift: f32, pub active: bool,
205 pub age: f32,
207 pub max_age: Option<f32>,
209}
210
211impl ChannelStrip {
212 pub fn new(id: u64, position: Vec3, bus: BusId) -> Self {
213 Self {
214 id,
215 position,
216 volume: 1.0,
217 bus,
218 looping: false,
219 attenuation: AttenuationModel::Inverse,
220 min_dist: 1.0,
221 max_dist: 50.0,
222 reverb_send: 0.0,
223 pitch_shift: 0.0,
224 active: true,
225 age: 0.0,
226 max_age: None,
227 }
228 }
229
230 pub fn one_shot(mut self, duration_s: f32) -> Self {
231 self.max_age = Some(duration_s);
232 self
233 }
234
235 pub fn looping(mut self) -> Self {
236 self.looping = true;
237 self
238 }
239
240 pub fn tick(&mut self, dt: f32) {
241 self.age += dt;
242 if let Some(max) = self.max_age {
243 if self.age >= max && !self.looping {
244 self.active = false;
245 }
246 }
247 }
248
249 pub fn stereo_gain(&self, listener: Vec3) -> (f32, f32) {
251 let dist = (self.position - listener).length();
252 let vol = self.volume * attenuate(dist, self.min_dist, self.max_dist, self.attenuation);
253 let (l_pan, r_pan) = stereo_pan(listener, self.position);
254 (vol * l_pan, vol * r_pan)
255 }
256
257 pub fn is_expired(&self) -> bool { !self.active }
258}
259
260#[derive(Debug, Clone)]
264pub struct Limiter {
265 pub threshold: f32,
266 pub release_coef: f32,
267 gain: f32,
268}
269
270impl Limiter {
271 pub fn new(threshold_db: f32, release_ms: f32) -> Self {
272 let threshold = 10.0f32.powf(threshold_db / 20.0);
273 let release_coef = 1.0 - 1.0 / (SAMPLE_RATE * release_ms * 0.001);
274 Self { threshold, release_coef, gain: 1.0 }
275 }
276
277 pub fn tick(&mut self, frame: StereoFrame) -> StereoFrame {
278 let peak = frame.left.abs().max(frame.right.abs());
279 if peak * self.gain > self.threshold {
280 self.gain = self.threshold / peak.max(0.0001);
281 } else {
282 self.gain = (self.gain * self.release_coef).min(1.0);
283 }
284 frame.scaled(self.gain)
285 }
286}
287
288const SAMPLE_RATE: f32 = 48_000.0;
289
290#[derive(Debug, Clone)]
294pub struct Compressor {
295 pub threshold_db: f32,
296 pub ratio: f32, pub attack_coef: f32,
298 pub release_coef: f32,
299 pub makeup_gain: f32, envelope: f32,
301}
302
303impl Compressor {
304 pub fn new(threshold_db: f32, ratio: f32, attack_ms: f32, release_ms: f32) -> Self {
305 let attack_coef = (-2.2 / (SAMPLE_RATE * attack_ms * 0.001)).exp();
306 let release_coef = (-2.2 / (SAMPLE_RATE * release_ms * 0.001)).exp();
307 Self {
308 threshold_db,
309 ratio,
310 attack_coef,
311 release_coef,
312 makeup_gain: 1.0,
313 envelope: 0.0,
314 }
315 }
316
317 pub fn tick(&mut self, frame: StereoFrame) -> StereoFrame {
318 let peak = frame.left.abs().max(frame.right.abs());
319 let peak_db = if peak > 0.0 { 20.0 * peak.log10() } else { -100.0 };
320
321 let coef = if peak_db > self.threshold_db { self.attack_coef } else { self.release_coef };
322 self.envelope = peak_db + coef * (self.envelope - peak_db);
323
324 let gain_db = if self.envelope > self.threshold_db {
325 self.threshold_db + (self.envelope - self.threshold_db) / self.ratio - self.envelope
326 } else {
327 0.0
328 };
329 let gain = 10.0f32.powf(gain_db / 20.0) * self.makeup_gain;
330
331 frame.scaled(gain)
332 }
333}
334
335pub struct Mixer {
339 pub music: Bus,
340 pub sfx: Bus,
341 pub ambient: Bus,
342 pub ui: Bus,
343 pub master: Bus,
344 pub limiter: Limiter,
345 pub compressor: Compressor,
346 channels: Vec<ChannelStrip>,
347 next_id: u64,
348 pub listener_pos: Vec3,
349 pub auto_duck: bool,
351 duck_threshold: f32,
352}
353
354impl Mixer {
355 pub fn new() -> Self {
356 Self {
357 music: Bus::new(BusId::Music, 0.8),
358 sfx: Bus::new(BusId::Sfx, 1.0),
359 ambient: Bus::new(BusId::Ambient, 0.5),
360 ui: Bus::new(BusId::Ui, 0.9),
361 master: Bus::new(BusId::Master, 1.0),
362 limiter: Limiter::new(-1.0, 100.0),
363 compressor: Compressor::new(-12.0, 4.0, 5.0, 100.0),
364 channels: Vec::new(),
365 next_id: 1,
366 listener_pos: Vec3::ZERO,
367 auto_duck: true,
368 duck_threshold: 0.7,
369 }
370 }
371
372 pub fn bus_mut(&mut self, id: BusId) -> &mut Bus {
375 match id {
376 BusId::Music => &mut self.music,
377 BusId::Sfx => &mut self.sfx,
378 BusId::Ambient => &mut self.ambient,
379 BusId::Ui => &mut self.ui,
380 BusId::Master => &mut self.master,
381 BusId::Reverb => &mut self.master, }
383 }
384
385 pub fn bus(&self, id: BusId) -> &Bus {
386 match id {
387 BusId::Music => &self.music,
388 BusId::Sfx => &self.sfx,
389 BusId::Ambient => &self.ambient,
390 BusId::Ui => &self.ui,
391 _ => &self.master,
392 }
393 }
394
395 pub fn set_music_volume(&mut self, v: f32) { self.music.volume = v.clamp(0.0, 1.0); }
396 pub fn set_sfx_volume(&mut self, v: f32) { self.sfx.volume = v.clamp(0.0, 1.0); }
397 pub fn set_ambient_volume(&mut self, v: f32) { self.ambient.volume = v.clamp(0.0, 1.0); }
398 pub fn set_master_volume(&mut self, v: f32) { self.master.volume = v.clamp(0.0, 1.0); }
399
400 pub fn fade_music(&mut self, target: f32, secs: f32) {
401 self.music.fade_to(target, secs);
402 }
403
404 pub fn mute_all(&mut self) {
405 self.music.muted = true;
406 self.sfx.muted = true;
407 self.ambient.muted = true;
408 }
409
410 pub fn unmute_all(&mut self) {
411 self.music.muted = false;
412 self.sfx.muted = false;
413 self.ambient.muted = false;
414 }
415
416 pub fn add_channel(&mut self, position: Vec3, bus: BusId) -> u64 {
420 let id = self.next_id;
421 self.next_id += 1;
422 self.channels.push(ChannelStrip::new(id, position, bus));
423 id
424 }
425
426 pub fn add_oneshot_sfx(&mut self, position: Vec3, duration_s: f32) -> u64 {
428 let id = self.next_id;
429 self.next_id += 1;
430 self.channels.push(ChannelStrip::new(id, position, BusId::Sfx).one_shot(duration_s));
431 id
432 }
433
434 pub fn remove_channel(&mut self, id: u64) {
436 self.channels.retain(|c| c.id != id);
437 }
438
439 pub fn get_channel_mut(&mut self, id: u64) -> Option<&mut ChannelStrip> {
440 self.channels.iter_mut().find(|c| c.id == id)
441 }
442
443 pub fn tick(&mut self, dt: f32) {
447 self.music.tick(dt);
448 self.sfx.tick(dt);
449 self.ambient.tick(dt);
450 self.ui.tick(dt);
451 self.master.tick(dt);
452
453 for ch in &mut self.channels { ch.tick(dt); }
455 self.channels.retain(|ch| !ch.is_expired());
456 }
457
458 pub fn mix_frame(&mut self, channel_gains: &[(u64, f32)]) -> StereoFrame {
460 let mut mix = StereoFrame::default();
461
462 for ch in &self.channels {
463 if !ch.active { continue; }
464 let bus_gain = self.bus(ch.bus).effective;
465 if bus_gain == 0.0 { continue; }
466
467 let ch_gain = channel_gains.iter()
469 .find(|(id, _)| *id == ch.id)
470 .map(|(_, g)| *g)
471 .unwrap_or(0.0);
472
473 let (l, r) = ch.stereo_gain(self.listener_pos);
474 mix += StereoFrame {
475 left: ch_gain * l * bus_gain,
476 right: ch_gain * r * bus_gain,
477 };
478 }
479
480 mix = mix.scaled(self.master.effective);
482 mix = self.compressor.tick(mix);
483 mix = self.limiter.tick(mix);
484 mix
485 }
486
487 pub fn channel_count(&self) -> usize { self.channels.len() }
489
490 pub fn update_auto_duck(&mut self) {
492 if !self.auto_duck { return; }
493 let sfx_vol = self.sfx.volume;
495 if sfx_vol > self.duck_threshold && !self.music.ducked {
496 self.music.duck(sfx_vol * 0.4, 0.2);
497 } else if sfx_vol <= self.duck_threshold && self.music.ducked {
498 self.music.unduck(0.5);
499 }
500 }
501}
502
503impl Default for Mixer {
504 fn default() -> Self { Self::new() }
505}
506
507fn smooth_step(t: f32) -> f32 {
510 let t = t.clamp(0.0, 1.0);
511 t * t * (3.0 - 2.0 * t)
512}
513
514#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn bus_fade_reaches_target() {
522 let mut bus = Bus::new(BusId::Music, 1.0);
523 bus.fade_to(0.0, 1.0);
524 for _ in 0..100 { bus.tick(0.01); }
525 assert!(bus.volume < 0.01, "Expected near-zero volume, got {}", bus.volume);
526 }
527
528 #[test]
529 fn spatial_weight_decreases_with_distance() {
530 let listener = Vec3::ZERO;
531 let near = spatial_weight(listener, Vec3::new(5.0, 0.0, 0.0), 50.0);
532 let far = spatial_weight(listener, Vec3::new(40.0, 0.0, 0.0), 50.0);
533 assert!(near > far);
534 }
535
536 #[test]
537 fn stereo_pan_right_source_louder_right() {
538 let listener = Vec3::ZERO;
539 let source = Vec3::new(5.0, 0.0, 0.0);
540 let (l, r) = stereo_pan(listener, source);
541 assert!(r > l, "Right source should be louder in right channel");
542 }
543
544 #[test]
545 fn attenuation_at_min_dist_is_one() {
546 assert!((attenuate(0.5, 1.0, 50.0, AttenuationModel::Linear) - 1.0).abs() < 0.001);
547 }
548
549 #[test]
550 fn attenuation_at_max_dist_is_zero() {
551 assert!(attenuate(50.0, 1.0, 50.0, AttenuationModel::Inverse) < 0.001);
552 }
553
554 #[test]
555 fn mixer_channel_expires() {
556 let mut mixer = Mixer::new();
557 mixer.add_oneshot_sfx(Vec3::ZERO, 0.1);
558 assert_eq!(mixer.channel_count(), 1);
559 mixer.tick(0.2);
560 assert_eq!(mixer.channel_count(), 0);
561 }
562
563 #[test]
564 fn limiter_clamps_peaks() {
565 let mut lim = Limiter::new(-0.0, 10.0);
566 let loud = StereoFrame { left: 5.0, right: 5.0 };
567 let out = lim.tick(loud);
568 assert!(out.left <= 1.01, "Expected ≤1, got {}", out.left);
569 }
570
571 #[test]
572 fn duck_reduces_music_volume() {
573 let mut mixer = Mixer::new();
574 mixer.music.volume = 1.0;
575 mixer.sfx.volume = 0.9;
576 mixer.update_auto_duck();
577 for _ in 0..100 { mixer.tick(0.01); }
579 assert!(mixer.music.volume < 0.9, "Expected ducked, got {}", mixer.music.volume);
580 }
581}