1
2use glam::{Vec2, Vec3};
5use std::collections::HashMap;
6
7#[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, }
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#[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 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 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 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#[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#[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 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#[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, 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#[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, pub cone_outer_angle: f32, 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#[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 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 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#[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 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#[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}