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 {
373 master_l.resize(num_frames, 0.0);
374 master_r.resize(num_frames, 0.0);
375 }
376 master_l[..num_frames].fill(0.0);
377 master_r[..num_frames].fill(0.0);
378
379 let clip_tx = &self.clip_tx;
380
381 for track in &mut self.tracks {
382 if track.buf_l.len() < num_frames {
383 track.buf_l.resize(num_frames, 0.0);
384 track.buf_r.resize(num_frames, 0.0);
385 }
386 track.buf_l[..num_frames].fill(0.0);
387 track.buf_r[..num_frames].fill(0.0);
388 track.plugin_events.clear();
389
390 let is_midi_active = track.kind == TrackKind::Instrument
391 && track.handle.config.is_midi_active();
392 let is_armed = track.handle.config.is_armed();
393 let should_record = playing && recording && is_armed && is_midi_active;
394
395 if should_record && !track.was_recording {
397 let rec_start = if looping { transport.loop_start() } else { current_tick };
400 track.record_buf.start(rec_start);
401 tracing::debug!("rec start track={} tick={}", track.id, current_tick);
402 }
403
404 if should_record && track.was_recording && looping
406 && track.record_buf.is_active() && track.last_record_tick >= 0
407 && current_tick < track.last_record_tick
408 {
409 commit_recording(track, loop_end, clip_tx);
410 track.record_buf.start(transport.loop_start());
413 }
414 if should_record {
415 track.last_record_tick = current_tick;
416 }
417
418 if !should_record && track.was_recording {
420 commit_recording(track, current_tick, clip_tx);
421 }
422 track.was_recording = should_record;
423
424 if is_midi_active {
426 for ev in &live_events {
427 track.plugin_events.push(*ev);
428 if should_record {
429 let event_tick = current_tick
430 + (ev.sample_offset as f64 * ticks_per_sample) as i64;
431 track.record_buf.record(event_tick, ev.status, ev.data1, ev.data2);
432 }
433 }
434 }
435
436 if playing && !track.clips.is_empty() {
438 let from = current_tick;
439 let to = current_tick + buffer_ticks;
440
441 let just_wrapped = looping && track.last_playback_tick >= 0
444 && current_tick < track.last_playback_tick;
445 track.last_playback_tick = current_tick;
446
447 if just_wrapped {
448 let wrap_start = transport.loop_start();
450 for clip in &track.clips {
451 for (tick_offset, event) in clip.events_in_range(wrap_start, to) {
452 let sample_offset = (tick_offset as f64 / ticks_per_sample) as u32;
453 track.plugin_events.push(MidiEvent {
454 sample_offset: sample_offset.min(num_frames as u32 - 1),
455 status: event.status,
456 data1: event.data1,
457 data2: event.data2,
458 });
459 }
460 }
461 } else {
462 for clip in &track.clips {
463 for (tick_offset, event) in clip.events_in_range(from, to) {
464 let sample_offset = (tick_offset as f64 / ticks_per_sample) as u32;
465 track.plugin_events.push(MidiEvent {
466 sample_offset: sample_offset.min(num_frames as u32 - 1),
467 status: event.status,
468 data1: event.data1,
469 data2: event.data2,
470 });
471 }
472 }
473 }
474 track.plugin_events.sort_by_key(|e| e.sample_offset);
475 }
476
477 if playing {
479 track.last_record_tick = current_tick;
480 }
481
482 if let Some(ref mut instrument) = track.instrument {
484 let out_l = &mut track.buf_l[..num_frames];
485 let out_r = &mut track.buf_r[..num_frames];
486 let mut out_slices: [&mut [f32]; 2] = [out_l, out_r];
487 instrument.process(&[], &mut out_slices, &track.plugin_events);
488 }
489
490 let muted = track.handle.config.is_muted();
492 let soloed = track.handle.config.is_soloed();
493 let audible = !muted && (!any_solo || soloed);
494 let volume = track.handle.config.get_volume();
495
496 let mut peak_l = 0.0f32;
497 let mut peak_r = 0.0f32;
498 for i in 0..num_frames {
499 peak_l = peak_l.max(track.buf_l[i].abs());
500 peak_r = peak_r.max(track.buf_r[i].abs());
501 }
502
503 let (old_l, old_r) = track.handle.vu.get();
504 let decay = 0.85f32;
505 track.handle.vu.set(
506 if peak_l > old_l { peak_l } else { old_l * decay },
507 if peak_r > old_r { peak_r } else { old_r * decay },
508 );
509
510 if audible {
511 for i in 0..num_frames {
512 master_l[i] += track.buf_l[i] * volume;
513 master_r[i] += track.buf_r[i] * volume;
514 }
515 }
516 }
517
518 for i in 0..num_frames {
520 output[i * 2] = master_l[i];
521 output[i * 2 + 1] = master_r[i];
522 }
523
524 self.scratch_l = master_l;
526 self.scratch_r = master_r;
527 self.live_events = live_events;
528
529 self.metronome.process(output, transport);
531
532 self.limiter.process(output);
537
538 let mut mp_l = 0.0f32;
541 let mut mp_r = 0.0f32;
542 for i in 0..num_frames {
543 mp_l = mp_l.max(output[i * 2].abs());
544 mp_r = mp_r.max(output[i * 2 + 1].abs());
545 }
546
547 let (old_l, old_r) = self.master_vu.get();
548 let decay = 0.85f32;
549 self.master_vu.set(
550 if mp_l > old_l { mp_l } else { old_l * decay },
551 if mp_r > old_r { mp_r } else { old_r * decay },
552 );
553 }
554
555 pub fn reset_all(&mut self) {
556 let clip_tx = &self.clip_tx;
557 for track in &mut self.tracks {
558 if let Some(ref mut inst) = track.instrument {
559 inst.reset();
560 }
561 track.handle.vu.set(0.0, 0.0);
562 if track.record_buf.is_active() && track.was_recording {
564 let end_tick = track.last_record_tick.max(0);
565 commit_recording(track, end_tick, clip_tx);
566 } else if track.record_buf.is_active() {
567 track.record_buf.discard();
568 }
569 track.was_recording = false;
570 track.last_playback_tick = -1;
571 }
572 self.metronome.reset();
573 self.limiter.reset();
574 }
575
576 fn drain_commands(&mut self) -> u32 {
587 let mut spent = 0;
588 while spent < COMMAND_BUDGET {
589 let Ok(cmd) = self.command_rx.try_recv() else { break };
590 spent += command_cost(&cmd);
591 self.apply_command(cmd);
592 }
593 spent
594 }
595
596 fn apply_command(&mut self, cmd: MixerCommand) {
597 match cmd {
598 MixerCommand::AddTrack { kind: _, handle } => {
599 let track = AudioTrack::new(handle, self.max_buffer_size);
600 self.tracks.push(track);
601 }
602 MixerCommand::SetInstrument { track_id, mut instrument } => {
603 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
604 instrument.init(self.sample_rate as f64, self.max_buffer_size);
605 track.instrument = Some(instrument);
606 }
607 }
608 MixerCommand::RemoveTrack { track_id } => {
609 self.tracks.retain(|t| t.id != track_id);
610 }
611 MixerCommand::SetParameter { track_id, param_index, value } => {
612 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
613 if let Some(ref mut inst) = track.instrument {
614 inst.set_parameter(param_index, value);
615 }
616 }
617 }
618 MixerCommand::CreateClip { track_id, start_tick, length_ticks } => {
619 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
620 track.clips.push(MidiClip::new(start_tick, length_ticks, Vec::new()));
621 }
622 }
623 MixerCommand::UpdateClip { track_id, clip_index, events } => {
624 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
625 if let Some(clip) = track.clips.get_mut(clip_index) {
626 clip.events = events;
627 clip.events.sort_by_key(|e| e.tick);
628 }
629 }
630 }
631 MixerCommand::UpdateClipPosition { track_id, clip_index, start_tick, length_ticks } => {
632 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
633 if let Some(clip) = track.clips.get_mut(clip_index) {
634 clip.start_tick = start_tick;
635 clip.length_ticks = length_ticks;
636 }
637 }
638 }
639 MixerCommand::RemoveClip { track_id, clip_index } => {
640 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
641 if clip_index < track.clips.len() {
642 track.clips.remove(clip_index);
643 }
644 }
645 }
646 }
647 }
648}
649
650fn commit_recording(track: &mut AudioTrack, end_tick: i64, clip_tx: &Sender<ClipSnapshot>) {
652 if let Some(clip) = track.record_buf.commit(end_tick) {
653 let idx = track.clips.len();
654 tracing::debug!(
655 "rec commit track={}: {} events, ticks {}..{}",
656 track.id, clip.events.len(), clip.start_tick, clip.end_tick()
657 );
658 let snapshot = ClipSnapshot::from_clip(track.id, idx, &clip);
659 track.clips.push(clip);
660 let _ = clip_tx.send(snapshot);
661 }
662}
663
664pub fn midi_to_plugin_event(msg: &MidiMessage) -> Option<MidiEvent> {
678 use phosphor_midi::message::MidiMessageType;
679 match msg.message_type {
680 MidiMessageType::NoteOn { .. }
681 | MidiMessageType::NoteOff { .. }
682 | MidiMessageType::ControlChange { .. }
683 | MidiMessageType::PitchBend { .. }
684 | MidiMessageType::ChannelPressure { .. } => Some(MidiEvent {
685 sample_offset: 0,
686 status: msg.raw[0],
687 data1: msg.raw[1],
688 data2: msg.raw[2],
689 }),
690 _ => None,
691 }
692}
693
694pub fn mixer_command_channel() -> (Sender<MixerCommand>, Receiver<MixerCommand>) {
695 crossbeam_channel::unbounded()
696}
697
698pub fn clip_snapshot_channel() -> (Sender<ClipSnapshot>, Receiver<ClipSnapshot>) {
700 crossbeam_channel::unbounded()
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706 use crate::cpal_backend::{Requested, StreamFormat};
707 use crate::project::TrackConfig;
708 use phosphor_dsp::synth::PhosphorSynth;
709 use phosphor_midi::message::{MidiMessage, MidiMessageType};
710
711 fn make_note_on(note: u8, vel: u8) -> MidiMessage {
712 MidiMessage {
713 timestamp: Some(0),
714 message_type: MidiMessageType::NoteOn { channel: 0, note, velocity: vel },
715 raw: [0x90, note, vel],
716 len: 3,
717 }
718 }
719
720 #[test]
723 fn channel_pressure_reaches_the_plugin_and_key_pressure_does_not() {
724 let pressure = MidiMessage {
725 timestamp: Some(0),
726 message_type: MidiMessageType::ChannelPressure { channel: 0, pressure: 96 },
727 raw: [0xD0, 96, 0],
728 len: 2,
729 };
730 let event = midi_to_plugin_event(&pressure).expect("channel pressure is dropped");
731 assert_eq!(event.status, 0xD0);
732 assert_eq!(event.data1, 96);
733
734 let key = MidiMessage::from_bytes(&[0xA0, 60, 96], 0).expect("parsed");
737 assert!(
738 midi_to_plugin_event(&key).is_none(),
739 "polyphonic key pressure has no destination in the rack"
740 );
741 }
742
743 fn make_note_off(note: u8) -> MidiMessage {
744 MidiMessage {
745 timestamp: Some(0),
746 message_type: MidiMessageType::NoteOff { channel: 0, note, velocity: 0 },
747 raw: [0x80, note, 0],
748 len: 3,
749 }
750 }
751
752 fn setup_mixer() -> (Mixer, Sender<MixerCommand>, Receiver<ClipSnapshot>, Arc<Transport>) {
753 let (tx, rx) = mixer_command_channel();
754 let (clip_tx, clip_rx) = clip_snapshot_channel();
755 let master_vu = Arc::new(VuLevels::new());
756 let transport = Arc::new(Transport::new(120.0));
757 let mixer = Mixer::new(rx, master_vu, clip_tx, 44100, 256);
758 (mixer, tx, clip_rx, transport)
759 }
760
761 fn add_armed_synth(tx: &Sender<MixerCommand>, id: usize) -> Arc<TrackHandle> {
762 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
763 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
764 handle.config.armed.store(true, std::sync::atomic::Ordering::Relaxed);
765 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
766 tx.send(MixerCommand::SetInstrument { track_id: id, instrument: Box::new(PhosphorSynth::new()) }).unwrap();
767 handle
768 }
769
770 #[test]
771 fn mixer_empty_output() {
772 let (mut mixer, _tx, _clip_rx, transport) = setup_mixer();
773 let mut output = vec![0.0f32; 128];
774 mixer.process(&mut output, &[], &transport);
775 assert!(output.iter().all(|&s| s == 0.0));
776 }
777
778 #[test]
779 fn mixer_live_midi_produces_sound() {
780 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
781 let _handle = add_armed_synth(&tx, 0);
782 transport.play();
783
784 let midi = vec![make_note_on(60, 100)];
785 let mut output = vec![0.0f32; 512];
786 mixer.process(&mut output, &midi, &transport);
787
788 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
789 assert!(peak > 0.001, "Should produce sound, peak={peak}");
792 }
793
794 #[test]
795 fn mixer_records_midi_clip() {
796 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
797 let _handle = add_armed_synth(&tx, 0);
798 transport.play();
799 transport.toggle_record();
800
801 let midi = vec![make_note_on(60, 100)];
803 let mut output = vec![0.0f32; 512];
804 mixer.process(&mut output, &midi, &transport);
805
806 let midi = vec![make_note_off(60)];
808 mixer.process(&mut output, &midi, &transport);
809
810 transport.toggle_record();
812 mixer.process(&mut output, &[], &transport);
813
814 let snap = clip_rx.try_recv().expect("Should receive clip snapshot");
816 assert_eq!(snap.track_id, 0);
817 assert!(snap.event_count >= 2, "Should have note on + off, got {}", snap.event_count);
818 assert!(!snap.notes.is_empty(), "Should have parsed notes");
819 }
820
821 #[test]
822 fn mixer_plays_back_recorded_clip() {
823 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
824 let _handle = add_armed_synth(&tx, 0);
825 transport.play();
826 transport.toggle_record();
827
828 let midi = vec![make_note_on(60, 100)];
830 let mut output = vec![0.0f32; 512];
831 mixer.process(&mut output, &midi, &transport);
832
833 let midi = vec![make_note_off(60)];
834 mixer.process(&mut output, &midi, &transport);
835
836 transport.toggle_record();
838 mixer.process(&mut output, &[], &transport);
839
840 transport.stop();
842
843 transport.play();
845 output.fill(0.0);
846 mixer.process(&mut output, &[], &transport);
847
848 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
849 assert!(peak > 0.001, "Playback should produce sound, peak={peak}");
850 }
851
852 #[test]
853 fn mixer_mute_silences() {
854 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
855 let handle = add_armed_synth(&tx, 0);
856 handle.config.muted.store(true, std::sync::atomic::Ordering::Relaxed);
857 transport.play();
858
859 let midi = vec![make_note_on(60, 100)];
860 let mut output = vec![0.0f32; 512];
861 mixer.process(&mut output, &midi, &transport);
862
863 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
864 assert!(peak == 0.0, "Muted track should be silent, peak={peak}");
865 }
866
867 #[test]
868 fn mixer_no_record_when_not_armed() {
869 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
870 let handle = add_armed_synth(&tx, 0);
871 handle.config.armed.store(false, std::sync::atomic::Ordering::Relaxed);
872 transport.play();
873 transport.toggle_record();
874
875 let midi = vec![make_note_on(60, 100)];
876 let mut output = vec![0.0f32; 512];
877 mixer.process(&mut output, &midi, &transport);
878
879 transport.toggle_record();
880 mixer.process(&mut output, &[], &transport);
881
882 assert!(clip_rx.try_recv().is_err(), "Should not record when not armed");
883 }
884
885 #[test]
886 fn mixer_reset_commits_recording() {
887 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
888 let _handle = add_armed_synth(&tx, 0);
889 transport.play();
890 transport.toggle_record();
891
892 let midi = vec![make_note_on(60, 100)];
893 let mut output = vec![0.0f32; 512];
894 mixer.process(&mut output, &midi, &transport);
895
896 mixer.reset_all();
897
898 assert!(clip_rx.try_recv().is_ok(), "Reset should commit active recording");
900 }
901
902 #[test]
903 fn end_to_end_record_and_playback() {
904 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
907 let _handle = add_armed_synth(&tx, 0);
908 let sr = 44100u32;
909 let buf_frames = 256;
910 let buf_samples = buf_frames * 2; transport.toggle_record();
914 transport.play();
915
916 let mut output = vec![0.0f32; buf_samples];
918 for _ in 0..4 {
919 mixer.process(&mut output, &[], &transport);
920 transport.advance(buf_frames as u32, sr);
921 }
922
923 let midi = vec![make_note_on(60, 100)];
925 mixer.process(&mut output, &midi, &transport);
926 let peak_during = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
927 assert!(peak_during > 0.001, "Should hear note during recording (monitoring)");
928 transport.advance(buf_frames as u32, sr);
929
930 for _ in 0..8 {
932 output.fill(0.0);
933 mixer.process(&mut output, &[], &transport);
934 transport.advance(buf_frames as u32, sr);
935 }
936
937 let midi = vec![make_note_off(60)];
939 mixer.process(&mut output, &midi, &transport);
940 transport.advance(buf_frames as u32, sr);
941
942 for _ in 0..4 {
944 output.fill(0.0);
945 mixer.process(&mut output, &[], &transport);
946 transport.advance(buf_frames as u32, sr);
947 }
948
949 transport.toggle_record();
951 mixer.process(&mut output, &[], &transport);
952 transport.advance(buf_frames as u32, sr);
953
954 let snap = clip_rx.try_recv().expect("Should receive clip snapshot after stopping record");
956 assert!(snap.event_count >= 2, "Clip should have note on + off");
957 assert!(!snap.notes.is_empty(), "Clip should have parsed notes");
958
959 transport.stop();
961
962 transport.play();
964
965 for _ in 0..4 {
968 output.fill(0.0);
969 mixer.process(&mut output, &[], &transport);
970 transport.advance(buf_frames as u32, sr);
971 }
972
973 output.fill(0.0);
975 mixer.process(&mut output, &[], &transport);
976 let peak_playback = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
977 assert!(peak_playback > 0.001, "Playback should produce sound at the recorded position, peak={peak_playback}");
978 }
979
980 #[test]
981 fn loop_record_commits_on_wrap() {
982 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
983 let _handle = add_armed_synth(&tx, 0);
984 let sr = 44100u32;
985 let buf_frames = 256u32;
986
987 transport.set_loop_bars(1, 1);
989 transport.start_loop_record();
990
991 let mut output = vec![0.0f32; buf_frames as usize * 2];
992
993 let midi = vec![make_note_on(60, 100)];
995 mixer.process(&mut output, &midi, &transport);
996 transport.advance(buf_frames, sr);
997
998 for _ in 0..5 {
1000 mixer.process(&mut output, &[], &transport);
1001 transport.advance(buf_frames, sr);
1002 }
1003 let midi = vec![make_note_off(60)];
1004 mixer.process(&mut output, &midi, &transport);
1005 transport.advance(buf_frames, sr);
1006
1007 for _ in 0..400 {
1010 mixer.process(&mut output, &[], &transport);
1011 transport.advance(buf_frames, sr);
1012
1013 if let Ok(snap) = clip_rx.try_recv() {
1014 assert!(snap.event_count >= 2, "Clip should have events, got {}", snap.event_count);
1015 assert!(!snap.notes.is_empty(), "Clip should have notes");
1016 transport.stop_loop_record();
1018 return;
1019 }
1020 }
1021
1022 panic!("Recording should have committed when the loop wrapped");
1023 }
1024
1025 #[test]
1026 fn loop_playback_after_record() {
1027 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
1028 let _handle = add_armed_synth(&tx, 0);
1029 let sr = 44100u32;
1030 let bf = 256u32;
1031
1032 transport.set_loop_bars(1, 1);
1034 transport.start_loop_record();
1035
1036 let mut output = vec![0.0f32; bf as usize * 2];
1037
1038 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
1040 transport.advance(bf, sr);
1041 for _ in 0..3 {
1042 mixer.process(&mut output, &[], &transport);
1043 transport.advance(bf, sr);
1044 }
1045 mixer.process(&mut output, &[make_note_off(60)], &transport);
1046 transport.advance(bf, sr);
1047
1048 for _ in 0..200 {
1050 mixer.process(&mut output, &[], &transport);
1051 transport.advance(bf, sr);
1052 if clip_rx.try_recv().is_ok() { break; }
1053 }
1054
1055 transport.stop_loop_record();
1057 transport.set_position(0);
1058
1059 transport.toggle_loop(); transport.play();
1062
1063 output.fill(0.0);
1064 mixer.process(&mut output, &[], &transport);
1065 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1066 assert!(peak > 0.001, "Should hear playback, peak={peak}");
1067 }
1068
1069 const WORST_CALLBACK: u32 = COMMAND_BUDGET - 1 + HEAVY_COMMAND;
1075
1076 #[derive(Clone)]
1083 struct ParamLog(Arc<std::sync::Mutex<Vec<(usize, f32)>>>);
1084
1085 impl ParamLog {
1086 fn new() -> Self {
1087 Self(Arc::new(std::sync::Mutex::new(Vec::new())))
1088 }
1089 fn seen(&self) -> Vec<(usize, f32)> {
1090 self.0.lock().unwrap().clone()
1091 }
1092 }
1093
1094 impl Plugin for ParamLog {
1095 fn info(&self) -> phosphor_plugin::PluginInfo {
1096 phosphor_plugin::PluginInfo {
1097 name: "ParamLog".into(),
1098 version: "0".into(),
1099 author: "test".into(),
1100 category: phosphor_plugin::PluginCategory::Instrument,
1101 }
1102 }
1103 fn init(&mut self, _sample_rate: f64, _max_buffer_size: usize) {}
1104 fn process(&mut self, _inputs: &[&[f32]], _outputs: &mut [&mut [f32]], _midi: &[MidiEvent]) {}
1105 fn parameter_count(&self) -> usize { 8 }
1106 fn parameter_info(&self, _index: usize) -> Option<phosphor_plugin::ParameterInfo> { None }
1107 fn get_parameter(&self, _index: usize) -> f32 { 0.0 }
1108 fn set_parameter(&mut self, index: usize, value: f32) {
1109 self.0.lock().unwrap().push((index, value));
1110 }
1111 fn reset(&mut self) {}
1112 }
1113
1114 fn add_logging_track(mixer: &mut Mixer, tx: &Sender<MixerCommand>, id: usize) -> ParamLog {
1116 let log = ParamLog::new();
1117 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1118 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1119 tx.send(MixerCommand::SetInstrument {
1120 track_id: id,
1121 instrument: Box::new(log.clone()),
1122 }).unwrap();
1123 mixer.drain_commands();
1124 log
1125 }
1126
1127 #[test]
1131 fn one_callback_applies_a_bounded_amount_of_work() {
1132 let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1133 let log = add_logging_track(&mut mixer, &tx, 0);
1134
1135 for i in 0..500 {
1136 tx.send(MixerCommand::SetParameter {
1137 track_id: 0,
1138 param_index: i % 8,
1139 value: i as f32,
1140 }).unwrap();
1141 }
1142
1143 let spent = mixer.drain_commands();
1144 assert!(
1145 spent <= WORST_CALLBACK,
1146 "one callback spent {spent} units, over the {WORST_CALLBACK} bound"
1147 );
1148 assert_eq!(
1149 log.seen().len(),
1150 COMMAND_BUDGET as usize,
1151 "a parameter costs one unit, so a full budget is exactly that many"
1152 );
1153 assert!(!mixer.command_rx.is_empty(), "the rest has to still be queued");
1154 }
1155
1156 #[test]
1159 fn nothing_is_lost_or_reordered_across_callbacks() {
1160 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1161 let log = add_logging_track(&mut mixer, &tx, 0);
1162
1163 let sent: Vec<(usize, f32)> = (0..500).map(|i| (i % 8, i as f32)).collect();
1164 for &(param_index, value) in &sent {
1165 tx.send(MixerCommand::SetParameter { track_id: 0, param_index, value }).unwrap();
1166 }
1167
1168 let mut output = vec![0.0f32; 128];
1172 let mut callbacks = 0;
1173 while !mixer.command_rx.is_empty() {
1174 mixer.process(&mut output, &[], &transport);
1175 callbacks += 1;
1176 assert!(callbacks < 100, "the drain is not making progress");
1177 }
1178 assert!(
1179 callbacks >= 500 / COMMAND_BUDGET as usize,
1180 "500 commands went through in {callbacks} callbacks, so the budget did not hold"
1181 );
1182 assert_eq!(log.seen(), sent, "the audio thread saw a different sequence");
1183 }
1184
1185 #[test]
1191 fn a_track_and_its_instrument_survive_a_budget_boundary() {
1192 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1193 let log = ParamLog::new();
1194
1195 for _ in 0..COMMAND_BUDGET {
1198 tx.send(MixerCommand::SetParameter { track_id: 99, param_index: 0, value: 0.0 })
1199 .unwrap();
1200 }
1201 let handle = Arc::new(TrackHandle::new(7, TrackKind::Instrument));
1202 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1203 tx.send(MixerCommand::SetInstrument {
1204 track_id: 7,
1205 instrument: Box::new(log.clone()),
1206 }).unwrap();
1207 tx.send(MixerCommand::SetParameter { track_id: 7, param_index: 3, value: 0.5 }).unwrap();
1208
1209 let mut output = vec![0.0f32; 128];
1210 mixer.process(&mut output, &[], &transport);
1211 assert!(mixer.tracks.is_empty(), "the budget did not stop at the parameters");
1212
1213 while !mixer.command_rx.is_empty() {
1214 mixer.process(&mut output, &[], &transport);
1215 }
1216 assert_eq!(mixer.tracks.len(), 1);
1217 assert!(mixer.tracks[0].instrument.is_some(), "the instrument never arrived");
1218 assert_eq!(
1219 log.seen(),
1220 vec![(3, 0.5)],
1221 "the parameter that follows the instrument did not reach it"
1222 );
1223 }
1224
1225 #[test]
1230 fn an_instrument_load_costs_more_than_a_parameter() {
1231 let param = MixerCommand::SetParameter { track_id: 0, param_index: 0, value: 0.0 };
1232 let load = MixerCommand::SetInstrument {
1233 track_id: 0,
1234 instrument: Box::new(FixedOutput(0.0)),
1235 };
1236 assert!(command_cost(&load) > command_cost(¶m));
1237
1238 let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1240 for id in 0..8 {
1241 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1242 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1243 }
1244 while !mixer.command_rx.is_empty() {
1245 mixer.drain_commands();
1246 }
1247 for id in 0..8 {
1248 tx.send(MixerCommand::SetInstrument {
1249 track_id: id,
1250 instrument: Box::new(FixedOutput(0.25)),
1251 }).unwrap();
1252 }
1253 mixer.drain_commands();
1254 let loaded = mixer.tracks.iter().filter(|t| t.instrument.is_some()).count();
1255 assert_eq!(loaded, (COMMAND_BUDGET / HEAVY_COMMAND) as usize);
1256 }
1257
1258 #[test]
1262 fn adding_tracks_does_not_grow_the_track_list() {
1263 let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1264 let capacity = mixer.tracks.capacity();
1265 assert!(capacity >= TRACK_CAPACITY);
1266
1267 for id in 0..TRACK_CAPACITY {
1268 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1269 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1270 }
1271 while !mixer.command_rx.is_empty() {
1272 mixer.drain_commands();
1273 }
1274 assert_eq!(mixer.tracks.len(), TRACK_CAPACITY);
1275 assert_eq!(
1276 mixer.tracks.capacity(), capacity,
1277 "the track list reallocated on the audio thread"
1278 );
1279 }
1280
1281 struct FixedOutput(f32);
1286
1287 impl Plugin for FixedOutput {
1288 fn info(&self) -> phosphor_plugin::PluginInfo {
1289 phosphor_plugin::PluginInfo {
1290 name: "Fixed".into(),
1291 version: "0".into(),
1292 author: "test".into(),
1293 category: phosphor_plugin::PluginCategory::Instrument,
1294 }
1295 }
1296 fn init(&mut self, _sample_rate: f64, _max_buffer_size: usize) {}
1297 fn process(&mut self, _inputs: &[&[f32]], outputs: &mut [&mut [f32]], _midi: &[MidiEvent]) {
1298 for ch in outputs.iter_mut() {
1299 ch.fill(self.0);
1300 }
1301 }
1302 fn parameter_count(&self) -> usize { 0 }
1303 fn parameter_info(&self, _index: usize) -> Option<phosphor_plugin::ParameterInfo> { None }
1304 fn get_parameter(&self, _index: usize) -> f32 { 0.0 }
1305 fn set_parameter(&mut self, _index: usize, _value: f32) {}
1306 fn reset(&mut self) {}
1307 }
1308
1309 fn add_fixed_track(tx: &Sender<MixerCommand>, id: usize, value: f32) -> Arc<TrackHandle> {
1310 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1311 handle.config.set_volume(1.0);
1312 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
1313 tx.send(MixerCommand::SetInstrument {
1314 track_id: id,
1315 instrument: Box::new(FixedOutput(value)),
1316 }).unwrap();
1317 handle
1318 }
1319
1320 #[test]
1323 fn master_limiter_bounds_many_loud_tracks() {
1324 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1325 for id in 0..6 {
1326 add_fixed_track(&tx, id, 0.75);
1327 }
1328 transport.play();
1329
1330 let mut output = vec![0.0f32; 512];
1331 for _ in 0..8 {
1332 mixer.process(&mut output, &[], &transport);
1333 for (i, &s) in output.iter().enumerate() {
1334 assert!(s.is_finite(), "non-finite sample at {i}");
1335 assert!(s.abs() <= 1.0, "sample {i} left the mixer at {s}");
1336 }
1337 }
1338
1339 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1341 assert!(peak > 0.8, "limiter over-attenuated, peak={peak}");
1342 }
1343
1344 #[test]
1348 fn non_finite_track_output_becomes_silence() {
1349 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1350 add_fixed_track(&tx, 0, f32::NAN);
1351 transport.play();
1352
1353 let mut output = vec![0.0f32; 512];
1354 mixer.process(&mut output, &[], &transport);
1355 assert!(output.iter().all(|s| *s == 0.0), "NaN track should render as silence");
1356
1357 tx.send(MixerCommand::RemoveTrack { track_id: 0 }).unwrap();
1360 add_fixed_track(&tx, 1, 0.5);
1361 mixer.process(&mut output, &[], &transport);
1362 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1363 assert!((peak - 0.5).abs() < 1.0e-6, "mixer did not recover, peak={peak}");
1364 }
1365
1366 #[test]
1367 fn infinite_track_output_becomes_silence() {
1368 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1369 add_fixed_track(&tx, 0, f32::INFINITY);
1370 transport.play();
1371
1372 let mut output = vec![0.0f32; 512];
1373 mixer.process(&mut output, &[], &transport);
1374 assert!(output.iter().all(|s| *s == 0.0), "infinite track should render as silence");
1375 }
1376
1377 #[test]
1381 fn limiter_is_bit_identical_below_the_ceiling() {
1382 let mut limiter = MasterLimiter::new(44_100);
1383
1384 let mut input: Vec<f32> = Vec::new();
1386 for i in 0..20_000u32 {
1387 let phase = i as f32 * 0.01;
1388 let amp = LIMITER_CEILING * (i as f32 / 20_000.0);
1389 input.push(phase.sin() * amp);
1390 input.push(phase.cos() * amp);
1391 }
1392 input.push(LIMITER_CEILING);
1393 input.push(-LIMITER_CEILING);
1394 input.push(0.0);
1395 input.push(-0.0);
1396 input.push(f32::MIN_POSITIVE);
1397 input.push(-f32::MIN_POSITIVE);
1398
1399 let mut output = input.clone();
1400 limiter.process(&mut output);
1401
1402 for (i, (a, b)) in input.iter().zip(output.iter()).enumerate() {
1403 assert_eq!(a.to_bits(), b.to_bits(), "limiter altered sample {i}: {a} -> {b}");
1404 }
1405 }
1406
1407 #[test]
1410 fn limiter_holds_the_ceiling_under_abuse() {
1411 let mut limiter = MasterLimiter::new(44_100);
1412 for amplitude in [1.0f32, 2.0, 10.0, 1.0e3, 1.0e6, 1.0e30] {
1413 let mut buf: Vec<f32> = (0..4_096)
1414 .map(|i| (i as f32 * 0.05).sin() * amplitude)
1415 .collect();
1416 limiter.process(&mut buf);
1417 for (i, &s) in buf.iter().enumerate() {
1418 assert!(s.is_finite(), "amplitude {amplitude}: sample {i} is {s}");
1419 assert!(
1420 s.abs() <= LIMITER_CEILING,
1421 "amplitude {amplitude}: sample {i} reached {s}, above the ceiling"
1422 );
1423 }
1424 }
1425 }
1426
1427 #[test]
1431 fn limiter_attack_has_no_overshoot() {
1432 let mut limiter = MasterLimiter::new(44_100);
1433 let mut buf = vec![0.0f32; 64];
1434 limiter.process(&mut buf);
1435 let mut step = vec![4.0f32; 64];
1436 limiter.process(&mut step);
1437 assert!(
1438 step[0].abs() <= LIMITER_CEILING,
1439 "first sample of the step overshot to {}",
1440 step[0]
1441 );
1442 }
1443
1444 #[test]
1447 fn limiter_release_is_gradual() {
1448 let mut limiter = MasterLimiter::new(44_100);
1449 let mut loud = vec![4.0f32; 64];
1450 limiter.process(&mut loud);
1451 let reduced = limiter.gain;
1452 assert!(reduced < 0.5, "limiter did not engage, gain={reduced}");
1453
1454 let mut quiet = vec![0.1f32; 441 * 2];
1457 limiter.process(&mut quiet);
1458 assert!(limiter.gain > reduced, "gain did not recover at all");
1459 assert!(
1460 limiter.gain < 1.0,
1461 "gain snapped back to unity within 10 ms, which is a click"
1462 );
1463
1464 let mut long = vec![0.1f32; 22_050 * 2];
1466 limiter.process(&mut long);
1467 assert!(
1468 (limiter.gain - 1.0).abs() < 1.0e-4,
1469 "gain never returned to unity: {}",
1470 limiter.gain
1471 );
1472 }
1473
1474 #[test]
1477 fn limiter_does_not_shift_the_stereo_image() {
1478 let mut limiter = MasterLimiter::new(44_100);
1479 let mut buf: Vec<f32> = Vec::new();
1481 for i in 0..1_024 {
1482 let phase = i as f32 * 0.05;
1483 buf.push(phase.sin() * 3.0);
1484 buf.push(phase.sin() * 1.5);
1485 }
1486 limiter.process(&mut buf);
1487 for frame in buf.chunks_exact(2) {
1488 if frame[1].abs() > 1.0e-4 {
1489 let ratio = frame[0] / frame[1];
1490 assert!(
1491 (ratio - 2.0).abs() < 1.0e-3,
1492 "channel balance moved: L/R = {ratio}"
1493 );
1494 }
1495 }
1496 }
1497
1498 fn loudest_dx7_voice() -> phosphor_dsp::dx7::Dx7Synth {
1505 use phosphor_dsp::dx7;
1506 let mut synth = dx7::Dx7Synth::new();
1507 let (bank, patch) = dx7::voice_knobs(147);
1508 synth.set_parameter(dx7::P_BANK, bank);
1509 synth.set_parameter(dx7::P_PATCH, patch);
1510 debug_assert_eq!(dx7::voice_name(147), "TIMPANI");
1511 synth
1512 }
1513
1514 #[test]
1518 fn master_limiter_bounds_four_loud_instrument_tracks() {
1519 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1520 for id in 0..4 {
1521 let handle = Arc::new(TrackHandle::new(id, 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 {
1527 track_id: id,
1528 instrument: Box::new(synth),
1529 }).unwrap();
1530 }
1531 transport.play();
1532
1533 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1534 .iter()
1535 .map(|¬e| make_note_on(note, 127))
1536 .collect();
1537
1538 let mut output = vec![0.0f32; 512];
1539 let mut peak = 0.0f32;
1540 for block in 0..200 {
1541 output.fill(0.0);
1542 if block == 0 {
1543 mixer.process(&mut output, &chord, &transport);
1544 } else {
1545 mixer.process(&mut output, &[], &transport);
1546 }
1547 for (i, &s) in output.iter().enumerate() {
1548 assert!(s.is_finite(), "block {block} sample {i} is {s}");
1549 assert!(s.abs() <= 1.0, "block {block} sample {i} left the mixer at {s}");
1550 peak = peak.max(s.abs());
1551 }
1552 }
1553 assert!(peak > 0.5, "four loud tracks should be loud, peak={peak}");
1554 }
1555
1556 #[test]
1562 fn limiter_idle_for_the_worst_single_track() {
1563 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1564 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1565 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1566 handle.config.set_volume(1.0);
1567 let synth = loudest_dx7_voice();
1568 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1569 tx.send(MixerCommand::SetInstrument { track_id: 0, instrument: Box::new(synth) }).unwrap();
1570 transport.play();
1571
1572 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1573 .iter()
1574 .map(|¬e| make_note_on(note, 127))
1575 .collect();
1576
1577 let mut output = vec![0.0f32; 512];
1578 let mut peak = 0.0f32;
1579 for block in 0..200 {
1580 output.fill(0.0);
1581 if block == 0 {
1582 mixer.process(&mut output, &chord, &transport);
1583 } else {
1584 mixer.process(&mut output, &[], &transport);
1585 }
1586 peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0f32, f32::max));
1587 assert_eq!(
1588 mixer.limiter.gain, 1.0,
1589 "limiter engaged at block {block}, peak {peak}"
1590 );
1591 }
1592 assert!(peak > 0.3, "expected a loud chord, peak={peak}");
1593 }
1594
1595 fn worst_track_through_the_mixer(volume: f32) -> (f32, f32) {
1601 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1602 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1603 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1604 handle.config.set_volume(volume);
1605 let synth = loudest_dx7_voice();
1606 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1607 tx.send(MixerCommand::SetInstrument { track_id: 0, instrument: Box::new(synth) }).unwrap();
1608 transport.play();
1609
1610 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1611 .iter()
1612 .map(|¬e| make_note_on(note, 127))
1613 .collect();
1614
1615 let mut output = vec![0.0f32; 512];
1616 let mut peak = 0.0f32;
1617 let mut min_gain = 1.0f32;
1618 for block in 0..200 {
1619 output.fill(0.0);
1620 if block == 0 {
1621 mixer.process(&mut output, &chord, &transport);
1622 } else {
1623 mixer.process(&mut output, &[], &transport);
1624 }
1625 for &s in output.iter() {
1626 assert!(s.is_finite(), "block {block}: non-finite sample");
1627 assert!(s.abs() <= 1.0, "block {block}: sample left the mixer at {s}");
1628 peak = peak.max(s.abs());
1629 }
1630 min_gain = min_gain.min(mixer.limiter.gain);
1631 }
1632 (peak, min_gain)
1633 }
1634
1635 #[test]
1643 fn fader_below_unity_never_engages_the_limiter() {
1644 for volume in [
1645 0.25,
1646 TrackConfig::DEFAULT_VOLUME,
1647 TrackConfig::UNITY_VOLUME,
1648 ] {
1649 let (peak, min_gain) = worst_track_through_the_mixer(volume);
1650 assert_eq!(
1651 min_gain, 1.0,
1652 "limiter reduced by {:.2} dB at fader {volume} (peak {peak:.4})",
1653 20.0 * min_gain.log10()
1654 );
1655 }
1656 }
1657
1658 #[test]
1664 fn fader_makeup_gain_is_bounded_not_wasted() {
1665 let (unity_peak, _) = worst_track_through_the_mixer(TrackConfig::UNITY_VOLUME);
1666 let (max_peak, min_gain) = worst_track_through_the_mixer(TrackConfig::MAX_VOLUME);
1667
1668 assert!(
1669 max_peak <= LIMITER_CEILING,
1670 "fader at maximum let {max_peak:.4} through, above the ceiling"
1671 );
1672 assert!(
1673 max_peak >= unity_peak,
1674 "turning the fader up made the track quieter: {unity_peak:.4} -> {max_peak:.4}"
1675 );
1676 let reduction_db = -20.0 * min_gain.log10();
1679 let boost_db = 20.0 * (TrackConfig::MAX_VOLUME / TrackConfig::UNITY_VOLUME).log10();
1680 assert!(
1681 reduction_db <= boost_db,
1682 "limiter took {reduction_db:.2} dB off a {boost_db:.2} dB boost"
1683 );
1684 }
1685
1686 #[test]
1698 fn metronome_click_sits_with_the_music() {
1699 use phosphor_dsp::dx7;
1700
1701 fn render(with_track: bool, metronome: bool) -> f32 {
1702 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1703 let chord: Vec<MidiMessage> = if with_track {
1704 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1705 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1706 tx.send(MixerCommand::AddTrack {
1707 kind: TrackKind::Instrument,
1708 handle,
1709 })
1710 .unwrap();
1711 tx.send(MixerCommand::SetInstrument {
1712 track_id: 0,
1713 instrument: Box::new(dx7::Dx7Synth::new()),
1714 })
1715 .unwrap();
1716 [60u8, 64, 67].iter().map(|&n| make_note_on(n, 100)).collect()
1717 } else {
1718 Vec::new()
1719 };
1720 if metronome {
1721 transport.toggle_metronome();
1722 }
1723 transport.play();
1724
1725 let mut output = vec![0.0f32; 512];
1726 let mut peak = 0.0f32;
1727 for block in 0..200 {
1728 output.fill(0.0);
1729 if block == 0 {
1730 mixer.process(&mut output, &chord, &transport);
1731 } else {
1732 mixer.process(&mut output, &[], &transport);
1733 }
1734 peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0f32, f32::max));
1735 transport.advance(256, 44_100);
1736 }
1737 peak
1738 }
1739
1740 let music = render(true, false);
1741 let click = render(false, true);
1742 assert!(music > 0.0 && click > 0.0, "music {music}, click {click}");
1743
1744 let relative_db = 20.0 * (click / music).log10();
1745 assert!(
1746 (-12.0..=0.0).contains(&relative_db),
1747 "the click is {relative_db:.1} dB against a triad (click {click:.4}, \
1748 music {music:.4}); it has to be audible over the music without \
1749 being the loudest thing in the mix"
1750 );
1751 }
1752
1753 #[test]
1757 fn fader_scales_the_track() {
1758 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1759 let handle = add_fixed_track(&tx, 0, 0.25);
1760 transport.play();
1761
1762 let mut output = vec![0.0f32; 512];
1763 for (volume, expected) in [(0.0f32, 0.0f32), (0.5, 0.125), (1.0, 0.25), (2.0, 0.5)] {
1764 handle.config.set_volume(volume);
1765 output.fill(0.0);
1766 mixer.process(&mut output, &[], &transport);
1767 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1768 assert!(
1769 (peak - expected).abs() < 1.0e-6,
1770 "fader at {volume} gave {peak}, expected {expected}"
1771 );
1772 }
1773 }
1774
1775 fn refused(asked: u32, sample_rate: u32, max_buffer_frames: u32) -> StreamFormat {
1779 StreamFormat {
1780 sample_rate,
1781 buffer_size: Some(64),
1782 max_buffer_frames,
1783 channels: 2,
1784 sample_rate_request: Requested::Refused(asked),
1785 buffer_size_request: Requested::Granted,
1786 }
1787 }
1788
1789 #[test]
1794 fn the_mixer_runs_at_the_rate_the_device_granted() {
1795 let requested = crate::EngineConfig { buffer_size: 64, sample_rate: 44100 };
1796 let format = refused(44100, 48000, 4096);
1797 let effective = crate::EngineConfig::from(format);
1798
1799 let (_tx, rx) = mixer_command_channel();
1800 let (clip_tx, _clip_rx) = clip_snapshot_channel();
1801 let mixer = Mixer::new(
1802 rx,
1803 Arc::new(VuLevels::new()),
1804 clip_tx,
1805 effective.sample_rate,
1806 format.max_buffer_frames as usize,
1807 );
1808
1809 assert_eq!(mixer.sample_rate, 48000, "mixer must adopt the device's rate");
1810 assert_ne!(
1811 mixer.sample_rate, requested.sample_rate,
1812 "the request was 44100 and the device said 48000; taking the \
1813 request here is the 8.84%-sharp bug"
1814 );
1815 assert_eq!(mixer.max_buffer_size, 4096);
1816 }
1817
1818 #[test]
1820 fn a_device_that_agrees_leaves_the_request_alone() {
1821 let requested = crate::EngineConfig { buffer_size: 64, sample_rate: 44100 };
1822 let format = StreamFormat {
1823 sample_rate: 44100,
1824 buffer_size: Some(64),
1825 max_buffer_frames: 4096,
1826 channels: 2,
1827 sample_rate_request: Requested::Granted,
1828 buffer_size_request: Requested::Granted,
1829 };
1830 assert_eq!(crate::EngineConfig::from(format), requested);
1831 }
1832
1833 #[test]
1837 fn asking_for_nothing_builds_the_mixer_at_the_devices_rate() {
1838 let format = StreamFormat {
1839 sample_rate: 48000,
1840 buffer_size: None,
1841 max_buffer_frames: 4096,
1842 channels: 2,
1843 sample_rate_request: Requested::Unasked,
1844 buffer_size_request: Requested::Unasked,
1845 };
1846 let effective = crate::EngineConfig::from(format);
1847
1848 let (_tx, rx) = mixer_command_channel();
1849 let (clip_tx, _clip_rx) = clip_snapshot_channel();
1850 let mixer = Mixer::new(
1851 rx,
1852 Arc::new(VuLevels::new()),
1853 clip_tx,
1854 effective.sample_rate,
1855 format.max_buffer_frames as usize,
1856 );
1857 assert_eq!(mixer.sample_rate, 48000);
1858 assert_eq!(mixer.max_buffer_size, 4096);
1859 assert!(format.divergence_notice().is_none(), "following the device is not news");
1860 }
1861
1862 #[test]
1866 fn the_largest_block_the_device_promised_never_grows_a_buffer() {
1867 let max_frames = 512usize;
1868 let (tx, rx) = mixer_command_channel();
1869 let (clip_tx, _clip_rx) = clip_snapshot_channel();
1870 let mut mixer = Mixer::new(
1871 rx,
1872 Arc::new(VuLevels::new()),
1873 clip_tx,
1874 48000,
1875 max_frames,
1876 );
1877 let transport = Arc::new(Transport::new(120.0));
1878 let _handle = add_armed_synth(&tx, 0);
1879 mixer.drain_commands();
1880
1881 let before = (
1884 mixer.scratch_l.capacity(),
1885 mixer.scratch_r.capacity(),
1886 mixer.tracks[0].buf_l.capacity(),
1887 mixer.tracks[0].buf_r.capacity(),
1888 );
1889
1890 transport.play();
1891 let mut output = vec![0.0f32; max_frames * 2];
1892 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
1893
1894 let after = (
1895 mixer.scratch_l.capacity(),
1896 mixer.scratch_r.capacity(),
1897 mixer.tracks[0].buf_l.capacity(),
1898 mixer.tracks[0].buf_r.capacity(),
1899 );
1900 assert_eq!(
1901 before, after,
1902 "a block the size the device promised must fit the buffers as \
1903 allocated; growing one means the audio thread called the allocator"
1904 );
1905 }
1906
1907 #[test]
1911 fn a_steady_state_callback_does_not_allocate() {
1912 let max_frames = 512usize;
1913 let (tx, rx) = mixer_command_channel();
1914 let (clip_tx, _clip_rx) = clip_snapshot_channel();
1915 let mut mixer = Mixer::new(rx, Arc::new(VuLevels::new()), clip_tx, 48000, max_frames);
1916 let transport = Arc::new(Transport::new(120.0));
1917 let _handle = add_armed_synth(&tx, 0);
1918 mixer.drain_commands();
1919 transport.play();
1920
1921 let mut output = vec![0.0f32; max_frames * 2];
1922 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
1926
1927 let allocations = crate::alloc_count::allocations_during(|| {
1928 for _ in 0..8 {
1929 mixer.process(&mut output, &[], &transport);
1930 }
1931 });
1932 assert_eq!(allocations, 0, "Mixer::process reached the allocator");
1933 }
1934
1935 #[test]
1938 fn a_short_callback_does_not_allocate_either() {
1939 let max_frames = 512usize;
1940 let (tx, rx) = mixer_command_channel();
1941 let (clip_tx, _clip_rx) = clip_snapshot_channel();
1942 let mut mixer = Mixer::new(rx, Arc::new(VuLevels::new()), clip_tx, 48000, max_frames);
1943 let transport = Arc::new(Transport::new(120.0));
1944 let _handle = add_armed_synth(&tx, 0);
1945 mixer.drain_commands();
1946 transport.play();
1947
1948 let mut output = vec![0.0f32; 64 * 2];
1949 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
1950
1951 let allocations = crate::alloc_count::allocations_during(|| {
1952 for _ in 0..8 {
1953 mixer.process(&mut output, &[], &transport);
1954 }
1955 });
1956 assert_eq!(allocations, 0, "Mixer::process reached the allocator");
1957 }
1958}