Skip to main content

proof_engine/editor/
audio_editor.rs

1
2//! Audio system editor — waveform display, mixer, spatial audio config, DSP chain.
3
4use glam::{Vec2, Vec3};
5use std::collections::HashMap;
6
7// ---------------------------------------------------------------------------
8// Primitive types
9// ---------------------------------------------------------------------------
10
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum AudioFormat {
13    Pcm8,
14    Pcm16,
15    Pcm24,
16    Pcm32,
17    Float32,
18    Float64,
19    Adpcm,
20    Opus,
21    Vorbis,
22    Mp3,
23    Aac,
24}
25
26impl AudioFormat {
27    pub fn bits_per_sample(self) -> u32 {
28        match self {
29            AudioFormat::Pcm8 => 8,
30            AudioFormat::Pcm16 => 16,
31            AudioFormat::Pcm24 => 24,
32            AudioFormat::Pcm32 | AudioFormat::Float32 => 32,
33            AudioFormat::Float64 => 64,
34            AudioFormat::Adpcm => 4,
35            _ => 0, // compressed
36        }
37    }
38    pub fn is_compressed(self) -> bool {
39        matches!(self, AudioFormat::Opus | AudioFormat::Vorbis | AudioFormat::Mp3 | AudioFormat::Aac)
40    }
41    pub fn label(self) -> &'static str {
42        match self {
43            AudioFormat::Pcm8 => "PCM 8-bit",
44            AudioFormat::Pcm16 => "PCM 16-bit",
45            AudioFormat::Pcm24 => "PCM 24-bit",
46            AudioFormat::Pcm32 => "PCM 32-bit",
47            AudioFormat::Float32 => "Float 32",
48            AudioFormat::Float64 => "Float 64",
49            AudioFormat::Adpcm => "ADPCM",
50            AudioFormat::Opus => "Opus",
51            AudioFormat::Vorbis => "Vorbis",
52            AudioFormat::Mp3 => "MP3",
53            AudioFormat::Aac => "AAC",
54        }
55    }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
59pub enum ChannelLayout {
60    Mono,
61    Stereo,
62    Quad,
63    Surround51,
64    Surround71,
65    Ambisonics1stOrder,
66    Ambisonics2ndOrder,
67    Ambisonics3rdOrder,
68}
69
70impl ChannelLayout {
71    pub fn channel_count(self) -> u32 {
72        match self {
73            ChannelLayout::Mono => 1,
74            ChannelLayout::Stereo => 2,
75            ChannelLayout::Quad => 4,
76            ChannelLayout::Surround51 => 6,
77            ChannelLayout::Surround71 => 8,
78            ChannelLayout::Ambisonics1stOrder => 4,
79            ChannelLayout::Ambisonics2ndOrder => 9,
80            ChannelLayout::Ambisonics3rdOrder => 16,
81        }
82    }
83    pub fn label(self) -> &'static str {
84        match self {
85            ChannelLayout::Mono => "Mono",
86            ChannelLayout::Stereo => "Stereo",
87            ChannelLayout::Quad => "Quad",
88            ChannelLayout::Surround51 => "5.1",
89            ChannelLayout::Surround71 => "7.1",
90            ChannelLayout::Ambisonics1stOrder => "Ambisonics 1st Order (4ch)",
91            ChannelLayout::Ambisonics2ndOrder => "Ambisonics 2nd Order (9ch)",
92            ChannelLayout::Ambisonics3rdOrder => "Ambisonics 3rd Order (16ch)",
93        }
94    }
95}
96
97// ---------------------------------------------------------------------------
98// Waveform data + display
99// ---------------------------------------------------------------------------
100
101#[derive(Debug, Clone)]
102pub struct AudioAsset {
103    pub id: u64,
104    pub name: String,
105    pub format: AudioFormat,
106    pub layout: ChannelLayout,
107    pub sample_rate: u32,
108    pub frame_count: u64,
109    pub loop_start: Option<u64>,
110    pub loop_end: Option<u64>,
111    pub peak_amplitude: f32,
112    pub rms_amplitude: f32,
113    pub tags: Vec<String>,
114    /// Downsampled waveform for display (interleaved by channel)
115    pub waveform_preview: Vec<f32>,
116    pub preview_frames_per_pixel: u32,
117}
118
119impl AudioAsset {
120    pub fn new(id: u64, name: impl Into<String>, format: AudioFormat, layout: ChannelLayout, sample_rate: u32, frame_count: u64) -> Self {
121        Self {
122            id,
123            name: name.into(),
124            format,
125            layout,
126            sample_rate,
127            frame_count,
128            loop_start: None,
129            loop_end: None,
130            peak_amplitude: 1.0,
131            rms_amplitude: 0.707,
132            tags: Vec::new(),
133            waveform_preview: Vec::new(),
134            preview_frames_per_pixel: 64,
135        }
136    }
137
138    pub fn duration_secs(&self) -> f64 {
139        self.frame_count as f64 / self.sample_rate as f64
140    }
141
142    pub fn byte_size_uncompressed(&self) -> u64 {
143        self.frame_count * self.layout.channel_count() as u64 * (self.format.bits_per_sample() as u64 / 8).max(1)
144    }
145
146    pub fn generate_synthetic_waveform(&mut self, pixel_width: usize) {
147        self.waveform_preview.clear();
148        let channels = self.layout.channel_count() as usize;
149        for ch in 0..channels {
150            for px in 0..pixel_width {
151                let t = px as f32 / pixel_width as f32;
152                // Simulate a realistic waveform shape using multiple harmonics
153                let v = (t * std::f32::consts::TAU * 3.0).sin() * 0.6
154                    + (t * std::f32::consts::TAU * 7.0).sin() * 0.25
155                    + (t * std::f32::consts::TAU * 11.0 + ch as f32).sin() * 0.15;
156                // Envelope shape
157                let envelope = (t * std::f32::consts::PI).sin().powf(0.3);
158                self.waveform_preview.push(v * envelope);
159            }
160        }
161        self.preview_frames_per_pixel = (self.frame_count as u32).saturating_div(pixel_width as u32).max(1);
162    }
163
164    pub fn set_loop_region(&mut self, start_frame: u64, end_frame: u64) {
165        assert!(start_frame < end_frame && end_frame <= self.frame_count);
166        self.loop_start = Some(start_frame);
167        self.loop_end = Some(end_frame);
168    }
169
170    pub fn clear_loop(&mut self) {
171        self.loop_start = None;
172        self.loop_end = None;
173    }
174
175    pub fn frame_at_time(&self, t: f64) -> u64 {
176        ((t * self.sample_rate as f64) as u64).min(self.frame_count.saturating_sub(1))
177    }
178}
179
180// ---------------------------------------------------------------------------
181// Waveform editor panel
182// ---------------------------------------------------------------------------
183
184#[derive(Debug, Clone)]
185pub struct WaveformView {
186    pub asset_id: u64,
187    pub view_start_frame: u64,
188    pub view_end_frame: u64,
189    pub channel_heights: Vec<f32>,
190    pub zoom: f32,
191    pub cursor_frame: u64,
192    pub selection_start: Option<u64>,
193    pub selection_end: Option<u64>,
194    pub show_loop_markers: bool,
195    pub show_spectogram: bool,
196    pub amplitude_scale: f32,
197}
198
199impl WaveformView {
200    pub fn new(asset: &AudioAsset) -> Self {
201        Self {
202            asset_id: asset.id,
203            view_start_frame: 0,
204            view_end_frame: asset.frame_count,
205            channel_heights: vec![80.0; asset.layout.channel_count() as usize],
206            zoom: 1.0,
207            cursor_frame: 0,
208            selection_start: None,
209            selection_end: None,
210            show_loop_markers: true,
211            show_spectogram: false,
212            amplitude_scale: 1.0,
213        }
214    }
215
216    pub fn view_duration_frames(&self) -> u64 {
217        self.view_end_frame.saturating_sub(self.view_start_frame)
218    }
219
220    pub fn frame_to_x(&self, frame: u64, width: f32) -> f32 {
221        let t = (frame.saturating_sub(self.view_start_frame)) as f32 / self.view_duration_frames().max(1) as f32;
222        t * width
223    }
224
225    pub fn x_to_frame(&self, x: f32, width: f32) -> u64 {
226        let t = (x / width).clamp(0.0, 1.0);
227        self.view_start_frame + (t * self.view_duration_frames() as f32) as u64
228    }
229
230    pub fn scroll(&mut self, delta_frames: i64, total_frames: u64) {
231        let dur = self.view_duration_frames();
232        let start = (self.view_start_frame as i64 + delta_frames).max(0) as u64;
233        let end = (start + dur).min(total_frames);
234        self.view_start_frame = end.saturating_sub(dur);
235        self.view_end_frame = end;
236    }
237
238    pub fn zoom_around_cursor(&mut self, factor: f32, total_frames: u64) {
239        let center = self.cursor_frame;
240        let half = (self.view_duration_frames() as f32 / factor / 2.0) as u64;
241        self.view_start_frame = center.saturating_sub(half);
242        self.view_end_frame = (center + half).min(total_frames);
243        self.zoom *= factor;
244    }
245
246    pub fn select_region(&mut self, start_x: f32, end_x: f32, width: f32) {
247        let a = self.x_to_frame(start_x.min(end_x), width);
248        let b = self.x_to_frame(start_x.max(end_x), width);
249        self.selection_start = Some(a);
250        self.selection_end = Some(b);
251    }
252
253    pub fn has_selection(&self) -> bool {
254        self.selection_start.is_some() && self.selection_end.is_some()
255    }
256
257    pub fn selection_length(&self) -> u64 {
258        match (self.selection_start, self.selection_end) {
259            (Some(a), Some(b)) => b.saturating_sub(a),
260            _ => 0,
261        }
262    }
263}
264
265// ---------------------------------------------------------------------------
266// DSP effect chain
267// ---------------------------------------------------------------------------
268
269#[derive(Debug, Clone, PartialEq)]
270pub enum DspEffectKind {
271    Gain,
272    Lowpass,
273    Highpass,
274    Bandpass,
275    Notch,
276    Peaking,
277    LowShelf,
278    HighShelf,
279    Compressor,
280    Limiter,
281    Gate,
282    Reverb,
283    Delay,
284    Chorus,
285    Flanger,
286    Phaser,
287    Distortion,
288    Bitcrusher,
289    Eq4Band,
290    Eq8Band,
291    Eq31Band,
292    StereoWidener,
293    MidSide,
294    Tremolo,
295    Vibrato,
296    Autopan,
297    RingModulator,
298    Pitch,
299    Transient,
300    DeEsser,
301    NoiseGate,
302}
303
304impl DspEffectKind {
305    pub fn label(&self) -> &'static str {
306        match self {
307            DspEffectKind::Gain => "Gain",
308            DspEffectKind::Lowpass => "Low-pass Filter",
309            DspEffectKind::Highpass => "High-pass Filter",
310            DspEffectKind::Bandpass => "Band-pass Filter",
311            DspEffectKind::Notch => "Notch Filter",
312            DspEffectKind::Peaking => "Peaking EQ",
313            DspEffectKind::LowShelf => "Low Shelf",
314            DspEffectKind::HighShelf => "High Shelf",
315            DspEffectKind::Compressor => "Compressor",
316            DspEffectKind::Limiter => "Limiter",
317            DspEffectKind::Gate => "Gate",
318            DspEffectKind::Reverb => "Reverb",
319            DspEffectKind::Delay => "Delay",
320            DspEffectKind::Chorus => "Chorus",
321            DspEffectKind::Flanger => "Flanger",
322            DspEffectKind::Phaser => "Phaser",
323            DspEffectKind::Distortion => "Distortion",
324            DspEffectKind::Bitcrusher => "Bitcrusher",
325            DspEffectKind::Eq4Band => "4-Band EQ",
326            DspEffectKind::Eq8Band => "8-Band EQ",
327            DspEffectKind::Eq31Band => "31-Band EQ",
328            DspEffectKind::StereoWidener => "Stereo Widener",
329            DspEffectKind::MidSide => "Mid/Side",
330            DspEffectKind::Tremolo => "Tremolo",
331            DspEffectKind::Vibrato => "Vibrato",
332            DspEffectKind::Autopan => "Auto-pan",
333            DspEffectKind::RingModulator => "Ring Modulator",
334            DspEffectKind::Pitch => "Pitch Shift",
335            DspEffectKind::Transient => "Transient Shaper",
336            DspEffectKind::DeEsser => "De-Esser",
337            DspEffectKind::NoiseGate => "Noise Gate",
338        }
339    }
340
341    pub fn category(&self) -> &'static str {
342        match self {
343            DspEffectKind::Gain => "Utility",
344            DspEffectKind::Lowpass | DspEffectKind::Highpass | DspEffectKind::Bandpass
345            | DspEffectKind::Notch | DspEffectKind::Peaking | DspEffectKind::LowShelf
346            | DspEffectKind::HighShelf => "Filter",
347            DspEffectKind::Eq4Band | DspEffectKind::Eq8Band | DspEffectKind::Eq31Band => "EQ",
348            DspEffectKind::Compressor | DspEffectKind::Limiter | DspEffectKind::Gate
349            | DspEffectKind::NoiseGate | DspEffectKind::Transient | DspEffectKind::DeEsser => "Dynamics",
350            DspEffectKind::Reverb | DspEffectKind::Delay => "Time-Based",
351            DspEffectKind::Chorus | DspEffectKind::Flanger | DspEffectKind::Phaser => "Modulation",
352            DspEffectKind::Distortion | DspEffectKind::Bitcrusher => "Distortion",
353            DspEffectKind::StereoWidener | DspEffectKind::MidSide => "Stereo",
354            DspEffectKind::Tremolo | DspEffectKind::Vibrato | DspEffectKind::Autopan
355            | DspEffectKind::RingModulator => "Modulation",
356            DspEffectKind::Pitch => "Pitch",
357        }
358    }
359
360    pub fn default_params(&self) -> Vec<(&'static str, f32, f32, f32)> {
361        // (name, default, min, max)
362        match self {
363            DspEffectKind::Gain => vec![("gain_db", 0.0, -60.0, 24.0)],
364            DspEffectKind::Lowpass | DspEffectKind::Highpass => vec![
365                ("freq_hz", 1000.0, 20.0, 20000.0),
366                ("resonance", 0.707, 0.1, 20.0),
367            ],
368            DspEffectKind::Bandpass | DspEffectKind::Notch => vec![
369                ("freq_hz", 1000.0, 20.0, 20000.0),
370                ("bandwidth", 1.0, 0.1, 10.0),
371            ],
372            DspEffectKind::Peaking | DspEffectKind::LowShelf | DspEffectKind::HighShelf => vec![
373                ("freq_hz", 1000.0, 20.0, 20000.0),
374                ("gain_db", 0.0, -24.0, 24.0),
375                ("q", 1.0, 0.1, 10.0),
376            ],
377            DspEffectKind::Compressor => vec![
378                ("threshold_db", -12.0, -60.0, 0.0),
379                ("ratio", 4.0, 1.0, 100.0),
380                ("attack_ms", 10.0, 0.1, 200.0),
381                ("release_ms", 100.0, 1.0, 3000.0),
382                ("makeup_db", 0.0, 0.0, 30.0),
383            ],
384            DspEffectKind::Reverb => vec![
385                ("room_size", 0.5, 0.0, 1.0),
386                ("damping", 0.5, 0.0, 1.0),
387                ("wet", 0.33, 0.0, 1.0),
388                ("width", 1.0, 0.0, 1.0),
389            ],
390            DspEffectKind::Delay => vec![
391                ("delay_ms", 250.0, 1.0, 5000.0),
392                ("feedback", 0.4, 0.0, 0.99),
393                ("wet", 0.5, 0.0, 1.0),
394                ("ping_pong", 0.0, 0.0, 1.0),
395            ],
396            DspEffectKind::Pitch => vec![
397                ("semitones", 0.0, -24.0, 24.0),
398                ("formant_shift", 0.0, -2.0, 2.0),
399            ],
400            _ => vec![],
401        }
402    }
403}
404
405#[derive(Debug, Clone)]
406pub struct DspEffect {
407    pub id: u32,
408    pub kind: DspEffectKind,
409    pub enabled: bool,
410    pub params: HashMap<String, f32>,
411    pub wet_dry: f32,
412}
413
414impl DspEffect {
415    pub fn new(id: u32, kind: DspEffectKind) -> Self {
416        let params = kind.default_params()
417            .into_iter()
418            .map(|(name, default, _, _)| (name.to_string(), default))
419            .collect();
420        Self { id, kind, enabled: true, params, wet_dry: 1.0 }
421    }
422
423    pub fn set_param(&mut self, name: &str, value: f32) {
424        if let Some(v) = self.params.get_mut(name) {
425            *v = value;
426        }
427    }
428
429    pub fn get_param(&self, name: &str) -> f32 {
430        self.params.get(name).copied().unwrap_or(0.0)
431    }
432}
433
434#[derive(Debug, Clone)]
435pub struct DspChain {
436    pub effects: Vec<DspEffect>,
437    pub next_id: u32,
438}
439
440impl DspChain {
441    pub fn new() -> Self {
442        Self { effects: Vec::new(), next_id: 1 }
443    }
444
445    pub fn add_effect(&mut self, kind: DspEffectKind) -> u32 {
446        let id = self.next_id;
447        self.next_id += 1;
448        self.effects.push(DspEffect::new(id, kind));
449        id
450    }
451
452    pub fn remove_effect(&mut self, id: u32) {
453        self.effects.retain(|e| e.id != id);
454    }
455
456    pub fn move_effect_up(&mut self, id: u32) {
457        if let Some(i) = self.effects.iter().position(|e| e.id == id) {
458            if i > 0 {
459                self.effects.swap(i, i - 1);
460            }
461        }
462    }
463
464    pub fn move_effect_down(&mut self, id: u32) {
465        if let Some(i) = self.effects.iter().position(|e| e.id == id) {
466            if i + 1 < self.effects.len() {
467                self.effects.swap(i, i + 1);
468            }
469        }
470    }
471
472    pub fn active_effects(&self) -> impl Iterator<Item = &DspEffect> {
473        self.effects.iter().filter(|e| e.enabled)
474    }
475}
476
477// ---------------------------------------------------------------------------
478// Audio bus / mixer channel
479// ---------------------------------------------------------------------------
480
481#[derive(Debug, Clone, Copy, PartialEq)]
482pub enum SendType {
483    PreFader,
484    PostFader,
485    PostFaderPostPan,
486}
487
488#[derive(Debug, Clone)]
489pub struct BusSend {
490    pub target_bus: String,
491    pub gain: f32,
492    pub send_type: SendType,
493    pub enabled: bool,
494}
495
496#[derive(Debug, Clone)]
497pub struct MixerChannel {
498    pub name: String,
499    pub gain_db: f32,
500    pub pan: f32,       // -1 .. 1
501    pub muted: bool,
502    pub soloed: bool,
503    pub fader_automation: bool,
504    pub sends: Vec<BusSend>,
505    pub dsp_chain: DspChain,
506    pub color: [u8; 3],
507    pub vumeter_l: f32,
508    pub vumeter_r: f32,
509    pub peak_l: f32,
510    pub peak_r: f32,
511    pub peak_hold_timer: f32,
512}
513
514impl MixerChannel {
515    pub fn new(name: impl Into<String>) -> Self {
516        Self {
517            name: name.into(),
518            gain_db: 0.0,
519            pan: 0.0,
520            muted: false,
521            soloed: false,
522            fader_automation: false,
523            sends: Vec::new(),
524            dsp_chain: DspChain::new(),
525            color: [80, 120, 200],
526            vumeter_l: 0.0,
527            vumeter_r: 0.0,
528            peak_l: 0.0,
529            peak_r: 0.0,
530            peak_hold_timer: 0.0,
531        }
532    }
533
534    pub fn linear_gain(&self) -> f32 {
535        if self.muted { return 0.0; }
536        10f32.powf(self.gain_db / 20.0)
537    }
538
539    pub fn pan_left(&self) -> f32 {
540        ((1.0 - self.pan) * 0.5).sqrt()
541    }
542
543    pub fn pan_right(&self) -> f32 {
544        ((1.0 + self.pan) * 0.5).sqrt()
545    }
546
547    pub fn update_vu(&mut self, l: f32, r: f32, dt: f32) {
548        let attack = 0.003_f32;
549        let release = 0.3_f32;
550        let a_attack = (-2.2 / (attack * 44100.0)).exp();
551        let a_release = (-2.2 / (release * 44100.0)).exp();
552        self.vumeter_l = if l > self.vumeter_l { a_attack * self.vumeter_l + (1.0 - a_attack) * l }
553                         else { a_release * self.vumeter_l + (1.0 - a_release) * l };
554        self.vumeter_r = if r > self.vumeter_r { a_attack * self.vumeter_r + (1.0 - a_attack) * r }
555                         else { a_release * self.vumeter_r + (1.0 - a_release) * r };
556        if l > self.peak_l { self.peak_l = l; self.peak_hold_timer = 2.0; }
557        if r > self.peak_r { self.peak_r = r; self.peak_hold_timer = 2.0; }
558        self.peak_hold_timer -= dt;
559        if self.peak_hold_timer <= 0.0 {
560            self.peak_l = (self.peak_l - dt * 0.3).max(0.0);
561            self.peak_r = (self.peak_r - dt * 0.3).max(0.0);
562        }
563    }
564}
565
566// ---------------------------------------------------------------------------
567// Spatial audio source
568// ---------------------------------------------------------------------------
569
570#[derive(Debug, Clone, Copy, PartialEq)]
571pub enum AttenuationCurve {
572    Linear,
573    InverseSquare,
574    Logarithmic,
575    Custom,
576}
577
578#[derive(Debug, Clone, Copy, PartialEq)]
579pub enum PanningModel {
580    Stereo,
581    Hrtf,
582    Ambisonics,
583    VbapSurround,
584}
585
586#[derive(Debug, Clone)]
587pub struct SpatialAudioSource {
588    pub name: String,
589    pub position: Vec3,
590    pub velocity: Vec3,
591    pub min_distance: f32,
592    pub max_distance: f32,
593    pub attenuation: AttenuationCurve,
594    pub rolloff_factor: f32,
595    pub cone_inner_angle: f32,   // degrees
596    pub cone_outer_angle: f32,   // degrees
597    pub cone_outer_gain: f32,
598    pub doppler_factor: f32,
599    pub panning_model: PanningModel,
600    pub occlude_factor: f32,
601    pub reverb_send: f32,
602    pub asset_id: Option<u64>,
603    pub bus: String,
604    pub loop_audio: bool,
605    pub auto_play: bool,
606    pub priority: i32,
607}
608
609impl SpatialAudioSource {
610    pub fn new(name: impl Into<String>) -> Self {
611        Self {
612            name: name.into(),
613            position: Vec3::ZERO,
614            velocity: Vec3::ZERO,
615            min_distance: 1.0,
616            max_distance: 100.0,
617            attenuation: AttenuationCurve::InverseSquare,
618            rolloff_factor: 1.0,
619            cone_inner_angle: 360.0,
620            cone_outer_angle: 360.0,
621            cone_outer_gain: 0.0,
622            doppler_factor: 1.0,
623            panning_model: PanningModel::Hrtf,
624            occlude_factor: 0.0,
625            reverb_send: 0.0,
626            asset_id: None,
627            bus: "SFX".to_string(),
628            loop_audio: false,
629            auto_play: false,
630            priority: 128,
631        }
632    }
633
634    pub fn gain_at_distance(&self, dist: f32) -> f32 {
635        let d = dist.max(self.min_distance);
636        if d >= self.max_distance { return 0.0; }
637        match self.attenuation {
638            AttenuationCurve::Linear => {
639                1.0 - self.rolloff_factor * (d - self.min_distance) / (self.max_distance - self.min_distance)
640            }
641            AttenuationCurve::InverseSquare => {
642                self.min_distance / (self.min_distance + self.rolloff_factor * (d - self.min_distance))
643            }
644            AttenuationCurve::Logarithmic => {
645                (1.0 - self.rolloff_factor * (d / self.min_distance).ln() / (self.max_distance / self.min_distance).ln()).max(0.0)
646            }
647            AttenuationCurve::Custom => 1.0,
648        }
649    }
650
651    pub fn directional_gain(&self, listener_dir: Vec3) -> f32 {
652        if self.cone_inner_angle >= 360.0 { return 1.0; }
653        let angle_deg = listener_dir.dot(Vec3::NEG_Z).acos().to_degrees();
654        if angle_deg <= self.cone_inner_angle * 0.5 {
655            1.0
656        } else if angle_deg >= self.cone_outer_angle * 0.5 {
657            self.cone_outer_gain
658        } else {
659            let t = (angle_deg - self.cone_inner_angle * 0.5) / ((self.cone_outer_angle - self.cone_inner_angle) * 0.5);
660            1.0 + (self.cone_outer_gain - 1.0) * t
661        }
662    }
663}
664
665// ---------------------------------------------------------------------------
666// Audio Mixer
667// ---------------------------------------------------------------------------
668
669#[derive(Debug, Clone)]
670pub struct AudioMixer {
671    pub buses: Vec<MixerChannel>,
672    pub master: MixerChannel,
673    pub sample_rate: u32,
674    pub buffer_size: u32,
675    pub latency_ms: f32,
676}
677
678impl AudioMixer {
679    pub fn new() -> Self {
680        let mut mixer = Self {
681            buses: Vec::new(),
682            master: MixerChannel::new("Master"),
683            sample_rate: 48000,
684            buffer_size: 512,
685            latency_ms: 0.0,
686        };
687        mixer.latency_ms = mixer.buffer_size as f32 / mixer.sample_rate as f32 * 1000.0;
688        // Default buses
689        mixer.add_bus("Music");
690        mixer.add_bus("SFX");
691        mixer.add_bus("Voice");
692        mixer.add_bus("Ambient");
693        mixer.add_bus("UI");
694        mixer.add_bus("Reverb");
695        mixer
696    }
697
698    pub fn add_bus(&mut self, name: impl Into<String>) {
699        self.buses.push(MixerChannel::new(name));
700    }
701
702    pub fn remove_bus(&mut self, name: &str) {
703        self.buses.retain(|b| b.name != name);
704    }
705
706    pub fn bus_mut(&mut self, name: &str) -> Option<&mut MixerChannel> {
707        self.buses.iter_mut().find(|b| b.name == name)
708    }
709
710    pub fn bus(&self, name: &str) -> Option<&MixerChannel> {
711        self.buses.iter().find(|b| b.name == name)
712    }
713
714    pub fn set_solo(&mut self, name: &str, solo: bool) {
715        for bus in self.buses.iter_mut() {
716            bus.soloed = bus.name == name && solo;
717        }
718    }
719
720    pub fn any_soloed(&self) -> bool {
721        self.buses.iter().any(|b| b.soloed)
722    }
723
724    pub fn update_meters(&mut self, dt: f32) {
725        // Simulate decaying meters
726        for bus in self.buses.iter_mut() {
727            bus.vumeter_l = (bus.vumeter_l - dt * 8.0).max(0.0);
728            bus.vumeter_r = (bus.vumeter_r - dt * 8.0).max(0.0);
729            bus.update_vu(bus.vumeter_l, bus.vumeter_r, dt);
730        }
731    }
732}
733
734// ---------------------------------------------------------------------------
735// Audio editor state
736// ---------------------------------------------------------------------------
737
738#[derive(Debug, Clone, Copy, PartialEq)]
739pub enum AudioEditorPanel {
740    Mixer,
741    WaveformEditor,
742    SpatialSourceList,
743    DspChainEditor,
744    AssetLibrary,
745}
746
747#[derive(Debug, Clone)]
748pub struct AudioEditor {
749    pub mixer: AudioMixer,
750    pub assets: Vec<AudioAsset>,
751    pub spatial_sources: Vec<SpatialAudioSource>,
752    pub waveform_views: HashMap<u64, WaveformView>,
753    pub selected_asset: Option<u64>,
754    pub selected_bus: Option<String>,
755    pub selected_source: Option<usize>,
756    pub active_panel: AudioEditorPanel,
757    pub search_query: String,
758    pub show_db_scale: bool,
759    pub master_volume: f32,
760    pub next_asset_id: u64,
761}
762
763impl AudioEditor {
764    pub fn new() -> Self {
765        let mut editor = Self {
766            mixer: AudioMixer::new(),
767            assets: Vec::new(),
768            spatial_sources: Vec::new(),
769            waveform_views: HashMap::new(),
770            selected_asset: None,
771            selected_bus: None,
772            selected_source: None,
773            active_panel: AudioEditorPanel::Mixer,
774            search_query: String::new(),
775            show_db_scale: true,
776            master_volume: 1.0,
777            next_asset_id: 1,
778        };
779        editor.populate_demo_assets();
780        editor
781    }
782
783    fn populate_demo_assets(&mut self) {
784        let formats = [
785            (AudioFormat::Pcm16, ChannelLayout::Stereo, "footstep_concrete.wav", 44100u32, 4410u64),
786            (AudioFormat::Opus, ChannelLayout::Stereo, "music_theme.opus", 48000, 2880000),
787            (AudioFormat::Float32, ChannelLayout::Surround51, "ambience_forest.wav", 48000, 288000),
788            (AudioFormat::Vorbis, ChannelLayout::Mono, "voice_line_01.ogg", 22050, 44100),
789            (AudioFormat::Pcm16, ChannelLayout::Mono, "explosion_01.wav", 44100, 22050),
790            (AudioFormat::Pcm24, ChannelLayout::Stereo, "ui_click.wav", 44100, 1323),
791            (AudioFormat::Pcm16, ChannelLayout::Stereo, "weapon_reload.wav", 44100, 8820),
792            (AudioFormat::Opus, ChannelLayout::Ambisonics1stOrder, "room_tone_ambisonic.opus", 48000, 96000),
793        ];
794        for (fmt, layout, name, sr, frames) in &formats {
795            let id = self.next_asset_id;
796            self.next_asset_id += 1;
797            let mut asset = AudioAsset::new(id, *name, *fmt, *layout, *sr, *frames);
798            asset.generate_synthetic_waveform(512);
799            asset.rms_amplitude = 0.3 + (id as f32 * 0.07) % 0.5;
800            asset.peak_amplitude = asset.rms_amplitude * 1.8;
801            self.assets.push(asset);
802        }
803        // Spatial sources
804        for i in 0..5usize {
805            let mut src = SpatialAudioSource::new(format!("SpatialSource_{}", i));
806            src.position = Vec3::new(i as f32 * 5.0 - 10.0, 0.0, i as f32 * 2.0);
807            src.asset_id = Some(i as u64 % self.assets.len() as u64 + 1);
808            self.spatial_sources.push(src);
809        }
810    }
811
812    pub fn open_waveform_editor(&mut self, asset_id: u64) {
813        if let Some(asset) = self.assets.iter().find(|a| a.id == asset_id) {
814            let view = WaveformView::new(asset);
815            self.waveform_views.insert(asset_id, view);
816            self.selected_asset = Some(asset_id);
817            self.active_panel = AudioEditorPanel::WaveformEditor;
818        }
819    }
820
821    pub fn search_assets(&self, query: &str) -> Vec<&AudioAsset> {
822        let q = query.to_lowercase();
823        self.assets.iter().filter(|a| {
824            a.name.to_lowercase().contains(&q) ||
825            a.tags.iter().any(|t| t.to_lowercase().contains(&q)) ||
826            a.format.label().to_lowercase().contains(&q)
827        }).collect()
828    }
829
830    pub fn total_asset_size_bytes(&self) -> u64 {
831        self.assets.iter().map(|a| a.byte_size_uncompressed()).sum()
832    }
833
834    pub fn add_dsp_to_selected_bus(&mut self, kind: DspEffectKind) {
835        if let Some(bus_name) = &self.selected_bus.clone() {
836            if let Some(bus) = self.mixer.bus_mut(bus_name) {
837                bus.dsp_chain.add_effect(kind);
838            }
839        }
840    }
841
842    pub fn update(&mut self, dt: f32) {
843        self.mixer.update_meters(dt);
844    }
845}
846
847// ---------------------------------------------------------------------------
848// Tests
849// ---------------------------------------------------------------------------
850#[cfg(test)]
851mod tests {
852    use super::*;
853
854    #[test]
855    fn test_audio_asset_creation() {
856        let asset = AudioAsset::new(1, "test.wav", AudioFormat::Pcm16, ChannelLayout::Stereo, 44100, 44100);
857        assert!((asset.duration_secs() - 1.0).abs() < 1e-6);
858        assert_eq!(asset.byte_size_uncompressed(), 44100 * 2 * 2);
859    }
860
861    #[test]
862    fn test_dsp_chain() {
863        let mut chain = DspChain::new();
864        let id = chain.add_effect(DspEffectKind::Compressor);
865        assert!(chain.effects.iter().any(|e| e.id == id));
866        chain.remove_effect(id);
867        assert!(chain.effects.is_empty());
868    }
869
870    #[test]
871    fn test_spatial_gain() {
872        let src = SpatialAudioSource::new("test");
873        let g = src.gain_at_distance(1.0);
874        assert!((g - 1.0).abs() < 0.01);
875        let g2 = src.gain_at_distance(100.0);
876        assert!(g2 < 0.01);
877    }
878
879    #[test]
880    fn test_audio_editor() {
881        let mut editor = AudioEditor::new();
882        assert!(!editor.assets.is_empty());
883        assert!(!editor.spatial_sources.is_empty());
884        editor.update(0.016);
885    }
886}