1use std::sync::Arc;
8
9use crossbeam_channel::{Receiver, Sender};
10use phosphor_midi::message::MidiMessage;
11use phosphor_plugin::{MidiEvent, Plugin};
12
13use crate::clip::{ClipEvent, ClipSnapshot, MidiClip, RecordBuffer};
14use crate::engine::VuLevels;
15use crate::metronome::Metronome;
16use crate::project::{TrackHandle, TrackKind};
17use crate::transport::Transport;
18
19pub enum MixerCommand {
22 AddTrack {
23 kind: TrackKind,
24 handle: Arc<TrackHandle>,
25 },
26 SetInstrument {
27 track_id: usize,
28 instrument: Box<dyn Plugin + Send>,
29 },
30 RemoveTrack {
31 track_id: usize,
32 },
33 SetParameter {
34 track_id: usize,
35 param_index: usize,
36 value: f32,
37 },
38 CreateClip {
40 track_id: usize,
41 start_tick: i64,
42 length_ticks: i64,
43 },
44 UpdateClip {
46 track_id: usize,
47 clip_index: usize,
48 events: Vec<ClipEvent>,
49 },
50 UpdateClipPosition {
52 track_id: usize,
53 clip_index: usize,
54 start_tick: i64,
55 length_ticks: i64,
56 },
57 RemoveClip {
59 track_id: usize,
60 clip_index: usize,
61 },
62}
63
64const HEAVY_COMMAND: u32 = 16;
83
84fn command_cost(cmd: &MixerCommand) -> u32 {
102 match cmd {
103 MixerCommand::SetParameter { .. } | MixerCommand::UpdateClipPosition { .. } => 1,
104 MixerCommand::AddTrack { .. }
105 | MixerCommand::SetInstrument { .. }
106 | MixerCommand::RemoveTrack { .. }
107 | MixerCommand::CreateClip { .. }
108 | MixerCommand::UpdateClip { .. }
109 | MixerCommand::RemoveClip { .. } => HEAVY_COMMAND,
110 }
111}
112
113const COMMAND_BUDGET: u32 = 64;
131
132const TRACK_CAPACITY: usize = 64;
140
141const LIMITER_CEILING: f32 = 0.891;
149
150const LIMITER_RELEASE_SECONDS: f32 = 0.050;
157
158struct MasterLimiter {
182 gain: f32,
184 release_coeff: f32,
186}
187
188impl MasterLimiter {
189 fn new(sample_rate: u32) -> Self {
190 let sr = (sample_rate as f32).max(1.0);
191 Self {
192 gain: 1.0,
193 release_coeff: 1.0 - (-1.0 / (LIMITER_RELEASE_SECONDS * sr)).exp(),
194 }
195 }
196
197 fn reset(&mut self) {
198 self.gain = 1.0;
199 }
200
201 fn process(&mut self, output: &mut [f32]) {
206 let mut frames = output.chunks_exact_mut(2);
207 for frame in frames.by_ref() {
208 let l = if frame[0].is_finite() { frame[0] } else { 0.0 };
213 let r = if frame[1].is_finite() { frame[1] } else { 0.0 };
214
215 let peak = l.abs().max(r.abs());
216 let target = if peak > LIMITER_CEILING {
222 (LIMITER_CEILING / peak) * (1.0 - 2.0 * f32::EPSILON)
223 } else {
224 1.0
225 };
226
227 if target < self.gain {
228 self.gain = target;
229 } else {
230 self.gain += (target - self.gain) * self.release_coeff;
231 }
232
233 frame[0] = (l * self.gain).clamp(-1.0, 1.0);
239 frame[1] = (r * self.gain).clamp(-1.0, 1.0);
240 }
241
242 for tail in frames.into_remainder() {
247 let s = if tail.is_finite() { *tail } else { 0.0 };
248 *tail = (s * self.gain).clamp(-LIMITER_CEILING, LIMITER_CEILING);
249 }
250 }
251}
252
253pub struct AudioTrack {
256 pub id: usize,
257 pub kind: TrackKind,
258 pub handle: Arc<TrackHandle>,
259 pub instrument: Option<Box<dyn Plugin>>,
260 pub clips: Vec<MidiClip>,
262 record_buf: RecordBuffer,
264 was_recording: bool,
266 last_record_tick: i64,
268 last_playback_tick: i64,
270 buf_l: Vec<f32>,
271 buf_r: Vec<f32>,
272 plugin_events: Vec<MidiEvent>,
273}
274
275impl AudioTrack {
276 pub fn new(handle: Arc<TrackHandle>, max_buffer_size: usize) -> Self {
277 Self {
278 id: handle.id,
279 kind: handle.kind,
280 handle,
281 instrument: None,
282 clips: Vec::new(),
283 record_buf: RecordBuffer::new(),
284 was_recording: false,
285 last_record_tick: -1,
286 last_playback_tick: -1,
287 buf_l: vec![0.0; max_buffer_size],
288 buf_r: vec![0.0; max_buffer_size],
289 plugin_events: Vec::with_capacity(256),
290 }
291 }
292}
293
294pub struct Mixer {
297 tracks: Vec<AudioTrack>,
298 master_vu: Arc<VuLevels>,
299 command_rx: Receiver<MixerCommand>,
300 clip_tx: Sender<ClipSnapshot>,
301 metronome: Metronome,
302 sample_rate: u32,
303 max_buffer_size: usize,
304 scratch_l: Vec<f32>,
306 scratch_r: Vec<f32>,
307 live_events: Vec<MidiEvent>,
309 limiter: MasterLimiter,
311}
312
313impl Mixer {
314 pub fn new(
315 command_rx: Receiver<MixerCommand>,
316 master_vu: Arc<VuLevels>,
317 clip_tx: Sender<ClipSnapshot>,
318 sample_rate: u32,
319 max_buffer_size: usize,
320 ) -> Self {
321 Self {
322 tracks: Vec::with_capacity(TRACK_CAPACITY),
323 master_vu,
324 command_rx,
325 clip_tx,
326 metronome: Metronome::new(sample_rate as f64),
327 sample_rate,
328 max_buffer_size,
329 scratch_l: vec![0.0; max_buffer_size],
330 scratch_r: vec![0.0; max_buffer_size],
331 live_events: Vec::with_capacity(256),
332 limiter: MasterLimiter::new(sample_rate),
333 }
334 }
335
336 pub fn process(&mut self, output: &mut [f32], midi_messages: &[MidiMessage], transport: &Transport) {
338 let _ = self.drain_commands();
341
342 let num_frames = output.len() / 2;
343 let playing = transport.is_playing();
344 let recording = transport.is_recording();
345 let looping = transport.is_looping();
346 let current_tick = transport.position_ticks();
347 let bpm = transport.tempo_bpm();
348 let ticks_per_sample = (bpm * Transport::PPQ as f64) / (60.0 * self.sample_rate as f64);
349 let buffer_ticks = (num_frames as f64 * ticks_per_sample) as i64;
350 let loop_end = transport.loop_end();
351
352 self.live_events.clear();
354 for msg in midi_messages {
355 if let Some(ev) = midi_to_plugin_event(msg) {
356 self.live_events.push(ev);
357 }
358 }
359
360 let any_solo = self.tracks.iter().any(|t| t.handle.config.is_soloed());
361
362 let mut master_l = std::mem::take(&mut self.scratch_l);
365 let mut master_r = std::mem::take(&mut self.scratch_r);
366 let live_events = std::mem::take(&mut self.live_events);
367 if master_l.len() < num_frames {
368 master_l.resize(num_frames, 0.0);
369 master_r.resize(num_frames, 0.0);
370 }
371 master_l[..num_frames].fill(0.0);
372 master_r[..num_frames].fill(0.0);
373
374 let clip_tx = &self.clip_tx;
375
376 for track in &mut self.tracks {
377 if track.buf_l.len() < num_frames {
378 track.buf_l.resize(num_frames, 0.0);
379 track.buf_r.resize(num_frames, 0.0);
380 }
381 track.buf_l[..num_frames].fill(0.0);
382 track.buf_r[..num_frames].fill(0.0);
383 track.plugin_events.clear();
384
385 let is_midi_active = track.kind == TrackKind::Instrument
386 && track.handle.config.is_midi_active();
387 let is_armed = track.handle.config.is_armed();
388 let should_record = playing && recording && is_armed && is_midi_active;
389
390 if should_record && !track.was_recording {
392 let rec_start = if looping { transport.loop_start() } else { current_tick };
395 track.record_buf.start(rec_start);
396 tracing::debug!("rec start track={} tick={}", track.id, current_tick);
397 }
398
399 if should_record && track.was_recording && looping
401 && track.record_buf.is_active() && track.last_record_tick >= 0
402 && current_tick < track.last_record_tick
403 {
404 commit_recording(track, loop_end, clip_tx);
405 track.record_buf.start(transport.loop_start());
408 }
409 if should_record {
410 track.last_record_tick = current_tick;
411 }
412
413 if !should_record && track.was_recording {
415 commit_recording(track, current_tick, clip_tx);
416 }
417 track.was_recording = should_record;
418
419 if is_midi_active {
421 for ev in &live_events {
422 track.plugin_events.push(*ev);
423 if should_record {
424 let event_tick = current_tick
425 + (ev.sample_offset as f64 * ticks_per_sample) as i64;
426 track.record_buf.record(event_tick, ev.status, ev.data1, ev.data2);
427 }
428 }
429 }
430
431 if playing && !track.clips.is_empty() {
433 let from = current_tick;
434 let to = current_tick + buffer_ticks;
435
436 let just_wrapped = looping && track.last_playback_tick >= 0
439 && current_tick < track.last_playback_tick;
440 track.last_playback_tick = current_tick;
441
442 if just_wrapped {
443 let wrap_start = transport.loop_start();
445 for clip in &track.clips {
446 for (tick_offset, event) in clip.events_in_range(wrap_start, to) {
447 let sample_offset = (tick_offset as f64 / ticks_per_sample) as u32;
448 track.plugin_events.push(MidiEvent {
449 sample_offset: sample_offset.min(num_frames as u32 - 1),
450 status: event.status,
451 data1: event.data1,
452 data2: event.data2,
453 });
454 }
455 }
456 } else {
457 for clip in &track.clips {
458 for (tick_offset, event) in clip.events_in_range(from, to) {
459 let sample_offset = (tick_offset as f64 / ticks_per_sample) as u32;
460 track.plugin_events.push(MidiEvent {
461 sample_offset: sample_offset.min(num_frames as u32 - 1),
462 status: event.status,
463 data1: event.data1,
464 data2: event.data2,
465 });
466 }
467 }
468 }
469 track.plugin_events.sort_by_key(|e| e.sample_offset);
470 }
471
472 if playing {
474 track.last_record_tick = current_tick;
475 }
476
477 if let Some(ref mut instrument) = track.instrument {
479 let out_l = &mut track.buf_l[..num_frames];
480 let out_r = &mut track.buf_r[..num_frames];
481 let mut out_slices: [&mut [f32]; 2] = [out_l, out_r];
482 instrument.process(&[], &mut out_slices, &track.plugin_events);
483 }
484
485 let muted = track.handle.config.is_muted();
487 let soloed = track.handle.config.is_soloed();
488 let audible = !muted && (!any_solo || soloed);
489 let volume = track.handle.config.get_volume();
490
491 let mut peak_l = 0.0f32;
492 let mut peak_r = 0.0f32;
493 for i in 0..num_frames {
494 peak_l = peak_l.max(track.buf_l[i].abs());
495 peak_r = peak_r.max(track.buf_r[i].abs());
496 }
497
498 let (old_l, old_r) = track.handle.vu.get();
499 let decay = 0.85f32;
500 track.handle.vu.set(
501 if peak_l > old_l { peak_l } else { old_l * decay },
502 if peak_r > old_r { peak_r } else { old_r * decay },
503 );
504
505 if audible {
506 for i in 0..num_frames {
507 master_l[i] += track.buf_l[i] * volume;
508 master_r[i] += track.buf_r[i] * volume;
509 }
510 }
511 }
512
513 for i in 0..num_frames {
515 output[i * 2] = master_l[i];
516 output[i * 2 + 1] = master_r[i];
517 }
518
519 self.scratch_l = master_l;
521 self.scratch_r = master_r;
522 self.live_events = live_events;
523
524 self.metronome.process(output, transport);
526
527 self.limiter.process(output);
532
533 let mut mp_l = 0.0f32;
536 let mut mp_r = 0.0f32;
537 for i in 0..num_frames {
538 mp_l = mp_l.max(output[i * 2].abs());
539 mp_r = mp_r.max(output[i * 2 + 1].abs());
540 }
541
542 let (old_l, old_r) = self.master_vu.get();
543 let decay = 0.85f32;
544 self.master_vu.set(
545 if mp_l > old_l { mp_l } else { old_l * decay },
546 if mp_r > old_r { mp_r } else { old_r * decay },
547 );
548 }
549
550 pub fn reset_all(&mut self) {
551 let clip_tx = &self.clip_tx;
552 for track in &mut self.tracks {
553 if let Some(ref mut inst) = track.instrument {
554 inst.reset();
555 }
556 track.handle.vu.set(0.0, 0.0);
557 if track.record_buf.is_active() && track.was_recording {
559 let end_tick = track.last_record_tick.max(0);
560 commit_recording(track, end_tick, clip_tx);
561 } else if track.record_buf.is_active() {
562 track.record_buf.discard();
563 }
564 track.was_recording = false;
565 track.last_playback_tick = -1;
566 }
567 self.metronome.reset();
568 self.limiter.reset();
569 }
570
571 fn drain_commands(&mut self) -> u32 {
582 let mut spent = 0;
583 while spent < COMMAND_BUDGET {
584 let Ok(cmd) = self.command_rx.try_recv() else { break };
585 spent += command_cost(&cmd);
586 self.apply_command(cmd);
587 }
588 spent
589 }
590
591 fn apply_command(&mut self, cmd: MixerCommand) {
592 match cmd {
593 MixerCommand::AddTrack { kind: _, handle } => {
594 let track = AudioTrack::new(handle, self.max_buffer_size);
595 self.tracks.push(track);
596 }
597 MixerCommand::SetInstrument { track_id, mut instrument } => {
598 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
599 instrument.init(self.sample_rate as f64, self.max_buffer_size);
600 track.instrument = Some(instrument);
601 }
602 }
603 MixerCommand::RemoveTrack { track_id } => {
604 self.tracks.retain(|t| t.id != track_id);
605 }
606 MixerCommand::SetParameter { track_id, param_index, value } => {
607 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
608 if let Some(ref mut inst) = track.instrument {
609 inst.set_parameter(param_index, value);
610 }
611 }
612 }
613 MixerCommand::CreateClip { track_id, start_tick, length_ticks } => {
614 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
615 track.clips.push(MidiClip::new(start_tick, length_ticks, Vec::new()));
616 }
617 }
618 MixerCommand::UpdateClip { track_id, clip_index, events } => {
619 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
620 if let Some(clip) = track.clips.get_mut(clip_index) {
621 clip.events = events;
622 clip.events.sort_by_key(|e| e.tick);
623 }
624 }
625 }
626 MixerCommand::UpdateClipPosition { track_id, clip_index, start_tick, length_ticks } => {
627 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
628 if let Some(clip) = track.clips.get_mut(clip_index) {
629 clip.start_tick = start_tick;
630 clip.length_ticks = length_ticks;
631 }
632 }
633 }
634 MixerCommand::RemoveClip { track_id, clip_index } => {
635 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
636 if clip_index < track.clips.len() {
637 track.clips.remove(clip_index);
638 }
639 }
640 }
641 }
642 }
643}
644
645fn commit_recording(track: &mut AudioTrack, end_tick: i64, clip_tx: &Sender<ClipSnapshot>) {
647 if let Some(clip) = track.record_buf.commit(end_tick) {
648 let idx = track.clips.len();
649 tracing::debug!(
650 "rec commit track={}: {} events, ticks {}..{}",
651 track.id, clip.events.len(), clip.start_tick, clip.end_tick()
652 );
653 let snapshot = ClipSnapshot::from_clip(track.id, idx, &clip);
654 track.clips.push(clip);
655 let _ = clip_tx.send(snapshot);
656 }
657}
658
659pub fn midi_to_plugin_event(msg: &MidiMessage) -> Option<MidiEvent> {
660 use phosphor_midi::message::MidiMessageType;
661 match msg.message_type {
662 MidiMessageType::NoteOn { .. }
663 | MidiMessageType::NoteOff { .. }
664 | MidiMessageType::ControlChange { .. }
665 | MidiMessageType::PitchBend { .. } => Some(MidiEvent {
666 sample_offset: 0,
667 status: msg.raw[0],
668 data1: msg.raw[1],
669 data2: msg.raw[2],
670 }),
671 _ => None,
672 }
673}
674
675pub fn mixer_command_channel() -> (Sender<MixerCommand>, Receiver<MixerCommand>) {
676 crossbeam_channel::unbounded()
677}
678
679pub fn clip_snapshot_channel() -> (Sender<ClipSnapshot>, Receiver<ClipSnapshot>) {
681 crossbeam_channel::unbounded()
682}
683
684#[cfg(test)]
685mod tests {
686 use super::*;
687 use crate::project::TrackConfig;
688 use phosphor_dsp::synth::PhosphorSynth;
689 use phosphor_midi::message::{MidiMessage, MidiMessageType};
690
691 fn make_note_on(note: u8, vel: u8) -> MidiMessage {
692 MidiMessage {
693 timestamp: Some(0),
694 message_type: MidiMessageType::NoteOn { channel: 0, note, velocity: vel },
695 raw: [0x90, note, vel],
696 len: 3,
697 }
698 }
699
700 fn make_note_off(note: u8) -> MidiMessage {
701 MidiMessage {
702 timestamp: Some(0),
703 message_type: MidiMessageType::NoteOff { channel: 0, note, velocity: 0 },
704 raw: [0x80, note, 0],
705 len: 3,
706 }
707 }
708
709 fn setup_mixer() -> (Mixer, Sender<MixerCommand>, Receiver<ClipSnapshot>, Arc<Transport>) {
710 let (tx, rx) = mixer_command_channel();
711 let (clip_tx, clip_rx) = clip_snapshot_channel();
712 let master_vu = Arc::new(VuLevels::new());
713 let transport = Arc::new(Transport::new(120.0));
714 let mixer = Mixer::new(rx, master_vu, clip_tx, 44100, 256);
715 (mixer, tx, clip_rx, transport)
716 }
717
718 fn add_armed_synth(tx: &Sender<MixerCommand>, id: usize) -> Arc<TrackHandle> {
719 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
720 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
721 handle.config.armed.store(true, std::sync::atomic::Ordering::Relaxed);
722 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
723 tx.send(MixerCommand::SetInstrument { track_id: id, instrument: Box::new(PhosphorSynth::new()) }).unwrap();
724 handle
725 }
726
727 #[test]
728 fn mixer_empty_output() {
729 let (mut mixer, _tx, _clip_rx, transport) = setup_mixer();
730 let mut output = vec![0.0f32; 128];
731 mixer.process(&mut output, &[], &transport);
732 assert!(output.iter().all(|&s| s == 0.0));
733 }
734
735 #[test]
736 fn mixer_live_midi_produces_sound() {
737 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
738 let _handle = add_armed_synth(&tx, 0);
739 transport.play();
740
741 let midi = vec![make_note_on(60, 100)];
742 let mut output = vec![0.0f32; 512];
743 mixer.process(&mut output, &midi, &transport);
744
745 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
746 assert!(peak > 0.001, "Should produce sound, peak={peak}");
749 }
750
751 #[test]
752 fn mixer_records_midi_clip() {
753 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
754 let _handle = add_armed_synth(&tx, 0);
755 transport.play();
756 transport.toggle_record();
757
758 let midi = vec![make_note_on(60, 100)];
760 let mut output = vec![0.0f32; 512];
761 mixer.process(&mut output, &midi, &transport);
762
763 let midi = vec![make_note_off(60)];
765 mixer.process(&mut output, &midi, &transport);
766
767 transport.toggle_record();
769 mixer.process(&mut output, &[], &transport);
770
771 let snap = clip_rx.try_recv().expect("Should receive clip snapshot");
773 assert_eq!(snap.track_id, 0);
774 assert!(snap.event_count >= 2, "Should have note on + off, got {}", snap.event_count);
775 assert!(!snap.notes.is_empty(), "Should have parsed notes");
776 }
777
778 #[test]
779 fn mixer_plays_back_recorded_clip() {
780 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
781 let _handle = add_armed_synth(&tx, 0);
782 transport.play();
783 transport.toggle_record();
784
785 let midi = vec![make_note_on(60, 100)];
787 let mut output = vec![0.0f32; 512];
788 mixer.process(&mut output, &midi, &transport);
789
790 let midi = vec![make_note_off(60)];
791 mixer.process(&mut output, &midi, &transport);
792
793 transport.toggle_record();
795 mixer.process(&mut output, &[], &transport);
796
797 transport.stop();
799
800 transport.play();
802 output.fill(0.0);
803 mixer.process(&mut output, &[], &transport);
804
805 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
806 assert!(peak > 0.001, "Playback should produce sound, peak={peak}");
807 }
808
809 #[test]
810 fn mixer_mute_silences() {
811 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
812 let handle = add_armed_synth(&tx, 0);
813 handle.config.muted.store(true, std::sync::atomic::Ordering::Relaxed);
814 transport.play();
815
816 let midi = vec![make_note_on(60, 100)];
817 let mut output = vec![0.0f32; 512];
818 mixer.process(&mut output, &midi, &transport);
819
820 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
821 assert!(peak == 0.0, "Muted track should be silent, peak={peak}");
822 }
823
824 #[test]
825 fn mixer_no_record_when_not_armed() {
826 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
827 let handle = add_armed_synth(&tx, 0);
828 handle.config.armed.store(false, std::sync::atomic::Ordering::Relaxed);
829 transport.play();
830 transport.toggle_record();
831
832 let midi = vec![make_note_on(60, 100)];
833 let mut output = vec![0.0f32; 512];
834 mixer.process(&mut output, &midi, &transport);
835
836 transport.toggle_record();
837 mixer.process(&mut output, &[], &transport);
838
839 assert!(clip_rx.try_recv().is_err(), "Should not record when not armed");
840 }
841
842 #[test]
843 fn mixer_reset_commits_recording() {
844 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
845 let _handle = add_armed_synth(&tx, 0);
846 transport.play();
847 transport.toggle_record();
848
849 let midi = vec![make_note_on(60, 100)];
850 let mut output = vec![0.0f32; 512];
851 mixer.process(&mut output, &midi, &transport);
852
853 mixer.reset_all();
854
855 assert!(clip_rx.try_recv().is_ok(), "Reset should commit active recording");
857 }
858
859 #[test]
860 fn end_to_end_record_and_playback() {
861 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
864 let _handle = add_armed_synth(&tx, 0);
865 let sr = 44100u32;
866 let buf_frames = 256;
867 let buf_samples = buf_frames * 2; transport.toggle_record();
871 transport.play();
872
873 let mut output = vec![0.0f32; buf_samples];
875 for _ in 0..4 {
876 mixer.process(&mut output, &[], &transport);
877 transport.advance(buf_frames as u32, sr);
878 }
879
880 let midi = vec![make_note_on(60, 100)];
882 mixer.process(&mut output, &midi, &transport);
883 let peak_during = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
884 assert!(peak_during > 0.001, "Should hear note during recording (monitoring)");
885 transport.advance(buf_frames as u32, sr);
886
887 for _ in 0..8 {
889 output.fill(0.0);
890 mixer.process(&mut output, &[], &transport);
891 transport.advance(buf_frames as u32, sr);
892 }
893
894 let midi = vec![make_note_off(60)];
896 mixer.process(&mut output, &midi, &transport);
897 transport.advance(buf_frames as u32, sr);
898
899 for _ in 0..4 {
901 output.fill(0.0);
902 mixer.process(&mut output, &[], &transport);
903 transport.advance(buf_frames as u32, sr);
904 }
905
906 transport.toggle_record();
908 mixer.process(&mut output, &[], &transport);
909 transport.advance(buf_frames as u32, sr);
910
911 let snap = clip_rx.try_recv().expect("Should receive clip snapshot after stopping record");
913 assert!(snap.event_count >= 2, "Clip should have note on + off");
914 assert!(!snap.notes.is_empty(), "Clip should have parsed notes");
915
916 transport.stop();
918
919 transport.play();
921
922 for _ in 0..4 {
925 output.fill(0.0);
926 mixer.process(&mut output, &[], &transport);
927 transport.advance(buf_frames as u32, sr);
928 }
929
930 output.fill(0.0);
932 mixer.process(&mut output, &[], &transport);
933 let peak_playback = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
934 assert!(peak_playback > 0.001, "Playback should produce sound at the recorded position, peak={peak_playback}");
935 }
936
937 #[test]
938 fn loop_record_commits_on_wrap() {
939 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
940 let _handle = add_armed_synth(&tx, 0);
941 let sr = 44100u32;
942 let buf_frames = 256u32;
943
944 transport.set_loop_bars(1, 1);
946 transport.start_loop_record();
947
948 let mut output = vec![0.0f32; buf_frames as usize * 2];
949
950 let midi = vec![make_note_on(60, 100)];
952 mixer.process(&mut output, &midi, &transport);
953 transport.advance(buf_frames, sr);
954
955 for _ in 0..5 {
957 mixer.process(&mut output, &[], &transport);
958 transport.advance(buf_frames, sr);
959 }
960 let midi = vec![make_note_off(60)];
961 mixer.process(&mut output, &midi, &transport);
962 transport.advance(buf_frames, sr);
963
964 for _ in 0..400 {
967 mixer.process(&mut output, &[], &transport);
968 transport.advance(buf_frames, sr);
969
970 if let Ok(snap) = clip_rx.try_recv() {
971 assert!(snap.event_count >= 2, "Clip should have events, got {}", snap.event_count);
972 assert!(!snap.notes.is_empty(), "Clip should have notes");
973 transport.stop_loop_record();
975 return;
976 }
977 }
978
979 panic!("Recording should have committed when the loop wrapped");
980 }
981
982 #[test]
983 fn loop_playback_after_record() {
984 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
985 let _handle = add_armed_synth(&tx, 0);
986 let sr = 44100u32;
987 let bf = 256u32;
988
989 transport.set_loop_bars(1, 1);
991 transport.start_loop_record();
992
993 let mut output = vec![0.0f32; bf as usize * 2];
994
995 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
997 transport.advance(bf, sr);
998 for _ in 0..3 {
999 mixer.process(&mut output, &[], &transport);
1000 transport.advance(bf, sr);
1001 }
1002 mixer.process(&mut output, &[make_note_off(60)], &transport);
1003 transport.advance(bf, sr);
1004
1005 for _ in 0..200 {
1007 mixer.process(&mut output, &[], &transport);
1008 transport.advance(bf, sr);
1009 if clip_rx.try_recv().is_ok() { break; }
1010 }
1011
1012 transport.stop_loop_record();
1014 transport.set_position(0);
1015
1016 transport.toggle_loop(); transport.play();
1019
1020 output.fill(0.0);
1021 mixer.process(&mut output, &[], &transport);
1022 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1023 assert!(peak > 0.001, "Should hear playback, peak={peak}");
1024 }
1025
1026 const WORST_CALLBACK: u32 = COMMAND_BUDGET - 1 + HEAVY_COMMAND;
1032
1033 #[derive(Clone)]
1040 struct ParamLog(Arc<std::sync::Mutex<Vec<(usize, f32)>>>);
1041
1042 impl ParamLog {
1043 fn new() -> Self {
1044 Self(Arc::new(std::sync::Mutex::new(Vec::new())))
1045 }
1046 fn seen(&self) -> Vec<(usize, f32)> {
1047 self.0.lock().unwrap().clone()
1048 }
1049 }
1050
1051 impl Plugin for ParamLog {
1052 fn info(&self) -> phosphor_plugin::PluginInfo {
1053 phosphor_plugin::PluginInfo {
1054 name: "ParamLog".into(),
1055 version: "0".into(),
1056 author: "test".into(),
1057 category: phosphor_plugin::PluginCategory::Instrument,
1058 }
1059 }
1060 fn init(&mut self, _sample_rate: f64, _max_buffer_size: usize) {}
1061 fn process(&mut self, _inputs: &[&[f32]], _outputs: &mut [&mut [f32]], _midi: &[MidiEvent]) {}
1062 fn parameter_count(&self) -> usize { 8 }
1063 fn parameter_info(&self, _index: usize) -> Option<phosphor_plugin::ParameterInfo> { None }
1064 fn get_parameter(&self, _index: usize) -> f32 { 0.0 }
1065 fn set_parameter(&mut self, index: usize, value: f32) {
1066 self.0.lock().unwrap().push((index, value));
1067 }
1068 fn reset(&mut self) {}
1069 }
1070
1071 fn add_logging_track(mixer: &mut Mixer, tx: &Sender<MixerCommand>, id: usize) -> ParamLog {
1073 let log = ParamLog::new();
1074 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1075 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1076 tx.send(MixerCommand::SetInstrument {
1077 track_id: id,
1078 instrument: Box::new(log.clone()),
1079 }).unwrap();
1080 mixer.drain_commands();
1081 log
1082 }
1083
1084 #[test]
1088 fn one_callback_applies_a_bounded_amount_of_work() {
1089 let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1090 let log = add_logging_track(&mut mixer, &tx, 0);
1091
1092 for i in 0..500 {
1093 tx.send(MixerCommand::SetParameter {
1094 track_id: 0,
1095 param_index: i % 8,
1096 value: i as f32,
1097 }).unwrap();
1098 }
1099
1100 let spent = mixer.drain_commands();
1101 assert!(
1102 spent <= WORST_CALLBACK,
1103 "one callback spent {spent} units, over the {WORST_CALLBACK} bound"
1104 );
1105 assert_eq!(
1106 log.seen().len(),
1107 COMMAND_BUDGET as usize,
1108 "a parameter costs one unit, so a full budget is exactly that many"
1109 );
1110 assert!(!mixer.command_rx.is_empty(), "the rest has to still be queued");
1111 }
1112
1113 #[test]
1116 fn nothing_is_lost_or_reordered_across_callbacks() {
1117 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1118 let log = add_logging_track(&mut mixer, &tx, 0);
1119
1120 let sent: Vec<(usize, f32)> = (0..500).map(|i| (i % 8, i as f32)).collect();
1121 for &(param_index, value) in &sent {
1122 tx.send(MixerCommand::SetParameter { track_id: 0, param_index, value }).unwrap();
1123 }
1124
1125 let mut output = vec![0.0f32; 128];
1129 let mut callbacks = 0;
1130 while !mixer.command_rx.is_empty() {
1131 mixer.process(&mut output, &[], &transport);
1132 callbacks += 1;
1133 assert!(callbacks < 100, "the drain is not making progress");
1134 }
1135 assert!(
1136 callbacks >= 500 / COMMAND_BUDGET as usize,
1137 "500 commands went through in {callbacks} callbacks, so the budget did not hold"
1138 );
1139 assert_eq!(log.seen(), sent, "the audio thread saw a different sequence");
1140 }
1141
1142 #[test]
1148 fn a_track_and_its_instrument_survive_a_budget_boundary() {
1149 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1150 let log = ParamLog::new();
1151
1152 for _ in 0..COMMAND_BUDGET {
1155 tx.send(MixerCommand::SetParameter { track_id: 99, param_index: 0, value: 0.0 })
1156 .unwrap();
1157 }
1158 let handle = Arc::new(TrackHandle::new(7, TrackKind::Instrument));
1159 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1160 tx.send(MixerCommand::SetInstrument {
1161 track_id: 7,
1162 instrument: Box::new(log.clone()),
1163 }).unwrap();
1164 tx.send(MixerCommand::SetParameter { track_id: 7, param_index: 3, value: 0.5 }).unwrap();
1165
1166 let mut output = vec![0.0f32; 128];
1167 mixer.process(&mut output, &[], &transport);
1168 assert!(mixer.tracks.is_empty(), "the budget did not stop at the parameters");
1169
1170 while !mixer.command_rx.is_empty() {
1171 mixer.process(&mut output, &[], &transport);
1172 }
1173 assert_eq!(mixer.tracks.len(), 1);
1174 assert!(mixer.tracks[0].instrument.is_some(), "the instrument never arrived");
1175 assert_eq!(
1176 log.seen(),
1177 vec![(3, 0.5)],
1178 "the parameter that follows the instrument did not reach it"
1179 );
1180 }
1181
1182 #[test]
1187 fn an_instrument_load_costs_more_than_a_parameter() {
1188 let param = MixerCommand::SetParameter { track_id: 0, param_index: 0, value: 0.0 };
1189 let load = MixerCommand::SetInstrument {
1190 track_id: 0,
1191 instrument: Box::new(FixedOutput(0.0)),
1192 };
1193 assert!(command_cost(&load) > command_cost(¶m));
1194
1195 let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1197 for id in 0..8 {
1198 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1199 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1200 }
1201 while !mixer.command_rx.is_empty() {
1202 mixer.drain_commands();
1203 }
1204 for id in 0..8 {
1205 tx.send(MixerCommand::SetInstrument {
1206 track_id: id,
1207 instrument: Box::new(FixedOutput(0.25)),
1208 }).unwrap();
1209 }
1210 mixer.drain_commands();
1211 let loaded = mixer.tracks.iter().filter(|t| t.instrument.is_some()).count();
1212 assert_eq!(loaded, (COMMAND_BUDGET / HEAVY_COMMAND) as usize);
1213 }
1214
1215 #[test]
1219 fn adding_tracks_does_not_grow_the_track_list() {
1220 let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1221 let capacity = mixer.tracks.capacity();
1222 assert!(capacity >= TRACK_CAPACITY);
1223
1224 for id in 0..TRACK_CAPACITY {
1225 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1226 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1227 }
1228 while !mixer.command_rx.is_empty() {
1229 mixer.drain_commands();
1230 }
1231 assert_eq!(mixer.tracks.len(), TRACK_CAPACITY);
1232 assert_eq!(
1233 mixer.tracks.capacity(), capacity,
1234 "the track list reallocated on the audio thread"
1235 );
1236 }
1237
1238 struct FixedOutput(f32);
1243
1244 impl Plugin for FixedOutput {
1245 fn info(&self) -> phosphor_plugin::PluginInfo {
1246 phosphor_plugin::PluginInfo {
1247 name: "Fixed".into(),
1248 version: "0".into(),
1249 author: "test".into(),
1250 category: phosphor_plugin::PluginCategory::Instrument,
1251 }
1252 }
1253 fn init(&mut self, _sample_rate: f64, _max_buffer_size: usize) {}
1254 fn process(&mut self, _inputs: &[&[f32]], outputs: &mut [&mut [f32]], _midi: &[MidiEvent]) {
1255 for ch in outputs.iter_mut() {
1256 ch.fill(self.0);
1257 }
1258 }
1259 fn parameter_count(&self) -> usize { 0 }
1260 fn parameter_info(&self, _index: usize) -> Option<phosphor_plugin::ParameterInfo> { None }
1261 fn get_parameter(&self, _index: usize) -> f32 { 0.0 }
1262 fn set_parameter(&mut self, _index: usize, _value: f32) {}
1263 fn reset(&mut self) {}
1264 }
1265
1266 fn add_fixed_track(tx: &Sender<MixerCommand>, id: usize, value: f32) -> Arc<TrackHandle> {
1267 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1268 handle.config.set_volume(1.0);
1269 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
1270 tx.send(MixerCommand::SetInstrument {
1271 track_id: id,
1272 instrument: Box::new(FixedOutput(value)),
1273 }).unwrap();
1274 handle
1275 }
1276
1277 #[test]
1280 fn master_limiter_bounds_many_loud_tracks() {
1281 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1282 for id in 0..6 {
1283 add_fixed_track(&tx, id, 0.75);
1284 }
1285 transport.play();
1286
1287 let mut output = vec![0.0f32; 512];
1288 for _ in 0..8 {
1289 mixer.process(&mut output, &[], &transport);
1290 for (i, &s) in output.iter().enumerate() {
1291 assert!(s.is_finite(), "non-finite sample at {i}");
1292 assert!(s.abs() <= 1.0, "sample {i} left the mixer at {s}");
1293 }
1294 }
1295
1296 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1298 assert!(peak > 0.8, "limiter over-attenuated, peak={peak}");
1299 }
1300
1301 #[test]
1305 fn non_finite_track_output_becomes_silence() {
1306 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1307 add_fixed_track(&tx, 0, f32::NAN);
1308 transport.play();
1309
1310 let mut output = vec![0.0f32; 512];
1311 mixer.process(&mut output, &[], &transport);
1312 assert!(output.iter().all(|s| *s == 0.0), "NaN track should render as silence");
1313
1314 tx.send(MixerCommand::RemoveTrack { track_id: 0 }).unwrap();
1317 add_fixed_track(&tx, 1, 0.5);
1318 mixer.process(&mut output, &[], &transport);
1319 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1320 assert!((peak - 0.5).abs() < 1.0e-6, "mixer did not recover, peak={peak}");
1321 }
1322
1323 #[test]
1324 fn infinite_track_output_becomes_silence() {
1325 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1326 add_fixed_track(&tx, 0, f32::INFINITY);
1327 transport.play();
1328
1329 let mut output = vec![0.0f32; 512];
1330 mixer.process(&mut output, &[], &transport);
1331 assert!(output.iter().all(|s| *s == 0.0), "infinite track should render as silence");
1332 }
1333
1334 #[test]
1338 fn limiter_is_bit_identical_below_the_ceiling() {
1339 let mut limiter = MasterLimiter::new(44_100);
1340
1341 let mut input: Vec<f32> = Vec::new();
1343 for i in 0..20_000u32 {
1344 let phase = i as f32 * 0.01;
1345 let amp = LIMITER_CEILING * (i as f32 / 20_000.0);
1346 input.push(phase.sin() * amp);
1347 input.push(phase.cos() * amp);
1348 }
1349 input.push(LIMITER_CEILING);
1350 input.push(-LIMITER_CEILING);
1351 input.push(0.0);
1352 input.push(-0.0);
1353 input.push(f32::MIN_POSITIVE);
1354 input.push(-f32::MIN_POSITIVE);
1355
1356 let mut output = input.clone();
1357 limiter.process(&mut output);
1358
1359 for (i, (a, b)) in input.iter().zip(output.iter()).enumerate() {
1360 assert_eq!(a.to_bits(), b.to_bits(), "limiter altered sample {i}: {a} -> {b}");
1361 }
1362 }
1363
1364 #[test]
1367 fn limiter_holds_the_ceiling_under_abuse() {
1368 let mut limiter = MasterLimiter::new(44_100);
1369 for amplitude in [1.0f32, 2.0, 10.0, 1.0e3, 1.0e6, 1.0e30] {
1370 let mut buf: Vec<f32> = (0..4_096)
1371 .map(|i| (i as f32 * 0.05).sin() * amplitude)
1372 .collect();
1373 limiter.process(&mut buf);
1374 for (i, &s) in buf.iter().enumerate() {
1375 assert!(s.is_finite(), "amplitude {amplitude}: sample {i} is {s}");
1376 assert!(
1377 s.abs() <= LIMITER_CEILING,
1378 "amplitude {amplitude}: sample {i} reached {s}, above the ceiling"
1379 );
1380 }
1381 }
1382 }
1383
1384 #[test]
1388 fn limiter_attack_has_no_overshoot() {
1389 let mut limiter = MasterLimiter::new(44_100);
1390 let mut buf = vec![0.0f32; 64];
1391 limiter.process(&mut buf);
1392 let mut step = vec![4.0f32; 64];
1393 limiter.process(&mut step);
1394 assert!(
1395 step[0].abs() <= LIMITER_CEILING,
1396 "first sample of the step overshot to {}",
1397 step[0]
1398 );
1399 }
1400
1401 #[test]
1404 fn limiter_release_is_gradual() {
1405 let mut limiter = MasterLimiter::new(44_100);
1406 let mut loud = vec![4.0f32; 64];
1407 limiter.process(&mut loud);
1408 let reduced = limiter.gain;
1409 assert!(reduced < 0.5, "limiter did not engage, gain={reduced}");
1410
1411 let mut quiet = vec![0.1f32; 441 * 2];
1414 limiter.process(&mut quiet);
1415 assert!(limiter.gain > reduced, "gain did not recover at all");
1416 assert!(
1417 limiter.gain < 1.0,
1418 "gain snapped back to unity within 10 ms, which is a click"
1419 );
1420
1421 let mut long = vec![0.1f32; 22_050 * 2];
1423 limiter.process(&mut long);
1424 assert!(
1425 (limiter.gain - 1.0).abs() < 1.0e-4,
1426 "gain never returned to unity: {}",
1427 limiter.gain
1428 );
1429 }
1430
1431 #[test]
1434 fn limiter_does_not_shift_the_stereo_image() {
1435 let mut limiter = MasterLimiter::new(44_100);
1436 let mut buf: Vec<f32> = Vec::new();
1438 for i in 0..1_024 {
1439 let phase = i as f32 * 0.05;
1440 buf.push(phase.sin() * 3.0);
1441 buf.push(phase.sin() * 1.5);
1442 }
1443 limiter.process(&mut buf);
1444 for frame in buf.chunks_exact(2) {
1445 if frame[1].abs() > 1.0e-4 {
1446 let ratio = frame[0] / frame[1];
1447 assert!(
1448 (ratio - 2.0).abs() < 1.0e-3,
1449 "channel balance moved: L/R = {ratio}"
1450 );
1451 }
1452 }
1453 }
1454
1455 fn loudest_dx7_voice() -> phosphor_dsp::dx7::Dx7Synth {
1462 use phosphor_dsp::dx7;
1463 let mut synth = dx7::Dx7Synth::new();
1464 let (bank, patch) = dx7::voice_knobs(147);
1465 synth.set_parameter(dx7::P_BANK, bank);
1466 synth.set_parameter(dx7::P_PATCH, patch);
1467 debug_assert_eq!(dx7::voice_name(147), "TIMPANI");
1468 synth
1469 }
1470
1471 #[test]
1475 fn master_limiter_bounds_four_loud_instrument_tracks() {
1476 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1477 for id in 0..4 {
1478 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1479 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1480 handle.config.set_volume(1.0);
1481 let synth = loudest_dx7_voice();
1482 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1483 tx.send(MixerCommand::SetInstrument {
1484 track_id: id,
1485 instrument: Box::new(synth),
1486 }).unwrap();
1487 }
1488 transport.play();
1489
1490 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1491 .iter()
1492 .map(|¬e| make_note_on(note, 127))
1493 .collect();
1494
1495 let mut output = vec![0.0f32; 512];
1496 let mut peak = 0.0f32;
1497 for block in 0..200 {
1498 output.fill(0.0);
1499 if block == 0 {
1500 mixer.process(&mut output, &chord, &transport);
1501 } else {
1502 mixer.process(&mut output, &[], &transport);
1503 }
1504 for (i, &s) in output.iter().enumerate() {
1505 assert!(s.is_finite(), "block {block} sample {i} is {s}");
1506 assert!(s.abs() <= 1.0, "block {block} sample {i} left the mixer at {s}");
1507 peak = peak.max(s.abs());
1508 }
1509 }
1510 assert!(peak > 0.5, "four loud tracks should be loud, peak={peak}");
1511 }
1512
1513 #[test]
1519 fn limiter_idle_for_the_worst_single_track() {
1520 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1521 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1522 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1523 handle.config.set_volume(1.0);
1524 let synth = loudest_dx7_voice();
1525 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1526 tx.send(MixerCommand::SetInstrument { track_id: 0, instrument: Box::new(synth) }).unwrap();
1527 transport.play();
1528
1529 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1530 .iter()
1531 .map(|¬e| make_note_on(note, 127))
1532 .collect();
1533
1534 let mut output = vec![0.0f32; 512];
1535 let mut peak = 0.0f32;
1536 for block in 0..200 {
1537 output.fill(0.0);
1538 if block == 0 {
1539 mixer.process(&mut output, &chord, &transport);
1540 } else {
1541 mixer.process(&mut output, &[], &transport);
1542 }
1543 peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0f32, f32::max));
1544 assert_eq!(
1545 mixer.limiter.gain, 1.0,
1546 "limiter engaged at block {block}, peak {peak}"
1547 );
1548 }
1549 assert!(peak > 0.3, "expected a loud chord, peak={peak}");
1550 }
1551
1552 fn worst_track_through_the_mixer(volume: f32) -> (f32, f32) {
1558 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1559 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1560 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1561 handle.config.set_volume(volume);
1562 let synth = loudest_dx7_voice();
1563 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1564 tx.send(MixerCommand::SetInstrument { track_id: 0, instrument: Box::new(synth) }).unwrap();
1565 transport.play();
1566
1567 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1568 .iter()
1569 .map(|¬e| make_note_on(note, 127))
1570 .collect();
1571
1572 let mut output = vec![0.0f32; 512];
1573 let mut peak = 0.0f32;
1574 let mut min_gain = 1.0f32;
1575 for block in 0..200 {
1576 output.fill(0.0);
1577 if block == 0 {
1578 mixer.process(&mut output, &chord, &transport);
1579 } else {
1580 mixer.process(&mut output, &[], &transport);
1581 }
1582 for &s in output.iter() {
1583 assert!(s.is_finite(), "block {block}: non-finite sample");
1584 assert!(s.abs() <= 1.0, "block {block}: sample left the mixer at {s}");
1585 peak = peak.max(s.abs());
1586 }
1587 min_gain = min_gain.min(mixer.limiter.gain);
1588 }
1589 (peak, min_gain)
1590 }
1591
1592 #[test]
1600 fn fader_below_unity_never_engages_the_limiter() {
1601 for volume in [
1602 0.25,
1603 TrackConfig::DEFAULT_VOLUME,
1604 TrackConfig::UNITY_VOLUME,
1605 ] {
1606 let (peak, min_gain) = worst_track_through_the_mixer(volume);
1607 assert_eq!(
1608 min_gain, 1.0,
1609 "limiter reduced by {:.2} dB at fader {volume} (peak {peak:.4})",
1610 20.0 * min_gain.log10()
1611 );
1612 }
1613 }
1614
1615 #[test]
1621 fn fader_makeup_gain_is_bounded_not_wasted() {
1622 let (unity_peak, _) = worst_track_through_the_mixer(TrackConfig::UNITY_VOLUME);
1623 let (max_peak, min_gain) = worst_track_through_the_mixer(TrackConfig::MAX_VOLUME);
1624
1625 assert!(
1626 max_peak <= LIMITER_CEILING,
1627 "fader at maximum let {max_peak:.4} through, above the ceiling"
1628 );
1629 assert!(
1630 max_peak >= unity_peak,
1631 "turning the fader up made the track quieter: {unity_peak:.4} -> {max_peak:.4}"
1632 );
1633 let reduction_db = -20.0 * min_gain.log10();
1636 let boost_db = 20.0 * (TrackConfig::MAX_VOLUME / TrackConfig::UNITY_VOLUME).log10();
1637 assert!(
1638 reduction_db <= boost_db,
1639 "limiter took {reduction_db:.2} dB off a {boost_db:.2} dB boost"
1640 );
1641 }
1642
1643 #[test]
1655 fn metronome_click_sits_with_the_music() {
1656 use phosphor_dsp::dx7;
1657
1658 fn render(with_track: bool, metronome: bool) -> f32 {
1659 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1660 let chord: Vec<MidiMessage> = if with_track {
1661 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1662 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1663 tx.send(MixerCommand::AddTrack {
1664 kind: TrackKind::Instrument,
1665 handle,
1666 })
1667 .unwrap();
1668 tx.send(MixerCommand::SetInstrument {
1669 track_id: 0,
1670 instrument: Box::new(dx7::Dx7Synth::new()),
1671 })
1672 .unwrap();
1673 [60u8, 64, 67].iter().map(|&n| make_note_on(n, 100)).collect()
1674 } else {
1675 Vec::new()
1676 };
1677 if metronome {
1678 transport.toggle_metronome();
1679 }
1680 transport.play();
1681
1682 let mut output = vec![0.0f32; 512];
1683 let mut peak = 0.0f32;
1684 for block in 0..200 {
1685 output.fill(0.0);
1686 if block == 0 {
1687 mixer.process(&mut output, &chord, &transport);
1688 } else {
1689 mixer.process(&mut output, &[], &transport);
1690 }
1691 peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0f32, f32::max));
1692 transport.advance(256, 44_100);
1693 }
1694 peak
1695 }
1696
1697 let music = render(true, false);
1698 let click = render(false, true);
1699 assert!(music > 0.0 && click > 0.0, "music {music}, click {click}");
1700
1701 let relative_db = 20.0 * (click / music).log10();
1702 assert!(
1703 (-12.0..=0.0).contains(&relative_db),
1704 "the click is {relative_db:.1} dB against a triad (click {click:.4}, \
1705 music {music:.4}); it has to be audible over the music without \
1706 being the loudest thing in the mix"
1707 );
1708 }
1709
1710 #[test]
1714 fn fader_scales_the_track() {
1715 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1716 let handle = add_fixed_track(&tx, 0, 0.25);
1717 transport.play();
1718
1719 let mut output = vec![0.0f32; 512];
1720 for (volume, expected) in [(0.0f32, 0.0f32), (0.5, 0.125), (1.0, 0.25), (2.0, 0.5)] {
1721 handle.config.set_volume(volume);
1722 output.fill(0.0);
1723 mixer.process(&mut output, &[], &transport);
1724 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1725 assert!(
1726 (peak - expected).abs() < 1.0e-6,
1727 "fader at {volume} gave {peak}, expected {expected}"
1728 );
1729 }
1730 }
1731}