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> {
665 use phosphor_midi::message::MidiMessageType;
666 match msg.message_type {
667 MidiMessageType::NoteOn { .. }
668 | MidiMessageType::NoteOff { .. }
669 | MidiMessageType::ControlChange { .. }
670 | MidiMessageType::PitchBend { .. } => Some(MidiEvent {
671 sample_offset: 0,
672 status: msg.raw[0],
673 data1: msg.raw[1],
674 data2: msg.raw[2],
675 }),
676 _ => None,
677 }
678}
679
680pub fn mixer_command_channel() -> (Sender<MixerCommand>, Receiver<MixerCommand>) {
681 crossbeam_channel::unbounded()
682}
683
684pub fn clip_snapshot_channel() -> (Sender<ClipSnapshot>, Receiver<ClipSnapshot>) {
686 crossbeam_channel::unbounded()
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692 use crate::cpal_backend::{Requested, StreamFormat};
693 use crate::project::TrackConfig;
694 use phosphor_dsp::synth::PhosphorSynth;
695 use phosphor_midi::message::{MidiMessage, MidiMessageType};
696
697 fn make_note_on(note: u8, vel: u8) -> MidiMessage {
698 MidiMessage {
699 timestamp: Some(0),
700 message_type: MidiMessageType::NoteOn { channel: 0, note, velocity: vel },
701 raw: [0x90, note, vel],
702 len: 3,
703 }
704 }
705
706 fn make_note_off(note: u8) -> MidiMessage {
707 MidiMessage {
708 timestamp: Some(0),
709 message_type: MidiMessageType::NoteOff { channel: 0, note, velocity: 0 },
710 raw: [0x80, note, 0],
711 len: 3,
712 }
713 }
714
715 fn setup_mixer() -> (Mixer, Sender<MixerCommand>, Receiver<ClipSnapshot>, Arc<Transport>) {
716 let (tx, rx) = mixer_command_channel();
717 let (clip_tx, clip_rx) = clip_snapshot_channel();
718 let master_vu = Arc::new(VuLevels::new());
719 let transport = Arc::new(Transport::new(120.0));
720 let mixer = Mixer::new(rx, master_vu, clip_tx, 44100, 256);
721 (mixer, tx, clip_rx, transport)
722 }
723
724 fn add_armed_synth(tx: &Sender<MixerCommand>, id: usize) -> Arc<TrackHandle> {
725 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
726 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
727 handle.config.armed.store(true, std::sync::atomic::Ordering::Relaxed);
728 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
729 tx.send(MixerCommand::SetInstrument { track_id: id, instrument: Box::new(PhosphorSynth::new()) }).unwrap();
730 handle
731 }
732
733 #[test]
734 fn mixer_empty_output() {
735 let (mut mixer, _tx, _clip_rx, transport) = setup_mixer();
736 let mut output = vec![0.0f32; 128];
737 mixer.process(&mut output, &[], &transport);
738 assert!(output.iter().all(|&s| s == 0.0));
739 }
740
741 #[test]
742 fn mixer_live_midi_produces_sound() {
743 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
744 let _handle = add_armed_synth(&tx, 0);
745 transport.play();
746
747 let midi = vec![make_note_on(60, 100)];
748 let mut output = vec![0.0f32; 512];
749 mixer.process(&mut output, &midi, &transport);
750
751 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
752 assert!(peak > 0.001, "Should produce sound, peak={peak}");
755 }
756
757 #[test]
758 fn mixer_records_midi_clip() {
759 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
760 let _handle = add_armed_synth(&tx, 0);
761 transport.play();
762 transport.toggle_record();
763
764 let midi = vec![make_note_on(60, 100)];
766 let mut output = vec![0.0f32; 512];
767 mixer.process(&mut output, &midi, &transport);
768
769 let midi = vec![make_note_off(60)];
771 mixer.process(&mut output, &midi, &transport);
772
773 transport.toggle_record();
775 mixer.process(&mut output, &[], &transport);
776
777 let snap = clip_rx.try_recv().expect("Should receive clip snapshot");
779 assert_eq!(snap.track_id, 0);
780 assert!(snap.event_count >= 2, "Should have note on + off, got {}", snap.event_count);
781 assert!(!snap.notes.is_empty(), "Should have parsed notes");
782 }
783
784 #[test]
785 fn mixer_plays_back_recorded_clip() {
786 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
787 let _handle = add_armed_synth(&tx, 0);
788 transport.play();
789 transport.toggle_record();
790
791 let midi = vec![make_note_on(60, 100)];
793 let mut output = vec![0.0f32; 512];
794 mixer.process(&mut output, &midi, &transport);
795
796 let midi = vec![make_note_off(60)];
797 mixer.process(&mut output, &midi, &transport);
798
799 transport.toggle_record();
801 mixer.process(&mut output, &[], &transport);
802
803 transport.stop();
805
806 transport.play();
808 output.fill(0.0);
809 mixer.process(&mut output, &[], &transport);
810
811 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
812 assert!(peak > 0.001, "Playback should produce sound, peak={peak}");
813 }
814
815 #[test]
816 fn mixer_mute_silences() {
817 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
818 let handle = add_armed_synth(&tx, 0);
819 handle.config.muted.store(true, std::sync::atomic::Ordering::Relaxed);
820 transport.play();
821
822 let midi = vec![make_note_on(60, 100)];
823 let mut output = vec![0.0f32; 512];
824 mixer.process(&mut output, &midi, &transport);
825
826 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
827 assert!(peak == 0.0, "Muted track should be silent, peak={peak}");
828 }
829
830 #[test]
831 fn mixer_no_record_when_not_armed() {
832 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
833 let handle = add_armed_synth(&tx, 0);
834 handle.config.armed.store(false, std::sync::atomic::Ordering::Relaxed);
835 transport.play();
836 transport.toggle_record();
837
838 let midi = vec![make_note_on(60, 100)];
839 let mut output = vec![0.0f32; 512];
840 mixer.process(&mut output, &midi, &transport);
841
842 transport.toggle_record();
843 mixer.process(&mut output, &[], &transport);
844
845 assert!(clip_rx.try_recv().is_err(), "Should not record when not armed");
846 }
847
848 #[test]
849 fn mixer_reset_commits_recording() {
850 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
851 let _handle = add_armed_synth(&tx, 0);
852 transport.play();
853 transport.toggle_record();
854
855 let midi = vec![make_note_on(60, 100)];
856 let mut output = vec![0.0f32; 512];
857 mixer.process(&mut output, &midi, &transport);
858
859 mixer.reset_all();
860
861 assert!(clip_rx.try_recv().is_ok(), "Reset should commit active recording");
863 }
864
865 #[test]
866 fn end_to_end_record_and_playback() {
867 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
870 let _handle = add_armed_synth(&tx, 0);
871 let sr = 44100u32;
872 let buf_frames = 256;
873 let buf_samples = buf_frames * 2; transport.toggle_record();
877 transport.play();
878
879 let mut output = vec![0.0f32; buf_samples];
881 for _ in 0..4 {
882 mixer.process(&mut output, &[], &transport);
883 transport.advance(buf_frames as u32, sr);
884 }
885
886 let midi = vec![make_note_on(60, 100)];
888 mixer.process(&mut output, &midi, &transport);
889 let peak_during = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
890 assert!(peak_during > 0.001, "Should hear note during recording (monitoring)");
891 transport.advance(buf_frames as u32, sr);
892
893 for _ in 0..8 {
895 output.fill(0.0);
896 mixer.process(&mut output, &[], &transport);
897 transport.advance(buf_frames as u32, sr);
898 }
899
900 let midi = vec![make_note_off(60)];
902 mixer.process(&mut output, &midi, &transport);
903 transport.advance(buf_frames as u32, sr);
904
905 for _ in 0..4 {
907 output.fill(0.0);
908 mixer.process(&mut output, &[], &transport);
909 transport.advance(buf_frames as u32, sr);
910 }
911
912 transport.toggle_record();
914 mixer.process(&mut output, &[], &transport);
915 transport.advance(buf_frames as u32, sr);
916
917 let snap = clip_rx.try_recv().expect("Should receive clip snapshot after stopping record");
919 assert!(snap.event_count >= 2, "Clip should have note on + off");
920 assert!(!snap.notes.is_empty(), "Clip should have parsed notes");
921
922 transport.stop();
924
925 transport.play();
927
928 for _ in 0..4 {
931 output.fill(0.0);
932 mixer.process(&mut output, &[], &transport);
933 transport.advance(buf_frames as u32, sr);
934 }
935
936 output.fill(0.0);
938 mixer.process(&mut output, &[], &transport);
939 let peak_playback = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
940 assert!(peak_playback > 0.001, "Playback should produce sound at the recorded position, peak={peak_playback}");
941 }
942
943 #[test]
944 fn loop_record_commits_on_wrap() {
945 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
946 let _handle = add_armed_synth(&tx, 0);
947 let sr = 44100u32;
948 let buf_frames = 256u32;
949
950 transport.set_loop_bars(1, 1);
952 transport.start_loop_record();
953
954 let mut output = vec![0.0f32; buf_frames as usize * 2];
955
956 let midi = vec![make_note_on(60, 100)];
958 mixer.process(&mut output, &midi, &transport);
959 transport.advance(buf_frames, sr);
960
961 for _ in 0..5 {
963 mixer.process(&mut output, &[], &transport);
964 transport.advance(buf_frames, sr);
965 }
966 let midi = vec![make_note_off(60)];
967 mixer.process(&mut output, &midi, &transport);
968 transport.advance(buf_frames, sr);
969
970 for _ in 0..400 {
973 mixer.process(&mut output, &[], &transport);
974 transport.advance(buf_frames, sr);
975
976 if let Ok(snap) = clip_rx.try_recv() {
977 assert!(snap.event_count >= 2, "Clip should have events, got {}", snap.event_count);
978 assert!(!snap.notes.is_empty(), "Clip should have notes");
979 transport.stop_loop_record();
981 return;
982 }
983 }
984
985 panic!("Recording should have committed when the loop wrapped");
986 }
987
988 #[test]
989 fn loop_playback_after_record() {
990 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
991 let _handle = add_armed_synth(&tx, 0);
992 let sr = 44100u32;
993 let bf = 256u32;
994
995 transport.set_loop_bars(1, 1);
997 transport.start_loop_record();
998
999 let mut output = vec![0.0f32; bf as usize * 2];
1000
1001 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
1003 transport.advance(bf, sr);
1004 for _ in 0..3 {
1005 mixer.process(&mut output, &[], &transport);
1006 transport.advance(bf, sr);
1007 }
1008 mixer.process(&mut output, &[make_note_off(60)], &transport);
1009 transport.advance(bf, sr);
1010
1011 for _ in 0..200 {
1013 mixer.process(&mut output, &[], &transport);
1014 transport.advance(bf, sr);
1015 if clip_rx.try_recv().is_ok() { break; }
1016 }
1017
1018 transport.stop_loop_record();
1020 transport.set_position(0);
1021
1022 transport.toggle_loop(); transport.play();
1025
1026 output.fill(0.0);
1027 mixer.process(&mut output, &[], &transport);
1028 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1029 assert!(peak > 0.001, "Should hear playback, peak={peak}");
1030 }
1031
1032 const WORST_CALLBACK: u32 = COMMAND_BUDGET - 1 + HEAVY_COMMAND;
1038
1039 #[derive(Clone)]
1046 struct ParamLog(Arc<std::sync::Mutex<Vec<(usize, f32)>>>);
1047
1048 impl ParamLog {
1049 fn new() -> Self {
1050 Self(Arc::new(std::sync::Mutex::new(Vec::new())))
1051 }
1052 fn seen(&self) -> Vec<(usize, f32)> {
1053 self.0.lock().unwrap().clone()
1054 }
1055 }
1056
1057 impl Plugin for ParamLog {
1058 fn info(&self) -> phosphor_plugin::PluginInfo {
1059 phosphor_plugin::PluginInfo {
1060 name: "ParamLog".into(),
1061 version: "0".into(),
1062 author: "test".into(),
1063 category: phosphor_plugin::PluginCategory::Instrument,
1064 }
1065 }
1066 fn init(&mut self, _sample_rate: f64, _max_buffer_size: usize) {}
1067 fn process(&mut self, _inputs: &[&[f32]], _outputs: &mut [&mut [f32]], _midi: &[MidiEvent]) {}
1068 fn parameter_count(&self) -> usize { 8 }
1069 fn parameter_info(&self, _index: usize) -> Option<phosphor_plugin::ParameterInfo> { None }
1070 fn get_parameter(&self, _index: usize) -> f32 { 0.0 }
1071 fn set_parameter(&mut self, index: usize, value: f32) {
1072 self.0.lock().unwrap().push((index, value));
1073 }
1074 fn reset(&mut self) {}
1075 }
1076
1077 fn add_logging_track(mixer: &mut Mixer, tx: &Sender<MixerCommand>, id: usize) -> ParamLog {
1079 let log = ParamLog::new();
1080 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1081 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1082 tx.send(MixerCommand::SetInstrument {
1083 track_id: id,
1084 instrument: Box::new(log.clone()),
1085 }).unwrap();
1086 mixer.drain_commands();
1087 log
1088 }
1089
1090 #[test]
1094 fn one_callback_applies_a_bounded_amount_of_work() {
1095 let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1096 let log = add_logging_track(&mut mixer, &tx, 0);
1097
1098 for i in 0..500 {
1099 tx.send(MixerCommand::SetParameter {
1100 track_id: 0,
1101 param_index: i % 8,
1102 value: i as f32,
1103 }).unwrap();
1104 }
1105
1106 let spent = mixer.drain_commands();
1107 assert!(
1108 spent <= WORST_CALLBACK,
1109 "one callback spent {spent} units, over the {WORST_CALLBACK} bound"
1110 );
1111 assert_eq!(
1112 log.seen().len(),
1113 COMMAND_BUDGET as usize,
1114 "a parameter costs one unit, so a full budget is exactly that many"
1115 );
1116 assert!(!mixer.command_rx.is_empty(), "the rest has to still be queued");
1117 }
1118
1119 #[test]
1122 fn nothing_is_lost_or_reordered_across_callbacks() {
1123 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1124 let log = add_logging_track(&mut mixer, &tx, 0);
1125
1126 let sent: Vec<(usize, f32)> = (0..500).map(|i| (i % 8, i as f32)).collect();
1127 for &(param_index, value) in &sent {
1128 tx.send(MixerCommand::SetParameter { track_id: 0, param_index, value }).unwrap();
1129 }
1130
1131 let mut output = vec![0.0f32; 128];
1135 let mut callbacks = 0;
1136 while !mixer.command_rx.is_empty() {
1137 mixer.process(&mut output, &[], &transport);
1138 callbacks += 1;
1139 assert!(callbacks < 100, "the drain is not making progress");
1140 }
1141 assert!(
1142 callbacks >= 500 / COMMAND_BUDGET as usize,
1143 "500 commands went through in {callbacks} callbacks, so the budget did not hold"
1144 );
1145 assert_eq!(log.seen(), sent, "the audio thread saw a different sequence");
1146 }
1147
1148 #[test]
1154 fn a_track_and_its_instrument_survive_a_budget_boundary() {
1155 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1156 let log = ParamLog::new();
1157
1158 for _ in 0..COMMAND_BUDGET {
1161 tx.send(MixerCommand::SetParameter { track_id: 99, param_index: 0, value: 0.0 })
1162 .unwrap();
1163 }
1164 let handle = Arc::new(TrackHandle::new(7, TrackKind::Instrument));
1165 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1166 tx.send(MixerCommand::SetInstrument {
1167 track_id: 7,
1168 instrument: Box::new(log.clone()),
1169 }).unwrap();
1170 tx.send(MixerCommand::SetParameter { track_id: 7, param_index: 3, value: 0.5 }).unwrap();
1171
1172 let mut output = vec![0.0f32; 128];
1173 mixer.process(&mut output, &[], &transport);
1174 assert!(mixer.tracks.is_empty(), "the budget did not stop at the parameters");
1175
1176 while !mixer.command_rx.is_empty() {
1177 mixer.process(&mut output, &[], &transport);
1178 }
1179 assert_eq!(mixer.tracks.len(), 1);
1180 assert!(mixer.tracks[0].instrument.is_some(), "the instrument never arrived");
1181 assert_eq!(
1182 log.seen(),
1183 vec![(3, 0.5)],
1184 "the parameter that follows the instrument did not reach it"
1185 );
1186 }
1187
1188 #[test]
1193 fn an_instrument_load_costs_more_than_a_parameter() {
1194 let param = MixerCommand::SetParameter { track_id: 0, param_index: 0, value: 0.0 };
1195 let load = MixerCommand::SetInstrument {
1196 track_id: 0,
1197 instrument: Box::new(FixedOutput(0.0)),
1198 };
1199 assert!(command_cost(&load) > command_cost(¶m));
1200
1201 let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1203 for id in 0..8 {
1204 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1205 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1206 }
1207 while !mixer.command_rx.is_empty() {
1208 mixer.drain_commands();
1209 }
1210 for id in 0..8 {
1211 tx.send(MixerCommand::SetInstrument {
1212 track_id: id,
1213 instrument: Box::new(FixedOutput(0.25)),
1214 }).unwrap();
1215 }
1216 mixer.drain_commands();
1217 let loaded = mixer.tracks.iter().filter(|t| t.instrument.is_some()).count();
1218 assert_eq!(loaded, (COMMAND_BUDGET / HEAVY_COMMAND) as usize);
1219 }
1220
1221 #[test]
1225 fn adding_tracks_does_not_grow_the_track_list() {
1226 let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1227 let capacity = mixer.tracks.capacity();
1228 assert!(capacity >= TRACK_CAPACITY);
1229
1230 for id in 0..TRACK_CAPACITY {
1231 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1232 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1233 }
1234 while !mixer.command_rx.is_empty() {
1235 mixer.drain_commands();
1236 }
1237 assert_eq!(mixer.tracks.len(), TRACK_CAPACITY);
1238 assert_eq!(
1239 mixer.tracks.capacity(), capacity,
1240 "the track list reallocated on the audio thread"
1241 );
1242 }
1243
1244 struct FixedOutput(f32);
1249
1250 impl Plugin for FixedOutput {
1251 fn info(&self) -> phosphor_plugin::PluginInfo {
1252 phosphor_plugin::PluginInfo {
1253 name: "Fixed".into(),
1254 version: "0".into(),
1255 author: "test".into(),
1256 category: phosphor_plugin::PluginCategory::Instrument,
1257 }
1258 }
1259 fn init(&mut self, _sample_rate: f64, _max_buffer_size: usize) {}
1260 fn process(&mut self, _inputs: &[&[f32]], outputs: &mut [&mut [f32]], _midi: &[MidiEvent]) {
1261 for ch in outputs.iter_mut() {
1262 ch.fill(self.0);
1263 }
1264 }
1265 fn parameter_count(&self) -> usize { 0 }
1266 fn parameter_info(&self, _index: usize) -> Option<phosphor_plugin::ParameterInfo> { None }
1267 fn get_parameter(&self, _index: usize) -> f32 { 0.0 }
1268 fn set_parameter(&mut self, _index: usize, _value: f32) {}
1269 fn reset(&mut self) {}
1270 }
1271
1272 fn add_fixed_track(tx: &Sender<MixerCommand>, id: usize, value: f32) -> Arc<TrackHandle> {
1273 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1274 handle.config.set_volume(1.0);
1275 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
1276 tx.send(MixerCommand::SetInstrument {
1277 track_id: id,
1278 instrument: Box::new(FixedOutput(value)),
1279 }).unwrap();
1280 handle
1281 }
1282
1283 #[test]
1286 fn master_limiter_bounds_many_loud_tracks() {
1287 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1288 for id in 0..6 {
1289 add_fixed_track(&tx, id, 0.75);
1290 }
1291 transport.play();
1292
1293 let mut output = vec![0.0f32; 512];
1294 for _ in 0..8 {
1295 mixer.process(&mut output, &[], &transport);
1296 for (i, &s) in output.iter().enumerate() {
1297 assert!(s.is_finite(), "non-finite sample at {i}");
1298 assert!(s.abs() <= 1.0, "sample {i} left the mixer at {s}");
1299 }
1300 }
1301
1302 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1304 assert!(peak > 0.8, "limiter over-attenuated, peak={peak}");
1305 }
1306
1307 #[test]
1311 fn non_finite_track_output_becomes_silence() {
1312 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1313 add_fixed_track(&tx, 0, f32::NAN);
1314 transport.play();
1315
1316 let mut output = vec![0.0f32; 512];
1317 mixer.process(&mut output, &[], &transport);
1318 assert!(output.iter().all(|s| *s == 0.0), "NaN track should render as silence");
1319
1320 tx.send(MixerCommand::RemoveTrack { track_id: 0 }).unwrap();
1323 add_fixed_track(&tx, 1, 0.5);
1324 mixer.process(&mut output, &[], &transport);
1325 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1326 assert!((peak - 0.5).abs() < 1.0e-6, "mixer did not recover, peak={peak}");
1327 }
1328
1329 #[test]
1330 fn infinite_track_output_becomes_silence() {
1331 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1332 add_fixed_track(&tx, 0, f32::INFINITY);
1333 transport.play();
1334
1335 let mut output = vec![0.0f32; 512];
1336 mixer.process(&mut output, &[], &transport);
1337 assert!(output.iter().all(|s| *s == 0.0), "infinite track should render as silence");
1338 }
1339
1340 #[test]
1344 fn limiter_is_bit_identical_below_the_ceiling() {
1345 let mut limiter = MasterLimiter::new(44_100);
1346
1347 let mut input: Vec<f32> = Vec::new();
1349 for i in 0..20_000u32 {
1350 let phase = i as f32 * 0.01;
1351 let amp = LIMITER_CEILING * (i as f32 / 20_000.0);
1352 input.push(phase.sin() * amp);
1353 input.push(phase.cos() * amp);
1354 }
1355 input.push(LIMITER_CEILING);
1356 input.push(-LIMITER_CEILING);
1357 input.push(0.0);
1358 input.push(-0.0);
1359 input.push(f32::MIN_POSITIVE);
1360 input.push(-f32::MIN_POSITIVE);
1361
1362 let mut output = input.clone();
1363 limiter.process(&mut output);
1364
1365 for (i, (a, b)) in input.iter().zip(output.iter()).enumerate() {
1366 assert_eq!(a.to_bits(), b.to_bits(), "limiter altered sample {i}: {a} -> {b}");
1367 }
1368 }
1369
1370 #[test]
1373 fn limiter_holds_the_ceiling_under_abuse() {
1374 let mut limiter = MasterLimiter::new(44_100);
1375 for amplitude in [1.0f32, 2.0, 10.0, 1.0e3, 1.0e6, 1.0e30] {
1376 let mut buf: Vec<f32> = (0..4_096)
1377 .map(|i| (i as f32 * 0.05).sin() * amplitude)
1378 .collect();
1379 limiter.process(&mut buf);
1380 for (i, &s) in buf.iter().enumerate() {
1381 assert!(s.is_finite(), "amplitude {amplitude}: sample {i} is {s}");
1382 assert!(
1383 s.abs() <= LIMITER_CEILING,
1384 "amplitude {amplitude}: sample {i} reached {s}, above the ceiling"
1385 );
1386 }
1387 }
1388 }
1389
1390 #[test]
1394 fn limiter_attack_has_no_overshoot() {
1395 let mut limiter = MasterLimiter::new(44_100);
1396 let mut buf = vec![0.0f32; 64];
1397 limiter.process(&mut buf);
1398 let mut step = vec![4.0f32; 64];
1399 limiter.process(&mut step);
1400 assert!(
1401 step[0].abs() <= LIMITER_CEILING,
1402 "first sample of the step overshot to {}",
1403 step[0]
1404 );
1405 }
1406
1407 #[test]
1410 fn limiter_release_is_gradual() {
1411 let mut limiter = MasterLimiter::new(44_100);
1412 let mut loud = vec![4.0f32; 64];
1413 limiter.process(&mut loud);
1414 let reduced = limiter.gain;
1415 assert!(reduced < 0.5, "limiter did not engage, gain={reduced}");
1416
1417 let mut quiet = vec![0.1f32; 441 * 2];
1420 limiter.process(&mut quiet);
1421 assert!(limiter.gain > reduced, "gain did not recover at all");
1422 assert!(
1423 limiter.gain < 1.0,
1424 "gain snapped back to unity within 10 ms, which is a click"
1425 );
1426
1427 let mut long = vec![0.1f32; 22_050 * 2];
1429 limiter.process(&mut long);
1430 assert!(
1431 (limiter.gain - 1.0).abs() < 1.0e-4,
1432 "gain never returned to unity: {}",
1433 limiter.gain
1434 );
1435 }
1436
1437 #[test]
1440 fn limiter_does_not_shift_the_stereo_image() {
1441 let mut limiter = MasterLimiter::new(44_100);
1442 let mut buf: Vec<f32> = Vec::new();
1444 for i in 0..1_024 {
1445 let phase = i as f32 * 0.05;
1446 buf.push(phase.sin() * 3.0);
1447 buf.push(phase.sin() * 1.5);
1448 }
1449 limiter.process(&mut buf);
1450 for frame in buf.chunks_exact(2) {
1451 if frame[1].abs() > 1.0e-4 {
1452 let ratio = frame[0] / frame[1];
1453 assert!(
1454 (ratio - 2.0).abs() < 1.0e-3,
1455 "channel balance moved: L/R = {ratio}"
1456 );
1457 }
1458 }
1459 }
1460
1461 fn loudest_dx7_voice() -> phosphor_dsp::dx7::Dx7Synth {
1468 use phosphor_dsp::dx7;
1469 let mut synth = dx7::Dx7Synth::new();
1470 let (bank, patch) = dx7::voice_knobs(147);
1471 synth.set_parameter(dx7::P_BANK, bank);
1472 synth.set_parameter(dx7::P_PATCH, patch);
1473 debug_assert_eq!(dx7::voice_name(147), "TIMPANI");
1474 synth
1475 }
1476
1477 #[test]
1481 fn master_limiter_bounds_four_loud_instrument_tracks() {
1482 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1483 for id in 0..4 {
1484 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1485 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1486 handle.config.set_volume(1.0);
1487 let synth = loudest_dx7_voice();
1488 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1489 tx.send(MixerCommand::SetInstrument {
1490 track_id: id,
1491 instrument: Box::new(synth),
1492 }).unwrap();
1493 }
1494 transport.play();
1495
1496 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1497 .iter()
1498 .map(|¬e| make_note_on(note, 127))
1499 .collect();
1500
1501 let mut output = vec![0.0f32; 512];
1502 let mut peak = 0.0f32;
1503 for block in 0..200 {
1504 output.fill(0.0);
1505 if block == 0 {
1506 mixer.process(&mut output, &chord, &transport);
1507 } else {
1508 mixer.process(&mut output, &[], &transport);
1509 }
1510 for (i, &s) in output.iter().enumerate() {
1511 assert!(s.is_finite(), "block {block} sample {i} is {s}");
1512 assert!(s.abs() <= 1.0, "block {block} sample {i} left the mixer at {s}");
1513 peak = peak.max(s.abs());
1514 }
1515 }
1516 assert!(peak > 0.5, "four loud tracks should be loud, peak={peak}");
1517 }
1518
1519 #[test]
1525 fn limiter_idle_for_the_worst_single_track() {
1526 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1527 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1528 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1529 handle.config.set_volume(1.0);
1530 let synth = loudest_dx7_voice();
1531 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1532 tx.send(MixerCommand::SetInstrument { track_id: 0, instrument: Box::new(synth) }).unwrap();
1533 transport.play();
1534
1535 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1536 .iter()
1537 .map(|¬e| make_note_on(note, 127))
1538 .collect();
1539
1540 let mut output = vec![0.0f32; 512];
1541 let mut peak = 0.0f32;
1542 for block in 0..200 {
1543 output.fill(0.0);
1544 if block == 0 {
1545 mixer.process(&mut output, &chord, &transport);
1546 } else {
1547 mixer.process(&mut output, &[], &transport);
1548 }
1549 peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0f32, f32::max));
1550 assert_eq!(
1551 mixer.limiter.gain, 1.0,
1552 "limiter engaged at block {block}, peak {peak}"
1553 );
1554 }
1555 assert!(peak > 0.3, "expected a loud chord, peak={peak}");
1556 }
1557
1558 fn worst_track_through_the_mixer(volume: f32) -> (f32, f32) {
1564 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1565 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1566 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1567 handle.config.set_volume(volume);
1568 let synth = loudest_dx7_voice();
1569 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1570 tx.send(MixerCommand::SetInstrument { track_id: 0, instrument: Box::new(synth) }).unwrap();
1571 transport.play();
1572
1573 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1574 .iter()
1575 .map(|¬e| make_note_on(note, 127))
1576 .collect();
1577
1578 let mut output = vec![0.0f32; 512];
1579 let mut peak = 0.0f32;
1580 let mut min_gain = 1.0f32;
1581 for block in 0..200 {
1582 output.fill(0.0);
1583 if block == 0 {
1584 mixer.process(&mut output, &chord, &transport);
1585 } else {
1586 mixer.process(&mut output, &[], &transport);
1587 }
1588 for &s in output.iter() {
1589 assert!(s.is_finite(), "block {block}: non-finite sample");
1590 assert!(s.abs() <= 1.0, "block {block}: sample left the mixer at {s}");
1591 peak = peak.max(s.abs());
1592 }
1593 min_gain = min_gain.min(mixer.limiter.gain);
1594 }
1595 (peak, min_gain)
1596 }
1597
1598 #[test]
1606 fn fader_below_unity_never_engages_the_limiter() {
1607 for volume in [
1608 0.25,
1609 TrackConfig::DEFAULT_VOLUME,
1610 TrackConfig::UNITY_VOLUME,
1611 ] {
1612 let (peak, min_gain) = worst_track_through_the_mixer(volume);
1613 assert_eq!(
1614 min_gain, 1.0,
1615 "limiter reduced by {:.2} dB at fader {volume} (peak {peak:.4})",
1616 20.0 * min_gain.log10()
1617 );
1618 }
1619 }
1620
1621 #[test]
1627 fn fader_makeup_gain_is_bounded_not_wasted() {
1628 let (unity_peak, _) = worst_track_through_the_mixer(TrackConfig::UNITY_VOLUME);
1629 let (max_peak, min_gain) = worst_track_through_the_mixer(TrackConfig::MAX_VOLUME);
1630
1631 assert!(
1632 max_peak <= LIMITER_CEILING,
1633 "fader at maximum let {max_peak:.4} through, above the ceiling"
1634 );
1635 assert!(
1636 max_peak >= unity_peak,
1637 "turning the fader up made the track quieter: {unity_peak:.4} -> {max_peak:.4}"
1638 );
1639 let reduction_db = -20.0 * min_gain.log10();
1642 let boost_db = 20.0 * (TrackConfig::MAX_VOLUME / TrackConfig::UNITY_VOLUME).log10();
1643 assert!(
1644 reduction_db <= boost_db,
1645 "limiter took {reduction_db:.2} dB off a {boost_db:.2} dB boost"
1646 );
1647 }
1648
1649 #[test]
1661 fn metronome_click_sits_with_the_music() {
1662 use phosphor_dsp::dx7;
1663
1664 fn render(with_track: bool, metronome: bool) -> f32 {
1665 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1666 let chord: Vec<MidiMessage> = if with_track {
1667 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1668 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1669 tx.send(MixerCommand::AddTrack {
1670 kind: TrackKind::Instrument,
1671 handle,
1672 })
1673 .unwrap();
1674 tx.send(MixerCommand::SetInstrument {
1675 track_id: 0,
1676 instrument: Box::new(dx7::Dx7Synth::new()),
1677 })
1678 .unwrap();
1679 [60u8, 64, 67].iter().map(|&n| make_note_on(n, 100)).collect()
1680 } else {
1681 Vec::new()
1682 };
1683 if metronome {
1684 transport.toggle_metronome();
1685 }
1686 transport.play();
1687
1688 let mut output = vec![0.0f32; 512];
1689 let mut peak = 0.0f32;
1690 for block in 0..200 {
1691 output.fill(0.0);
1692 if block == 0 {
1693 mixer.process(&mut output, &chord, &transport);
1694 } else {
1695 mixer.process(&mut output, &[], &transport);
1696 }
1697 peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0f32, f32::max));
1698 transport.advance(256, 44_100);
1699 }
1700 peak
1701 }
1702
1703 let music = render(true, false);
1704 let click = render(false, true);
1705 assert!(music > 0.0 && click > 0.0, "music {music}, click {click}");
1706
1707 let relative_db = 20.0 * (click / music).log10();
1708 assert!(
1709 (-12.0..=0.0).contains(&relative_db),
1710 "the click is {relative_db:.1} dB against a triad (click {click:.4}, \
1711 music {music:.4}); it has to be audible over the music without \
1712 being the loudest thing in the mix"
1713 );
1714 }
1715
1716 #[test]
1720 fn fader_scales_the_track() {
1721 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1722 let handle = add_fixed_track(&tx, 0, 0.25);
1723 transport.play();
1724
1725 let mut output = vec![0.0f32; 512];
1726 for (volume, expected) in [(0.0f32, 0.0f32), (0.5, 0.125), (1.0, 0.25), (2.0, 0.5)] {
1727 handle.config.set_volume(volume);
1728 output.fill(0.0);
1729 mixer.process(&mut output, &[], &transport);
1730 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1731 assert!(
1732 (peak - expected).abs() < 1.0e-6,
1733 "fader at {volume} gave {peak}, expected {expected}"
1734 );
1735 }
1736 }
1737
1738 fn refused(asked: u32, sample_rate: u32, max_buffer_frames: u32) -> StreamFormat {
1742 StreamFormat {
1743 sample_rate,
1744 buffer_size: Some(64),
1745 max_buffer_frames,
1746 channels: 2,
1747 sample_rate_request: Requested::Refused(asked),
1748 buffer_size_request: Requested::Granted,
1749 }
1750 }
1751
1752 #[test]
1757 fn the_mixer_runs_at_the_rate_the_device_granted() {
1758 let requested = crate::EngineConfig { buffer_size: 64, sample_rate: 44100 };
1759 let format = refused(44100, 48000, 4096);
1760 let effective = crate::EngineConfig::from(format);
1761
1762 let (_tx, rx) = mixer_command_channel();
1763 let (clip_tx, _clip_rx) = clip_snapshot_channel();
1764 let mixer = Mixer::new(
1765 rx,
1766 Arc::new(VuLevels::new()),
1767 clip_tx,
1768 effective.sample_rate,
1769 format.max_buffer_frames as usize,
1770 );
1771
1772 assert_eq!(mixer.sample_rate, 48000, "mixer must adopt the device's rate");
1773 assert_ne!(
1774 mixer.sample_rate, requested.sample_rate,
1775 "the request was 44100 and the device said 48000; taking the \
1776 request here is the 8.84%-sharp bug"
1777 );
1778 assert_eq!(mixer.max_buffer_size, 4096);
1779 }
1780
1781 #[test]
1783 fn a_device_that_agrees_leaves_the_request_alone() {
1784 let requested = crate::EngineConfig { buffer_size: 64, sample_rate: 44100 };
1785 let format = StreamFormat {
1786 sample_rate: 44100,
1787 buffer_size: Some(64),
1788 max_buffer_frames: 4096,
1789 channels: 2,
1790 sample_rate_request: Requested::Granted,
1791 buffer_size_request: Requested::Granted,
1792 };
1793 assert_eq!(crate::EngineConfig::from(format), requested);
1794 }
1795
1796 #[test]
1800 fn asking_for_nothing_builds_the_mixer_at_the_devices_rate() {
1801 let format = StreamFormat {
1802 sample_rate: 48000,
1803 buffer_size: None,
1804 max_buffer_frames: 4096,
1805 channels: 2,
1806 sample_rate_request: Requested::Unasked,
1807 buffer_size_request: Requested::Unasked,
1808 };
1809 let effective = crate::EngineConfig::from(format);
1810
1811 let (_tx, rx) = mixer_command_channel();
1812 let (clip_tx, _clip_rx) = clip_snapshot_channel();
1813 let mixer = Mixer::new(
1814 rx,
1815 Arc::new(VuLevels::new()),
1816 clip_tx,
1817 effective.sample_rate,
1818 format.max_buffer_frames as usize,
1819 );
1820 assert_eq!(mixer.sample_rate, 48000);
1821 assert_eq!(mixer.max_buffer_size, 4096);
1822 assert!(format.divergence_notice().is_none(), "following the device is not news");
1823 }
1824
1825 #[test]
1829 fn the_largest_block_the_device_promised_never_grows_a_buffer() {
1830 let max_frames = 512usize;
1831 let (tx, rx) = mixer_command_channel();
1832 let (clip_tx, _clip_rx) = clip_snapshot_channel();
1833 let mut mixer = Mixer::new(
1834 rx,
1835 Arc::new(VuLevels::new()),
1836 clip_tx,
1837 48000,
1838 max_frames,
1839 );
1840 let transport = Arc::new(Transport::new(120.0));
1841 let _handle = add_armed_synth(&tx, 0);
1842 mixer.drain_commands();
1843
1844 let before = (
1847 mixer.scratch_l.capacity(),
1848 mixer.scratch_r.capacity(),
1849 mixer.tracks[0].buf_l.capacity(),
1850 mixer.tracks[0].buf_r.capacity(),
1851 );
1852
1853 transport.play();
1854 let mut output = vec![0.0f32; max_frames * 2];
1855 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
1856
1857 let after = (
1858 mixer.scratch_l.capacity(),
1859 mixer.scratch_r.capacity(),
1860 mixer.tracks[0].buf_l.capacity(),
1861 mixer.tracks[0].buf_r.capacity(),
1862 );
1863 assert_eq!(
1864 before, after,
1865 "a block the size the device promised must fit the buffers as \
1866 allocated; growing one means the audio thread called the allocator"
1867 );
1868 }
1869
1870 #[test]
1874 fn a_steady_state_callback_does_not_allocate() {
1875 let max_frames = 512usize;
1876 let (tx, rx) = mixer_command_channel();
1877 let (clip_tx, _clip_rx) = clip_snapshot_channel();
1878 let mut mixer = Mixer::new(rx, Arc::new(VuLevels::new()), clip_tx, 48000, max_frames);
1879 let transport = Arc::new(Transport::new(120.0));
1880 let _handle = add_armed_synth(&tx, 0);
1881 mixer.drain_commands();
1882 transport.play();
1883
1884 let mut output = vec![0.0f32; max_frames * 2];
1885 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
1889
1890 let allocations = crate::alloc_count::allocations_during(|| {
1891 for _ in 0..8 {
1892 mixer.process(&mut output, &[], &transport);
1893 }
1894 });
1895 assert_eq!(allocations, 0, "Mixer::process reached the allocator");
1896 }
1897
1898 #[test]
1901 fn a_short_callback_does_not_allocate_either() {
1902 let max_frames = 512usize;
1903 let (tx, rx) = mixer_command_channel();
1904 let (clip_tx, _clip_rx) = clip_snapshot_channel();
1905 let mut mixer = Mixer::new(rx, Arc::new(VuLevels::new()), clip_tx, 48000, max_frames);
1906 let transport = Arc::new(Transport::new(120.0));
1907 let _handle = add_armed_synth(&tx, 0);
1908 mixer.drain_commands();
1909 transport.play();
1910
1911 let mut output = vec![0.0f32; 64 * 2];
1912 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
1913
1914 let allocations = crate::alloc_count::allocations_during(|| {
1915 for _ in 0..8 {
1916 mixer.process(&mut output, &[], &transport);
1917 }
1918 });
1919 assert_eq!(allocations, 0, "Mixer::process reached the allocator");
1920 }
1921}