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 LIMITER_CEILING: f32 = 0.891;
72
73const LIMITER_RELEASE_SECONDS: f32 = 0.050;
80
81struct MasterLimiter {
105 gain: f32,
107 release_coeff: f32,
109}
110
111impl MasterLimiter {
112 fn new(sample_rate: u32) -> Self {
113 let sr = (sample_rate as f32).max(1.0);
114 Self {
115 gain: 1.0,
116 release_coeff: 1.0 - (-1.0 / (LIMITER_RELEASE_SECONDS * sr)).exp(),
117 }
118 }
119
120 fn reset(&mut self) {
121 self.gain = 1.0;
122 }
123
124 fn process(&mut self, output: &mut [f32]) {
129 let mut frames = output.chunks_exact_mut(2);
130 for frame in frames.by_ref() {
131 let l = if frame[0].is_finite() { frame[0] } else { 0.0 };
136 let r = if frame[1].is_finite() { frame[1] } else { 0.0 };
137
138 let peak = l.abs().max(r.abs());
139 let target = if peak > LIMITER_CEILING {
145 (LIMITER_CEILING / peak) * (1.0 - 2.0 * f32::EPSILON)
146 } else {
147 1.0
148 };
149
150 if target < self.gain {
151 self.gain = target;
152 } else {
153 self.gain += (target - self.gain) * self.release_coeff;
154 }
155
156 frame[0] = (l * self.gain).clamp(-1.0, 1.0);
162 frame[1] = (r * self.gain).clamp(-1.0, 1.0);
163 }
164
165 for tail in frames.into_remainder() {
170 let s = if tail.is_finite() { *tail } else { 0.0 };
171 *tail = (s * self.gain).clamp(-LIMITER_CEILING, LIMITER_CEILING);
172 }
173 }
174}
175
176pub struct AudioTrack {
179 pub id: usize,
180 pub kind: TrackKind,
181 pub handle: Arc<TrackHandle>,
182 pub instrument: Option<Box<dyn Plugin>>,
183 pub clips: Vec<MidiClip>,
185 record_buf: RecordBuffer,
187 was_recording: bool,
189 last_record_tick: i64,
191 last_playback_tick: i64,
193 buf_l: Vec<f32>,
194 buf_r: Vec<f32>,
195 plugin_events: Vec<MidiEvent>,
196}
197
198impl AudioTrack {
199 pub fn new(handle: Arc<TrackHandle>, max_buffer_size: usize) -> Self {
200 Self {
201 id: handle.id,
202 kind: handle.kind,
203 handle,
204 instrument: None,
205 clips: Vec::new(),
206 record_buf: RecordBuffer::new(),
207 was_recording: false,
208 last_record_tick: -1,
209 last_playback_tick: -1,
210 buf_l: vec![0.0; max_buffer_size],
211 buf_r: vec![0.0; max_buffer_size],
212 plugin_events: Vec::with_capacity(256),
213 }
214 }
215}
216
217pub struct Mixer {
220 tracks: Vec<AudioTrack>,
221 master_vu: Arc<VuLevels>,
222 command_rx: Receiver<MixerCommand>,
223 clip_tx: Sender<ClipSnapshot>,
224 metronome: Metronome,
225 sample_rate: u32,
226 max_buffer_size: usize,
227 scratch_l: Vec<f32>,
229 scratch_r: Vec<f32>,
230 live_events: Vec<MidiEvent>,
232 limiter: MasterLimiter,
234}
235
236impl Mixer {
237 pub fn new(
238 command_rx: Receiver<MixerCommand>,
239 master_vu: Arc<VuLevels>,
240 clip_tx: Sender<ClipSnapshot>,
241 sample_rate: u32,
242 max_buffer_size: usize,
243 ) -> Self {
244 Self {
245 tracks: Vec::new(),
246 master_vu,
247 command_rx,
248 clip_tx,
249 metronome: Metronome::new(sample_rate as f64),
250 sample_rate,
251 max_buffer_size,
252 scratch_l: vec![0.0; max_buffer_size],
253 scratch_r: vec![0.0; max_buffer_size],
254 live_events: Vec::with_capacity(256),
255 limiter: MasterLimiter::new(sample_rate),
256 }
257 }
258
259 pub fn process(&mut self, output: &mut [f32], midi_messages: &[MidiMessage], transport: &Transport) {
261 self.drain_commands();
262
263 let num_frames = output.len() / 2;
264 let playing = transport.is_playing();
265 let recording = transport.is_recording();
266 let looping = transport.is_looping();
267 let current_tick = transport.position_ticks();
268 let bpm = transport.tempo_bpm();
269 let ticks_per_sample = (bpm * Transport::PPQ as f64) / (60.0 * self.sample_rate as f64);
270 let buffer_ticks = (num_frames as f64 * ticks_per_sample) as i64;
271 let loop_end = transport.loop_end();
272
273 self.live_events.clear();
275 for msg in midi_messages {
276 if let Some(ev) = midi_to_plugin_event(msg) {
277 self.live_events.push(ev);
278 }
279 }
280
281 let any_solo = self.tracks.iter().any(|t| t.handle.config.is_soloed());
282
283 let mut master_l = std::mem::take(&mut self.scratch_l);
286 let mut master_r = std::mem::take(&mut self.scratch_r);
287 let live_events = std::mem::take(&mut self.live_events);
288 if master_l.len() < num_frames {
289 master_l.resize(num_frames, 0.0);
290 master_r.resize(num_frames, 0.0);
291 }
292 master_l[..num_frames].fill(0.0);
293 master_r[..num_frames].fill(0.0);
294
295 let clip_tx = &self.clip_tx;
296
297 for track in &mut self.tracks {
298 if track.buf_l.len() < num_frames {
299 track.buf_l.resize(num_frames, 0.0);
300 track.buf_r.resize(num_frames, 0.0);
301 }
302 track.buf_l[..num_frames].fill(0.0);
303 track.buf_r[..num_frames].fill(0.0);
304 track.plugin_events.clear();
305
306 let is_midi_active = track.kind == TrackKind::Instrument
307 && track.handle.config.is_midi_active();
308 let is_armed = track.handle.config.is_armed();
309 let should_record = playing && recording && is_armed && is_midi_active;
310
311 if should_record && !track.was_recording {
313 let rec_start = if looping { transport.loop_start() } else { current_tick };
316 track.record_buf.start(rec_start);
317 tracing::debug!("rec start track={} tick={}", track.id, current_tick);
318 }
319
320 if should_record && track.was_recording && looping
322 && track.record_buf.is_active() && track.last_record_tick >= 0
323 && current_tick < track.last_record_tick
324 {
325 commit_recording(track, loop_end, clip_tx);
326 track.record_buf.start(transport.loop_start());
329 }
330 if should_record {
331 track.last_record_tick = current_tick;
332 }
333
334 if !should_record && track.was_recording {
336 commit_recording(track, current_tick, clip_tx);
337 }
338 track.was_recording = should_record;
339
340 if is_midi_active {
342 for ev in &live_events {
343 track.plugin_events.push(*ev);
344 if should_record {
345 let event_tick = current_tick
346 + (ev.sample_offset as f64 * ticks_per_sample) as i64;
347 track.record_buf.record(event_tick, ev.status, ev.data1, ev.data2);
348 }
349 }
350 }
351
352 if playing && !track.clips.is_empty() {
354 let from = current_tick;
355 let to = current_tick + buffer_ticks;
356
357 let just_wrapped = looping && track.last_playback_tick >= 0
360 && current_tick < track.last_playback_tick;
361 track.last_playback_tick = current_tick;
362
363 if just_wrapped {
364 let wrap_start = transport.loop_start();
366 for clip in &track.clips {
367 for (tick_offset, event) in clip.events_in_range(wrap_start, to) {
368 let sample_offset = (tick_offset as f64 / ticks_per_sample) as u32;
369 track.plugin_events.push(MidiEvent {
370 sample_offset: sample_offset.min(num_frames as u32 - 1),
371 status: event.status,
372 data1: event.data1,
373 data2: event.data2,
374 });
375 }
376 }
377 } else {
378 for clip in &track.clips {
379 for (tick_offset, event) in clip.events_in_range(from, to) {
380 let sample_offset = (tick_offset as f64 / ticks_per_sample) as u32;
381 track.plugin_events.push(MidiEvent {
382 sample_offset: sample_offset.min(num_frames as u32 - 1),
383 status: event.status,
384 data1: event.data1,
385 data2: event.data2,
386 });
387 }
388 }
389 }
390 track.plugin_events.sort_by_key(|e| e.sample_offset);
391 }
392
393 if playing {
395 track.last_record_tick = current_tick;
396 }
397
398 if let Some(ref mut instrument) = track.instrument {
400 let out_l = &mut track.buf_l[..num_frames];
401 let out_r = &mut track.buf_r[..num_frames];
402 let mut out_slices: [&mut [f32]; 2] = [out_l, out_r];
403 instrument.process(&[], &mut out_slices, &track.plugin_events);
404 }
405
406 let muted = track.handle.config.is_muted();
408 let soloed = track.handle.config.is_soloed();
409 let audible = !muted && (!any_solo || soloed);
410 let volume = track.handle.config.get_volume();
411
412 let mut peak_l = 0.0f32;
413 let mut peak_r = 0.0f32;
414 for i in 0..num_frames {
415 peak_l = peak_l.max(track.buf_l[i].abs());
416 peak_r = peak_r.max(track.buf_r[i].abs());
417 }
418
419 let (old_l, old_r) = track.handle.vu.get();
420 let decay = 0.85f32;
421 track.handle.vu.set(
422 if peak_l > old_l { peak_l } else { old_l * decay },
423 if peak_r > old_r { peak_r } else { old_r * decay },
424 );
425
426 if audible {
427 for i in 0..num_frames {
428 master_l[i] += track.buf_l[i] * volume;
429 master_r[i] += track.buf_r[i] * volume;
430 }
431 }
432 }
433
434 for i in 0..num_frames {
436 output[i * 2] = master_l[i];
437 output[i * 2 + 1] = master_r[i];
438 }
439
440 self.scratch_l = master_l;
442 self.scratch_r = master_r;
443 self.live_events = live_events;
444
445 self.metronome.process(output, transport);
447
448 self.limiter.process(output);
453
454 let mut mp_l = 0.0f32;
457 let mut mp_r = 0.0f32;
458 for i in 0..num_frames {
459 mp_l = mp_l.max(output[i * 2].abs());
460 mp_r = mp_r.max(output[i * 2 + 1].abs());
461 }
462
463 let (old_l, old_r) = self.master_vu.get();
464 let decay = 0.85f32;
465 self.master_vu.set(
466 if mp_l > old_l { mp_l } else { old_l * decay },
467 if mp_r > old_r { mp_r } else { old_r * decay },
468 );
469 }
470
471 pub fn reset_all(&mut self) {
472 let clip_tx = &self.clip_tx;
473 for track in &mut self.tracks {
474 if let Some(ref mut inst) = track.instrument {
475 inst.reset();
476 }
477 track.handle.vu.set(0.0, 0.0);
478 if track.record_buf.is_active() && track.was_recording {
480 let end_tick = track.last_record_tick.max(0);
481 commit_recording(track, end_tick, clip_tx);
482 } else if track.record_buf.is_active() {
483 track.record_buf.discard();
484 }
485 track.was_recording = false;
486 track.last_playback_tick = -1;
487 }
488 self.metronome.reset();
489 self.limiter.reset();
490 }
491
492 fn drain_commands(&mut self) {
493 while let Ok(cmd) = self.command_rx.try_recv() {
494 match cmd {
495 MixerCommand::AddTrack { kind: _, handle } => {
496 let track = AudioTrack::new(handle, self.max_buffer_size);
497 self.tracks.push(track);
498 }
499 MixerCommand::SetInstrument { track_id, mut instrument } => {
500 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
501 instrument.init(self.sample_rate as f64, self.max_buffer_size);
502 track.instrument = Some(instrument);
503 }
504 }
505 MixerCommand::RemoveTrack { track_id } => {
506 self.tracks.retain(|t| t.id != track_id);
507 }
508 MixerCommand::SetParameter { track_id, param_index, value } => {
509 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
510 if let Some(ref mut inst) = track.instrument {
511 inst.set_parameter(param_index, value);
512 }
513 }
514 }
515 MixerCommand::CreateClip { track_id, start_tick, length_ticks } => {
516 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
517 track.clips.push(MidiClip::new(start_tick, length_ticks, Vec::new()));
518 }
519 }
520 MixerCommand::UpdateClip { track_id, clip_index, events } => {
521 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
522 if let Some(clip) = track.clips.get_mut(clip_index) {
523 clip.events = events;
524 clip.events.sort_by_key(|e| e.tick);
525 }
526 }
527 }
528 MixerCommand::UpdateClipPosition { track_id, clip_index, start_tick, length_ticks } => {
529 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
530 if let Some(clip) = track.clips.get_mut(clip_index) {
531 clip.start_tick = start_tick;
532 clip.length_ticks = length_ticks;
533 }
534 }
535 }
536 MixerCommand::RemoveClip { track_id, clip_index } => {
537 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
538 if clip_index < track.clips.len() {
539 track.clips.remove(clip_index);
540 }
541 }
542 }
543 }
544 }
545 }
546}
547
548fn commit_recording(track: &mut AudioTrack, end_tick: i64, clip_tx: &Sender<ClipSnapshot>) {
550 if let Some(clip) = track.record_buf.commit(end_tick) {
551 let idx = track.clips.len();
552 tracing::debug!(
553 "rec commit track={}: {} events, ticks {}..{}",
554 track.id, clip.events.len(), clip.start_tick, clip.end_tick()
555 );
556 let snapshot = ClipSnapshot::from_clip(track.id, idx, &clip);
557 track.clips.push(clip);
558 let _ = clip_tx.send(snapshot);
559 }
560}
561
562pub fn midi_to_plugin_event(msg: &MidiMessage) -> Option<MidiEvent> {
563 use phosphor_midi::message::MidiMessageType;
564 match msg.message_type {
565 MidiMessageType::NoteOn { .. }
566 | MidiMessageType::NoteOff { .. }
567 | MidiMessageType::ControlChange { .. }
568 | MidiMessageType::PitchBend { .. } => Some(MidiEvent {
569 sample_offset: 0,
570 status: msg.raw[0],
571 data1: msg.raw[1],
572 data2: msg.raw[2],
573 }),
574 _ => None,
575 }
576}
577
578pub fn mixer_command_channel() -> (Sender<MixerCommand>, Receiver<MixerCommand>) {
579 crossbeam_channel::unbounded()
580}
581
582pub fn clip_snapshot_channel() -> (Sender<ClipSnapshot>, Receiver<ClipSnapshot>) {
584 crossbeam_channel::unbounded()
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590 use crate::project::TrackConfig;
591 use phosphor_dsp::synth::PhosphorSynth;
592 use phosphor_midi::message::{MidiMessage, MidiMessageType};
593
594 fn make_note_on(note: u8, vel: u8) -> MidiMessage {
595 MidiMessage {
596 timestamp: Some(0),
597 message_type: MidiMessageType::NoteOn { channel: 0, note, velocity: vel },
598 raw: [0x90, note, vel],
599 len: 3,
600 }
601 }
602
603 fn make_note_off(note: u8) -> MidiMessage {
604 MidiMessage {
605 timestamp: Some(0),
606 message_type: MidiMessageType::NoteOff { channel: 0, note, velocity: 0 },
607 raw: [0x80, note, 0],
608 len: 3,
609 }
610 }
611
612 fn setup_mixer() -> (Mixer, Sender<MixerCommand>, Receiver<ClipSnapshot>, Arc<Transport>) {
613 let (tx, rx) = mixer_command_channel();
614 let (clip_tx, clip_rx) = clip_snapshot_channel();
615 let master_vu = Arc::new(VuLevels::new());
616 let transport = Arc::new(Transport::new(120.0));
617 let mixer = Mixer::new(rx, master_vu, clip_tx, 44100, 256);
618 (mixer, tx, clip_rx, transport)
619 }
620
621 fn add_armed_synth(tx: &Sender<MixerCommand>, id: usize) -> Arc<TrackHandle> {
622 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
623 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
624 handle.config.armed.store(true, std::sync::atomic::Ordering::Relaxed);
625 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
626 tx.send(MixerCommand::SetInstrument { track_id: id, instrument: Box::new(PhosphorSynth::new()) }).unwrap();
627 handle
628 }
629
630 #[test]
631 fn mixer_empty_output() {
632 let (mut mixer, _tx, _clip_rx, transport) = setup_mixer();
633 let mut output = vec![0.0f32; 128];
634 mixer.process(&mut output, &[], &transport);
635 assert!(output.iter().all(|&s| s == 0.0));
636 }
637
638 #[test]
639 fn mixer_live_midi_produces_sound() {
640 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
641 let _handle = add_armed_synth(&tx, 0);
642 transport.play();
643
644 let midi = vec![make_note_on(60, 100)];
645 let mut output = vec![0.0f32; 512];
646 mixer.process(&mut output, &midi, &transport);
647
648 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
649 assert!(peak > 0.001, "Should produce sound, peak={peak}");
652 }
653
654 #[test]
655 fn mixer_records_midi_clip() {
656 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
657 let _handle = add_armed_synth(&tx, 0);
658 transport.play();
659 transport.toggle_record();
660
661 let midi = vec![make_note_on(60, 100)];
663 let mut output = vec![0.0f32; 512];
664 mixer.process(&mut output, &midi, &transport);
665
666 let midi = vec![make_note_off(60)];
668 mixer.process(&mut output, &midi, &transport);
669
670 transport.toggle_record();
672 mixer.process(&mut output, &[], &transport);
673
674 let snap = clip_rx.try_recv().expect("Should receive clip snapshot");
676 assert_eq!(snap.track_id, 0);
677 assert!(snap.event_count >= 2, "Should have note on + off, got {}", snap.event_count);
678 assert!(!snap.notes.is_empty(), "Should have parsed notes");
679 }
680
681 #[test]
682 fn mixer_plays_back_recorded_clip() {
683 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
684 let _handle = add_armed_synth(&tx, 0);
685 transport.play();
686 transport.toggle_record();
687
688 let midi = vec![make_note_on(60, 100)];
690 let mut output = vec![0.0f32; 512];
691 mixer.process(&mut output, &midi, &transport);
692
693 let midi = vec![make_note_off(60)];
694 mixer.process(&mut output, &midi, &transport);
695
696 transport.toggle_record();
698 mixer.process(&mut output, &[], &transport);
699
700 transport.stop();
702
703 transport.play();
705 output.fill(0.0);
706 mixer.process(&mut output, &[], &transport);
707
708 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
709 assert!(peak > 0.001, "Playback should produce sound, peak={peak}");
710 }
711
712 #[test]
713 fn mixer_mute_silences() {
714 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
715 let handle = add_armed_synth(&tx, 0);
716 handle.config.muted.store(true, std::sync::atomic::Ordering::Relaxed);
717 transport.play();
718
719 let midi = vec![make_note_on(60, 100)];
720 let mut output = vec![0.0f32; 512];
721 mixer.process(&mut output, &midi, &transport);
722
723 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
724 assert!(peak == 0.0, "Muted track should be silent, peak={peak}");
725 }
726
727 #[test]
728 fn mixer_no_record_when_not_armed() {
729 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
730 let handle = add_armed_synth(&tx, 0);
731 handle.config.armed.store(false, std::sync::atomic::Ordering::Relaxed);
732 transport.play();
733 transport.toggle_record();
734
735 let midi = vec![make_note_on(60, 100)];
736 let mut output = vec![0.0f32; 512];
737 mixer.process(&mut output, &midi, &transport);
738
739 transport.toggle_record();
740 mixer.process(&mut output, &[], &transport);
741
742 assert!(clip_rx.try_recv().is_err(), "Should not record when not armed");
743 }
744
745 #[test]
746 fn mixer_reset_commits_recording() {
747 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
748 let _handle = add_armed_synth(&tx, 0);
749 transport.play();
750 transport.toggle_record();
751
752 let midi = vec![make_note_on(60, 100)];
753 let mut output = vec![0.0f32; 512];
754 mixer.process(&mut output, &midi, &transport);
755
756 mixer.reset_all();
757
758 assert!(clip_rx.try_recv().is_ok(), "Reset should commit active recording");
760 }
761
762 #[test]
763 fn end_to_end_record_and_playback() {
764 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
767 let _handle = add_armed_synth(&tx, 0);
768 let sr = 44100u32;
769 let buf_frames = 256;
770 let buf_samples = buf_frames * 2; transport.toggle_record();
774 transport.play();
775
776 let mut output = vec![0.0f32; buf_samples];
778 for _ in 0..4 {
779 mixer.process(&mut output, &[], &transport);
780 transport.advance(buf_frames as u32, sr);
781 }
782
783 let midi = vec![make_note_on(60, 100)];
785 mixer.process(&mut output, &midi, &transport);
786 let peak_during = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
787 assert!(peak_during > 0.001, "Should hear note during recording (monitoring)");
788 transport.advance(buf_frames as u32, sr);
789
790 for _ in 0..8 {
792 output.fill(0.0);
793 mixer.process(&mut output, &[], &transport);
794 transport.advance(buf_frames as u32, sr);
795 }
796
797 let midi = vec![make_note_off(60)];
799 mixer.process(&mut output, &midi, &transport);
800 transport.advance(buf_frames as u32, sr);
801
802 for _ in 0..4 {
804 output.fill(0.0);
805 mixer.process(&mut output, &[], &transport);
806 transport.advance(buf_frames as u32, sr);
807 }
808
809 transport.toggle_record();
811 mixer.process(&mut output, &[], &transport);
812 transport.advance(buf_frames as u32, sr);
813
814 let snap = clip_rx.try_recv().expect("Should receive clip snapshot after stopping record");
816 assert!(snap.event_count >= 2, "Clip should have note on + off");
817 assert!(!snap.notes.is_empty(), "Clip should have parsed notes");
818
819 transport.stop();
821
822 transport.play();
824
825 for _ in 0..4 {
828 output.fill(0.0);
829 mixer.process(&mut output, &[], &transport);
830 transport.advance(buf_frames as u32, sr);
831 }
832
833 output.fill(0.0);
835 mixer.process(&mut output, &[], &transport);
836 let peak_playback = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
837 assert!(peak_playback > 0.001, "Playback should produce sound at the recorded position, peak={peak_playback}");
838 }
839
840 #[test]
841 fn loop_record_commits_on_wrap() {
842 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
843 let _handle = add_armed_synth(&tx, 0);
844 let sr = 44100u32;
845 let buf_frames = 256u32;
846
847 transport.set_loop_bars(1, 1);
849 transport.start_loop_record();
850
851 let mut output = vec![0.0f32; buf_frames as usize * 2];
852
853 let midi = vec![make_note_on(60, 100)];
855 mixer.process(&mut output, &midi, &transport);
856 transport.advance(buf_frames, sr);
857
858 for _ in 0..5 {
860 mixer.process(&mut output, &[], &transport);
861 transport.advance(buf_frames, sr);
862 }
863 let midi = vec![make_note_off(60)];
864 mixer.process(&mut output, &midi, &transport);
865 transport.advance(buf_frames, sr);
866
867 for _ in 0..400 {
870 mixer.process(&mut output, &[], &transport);
871 transport.advance(buf_frames, sr);
872
873 if let Ok(snap) = clip_rx.try_recv() {
874 assert!(snap.event_count >= 2, "Clip should have events, got {}", snap.event_count);
875 assert!(!snap.notes.is_empty(), "Clip should have notes");
876 transport.stop_loop_record();
878 return;
879 }
880 }
881
882 panic!("Recording should have committed when the loop wrapped");
883 }
884
885 #[test]
886 fn loop_playback_after_record() {
887 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
888 let _handle = add_armed_synth(&tx, 0);
889 let sr = 44100u32;
890 let bf = 256u32;
891
892 transport.set_loop_bars(1, 1);
894 transport.start_loop_record();
895
896 let mut output = vec![0.0f32; bf as usize * 2];
897
898 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
900 transport.advance(bf, sr);
901 for _ in 0..3 {
902 mixer.process(&mut output, &[], &transport);
903 transport.advance(bf, sr);
904 }
905 mixer.process(&mut output, &[make_note_off(60)], &transport);
906 transport.advance(bf, sr);
907
908 for _ in 0..200 {
910 mixer.process(&mut output, &[], &transport);
911 transport.advance(bf, sr);
912 if clip_rx.try_recv().is_ok() { break; }
913 }
914
915 transport.stop_loop_record();
917 transport.set_position(0);
918
919 transport.toggle_loop(); transport.play();
922
923 output.fill(0.0);
924 mixer.process(&mut output, &[], &transport);
925 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
926 assert!(peak > 0.001, "Should hear playback, peak={peak}");
927 }
928
929 struct FixedOutput(f32);
934
935 impl Plugin for FixedOutput {
936 fn info(&self) -> phosphor_plugin::PluginInfo {
937 phosphor_plugin::PluginInfo {
938 name: "Fixed".into(),
939 version: "0".into(),
940 author: "test".into(),
941 category: phosphor_plugin::PluginCategory::Instrument,
942 }
943 }
944 fn init(&mut self, _sample_rate: f64, _max_buffer_size: usize) {}
945 fn process(&mut self, _inputs: &[&[f32]], outputs: &mut [&mut [f32]], _midi: &[MidiEvent]) {
946 for ch in outputs.iter_mut() {
947 ch.fill(self.0);
948 }
949 }
950 fn parameter_count(&self) -> usize { 0 }
951 fn parameter_info(&self, _index: usize) -> Option<phosphor_plugin::ParameterInfo> { None }
952 fn get_parameter(&self, _index: usize) -> f32 { 0.0 }
953 fn set_parameter(&mut self, _index: usize, _value: f32) {}
954 fn reset(&mut self) {}
955 }
956
957 fn add_fixed_track(tx: &Sender<MixerCommand>, id: usize, value: f32) -> Arc<TrackHandle> {
958 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
959 handle.config.set_volume(1.0);
960 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
961 tx.send(MixerCommand::SetInstrument {
962 track_id: id,
963 instrument: Box::new(FixedOutput(value)),
964 }).unwrap();
965 handle
966 }
967
968 #[test]
971 fn master_limiter_bounds_many_loud_tracks() {
972 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
973 for id in 0..6 {
974 add_fixed_track(&tx, id, 0.75);
975 }
976 transport.play();
977
978 let mut output = vec![0.0f32; 512];
979 for _ in 0..8 {
980 mixer.process(&mut output, &[], &transport);
981 for (i, &s) in output.iter().enumerate() {
982 assert!(s.is_finite(), "non-finite sample at {i}");
983 assert!(s.abs() <= 1.0, "sample {i} left the mixer at {s}");
984 }
985 }
986
987 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
989 assert!(peak > 0.8, "limiter over-attenuated, peak={peak}");
990 }
991
992 #[test]
996 fn non_finite_track_output_becomes_silence() {
997 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
998 add_fixed_track(&tx, 0, f32::NAN);
999 transport.play();
1000
1001 let mut output = vec![0.0f32; 512];
1002 mixer.process(&mut output, &[], &transport);
1003 assert!(output.iter().all(|s| *s == 0.0), "NaN track should render as silence");
1004
1005 tx.send(MixerCommand::RemoveTrack { track_id: 0 }).unwrap();
1008 add_fixed_track(&tx, 1, 0.5);
1009 mixer.process(&mut output, &[], &transport);
1010 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1011 assert!((peak - 0.5).abs() < 1.0e-6, "mixer did not recover, peak={peak}");
1012 }
1013
1014 #[test]
1015 fn infinite_track_output_becomes_silence() {
1016 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1017 add_fixed_track(&tx, 0, f32::INFINITY);
1018 transport.play();
1019
1020 let mut output = vec![0.0f32; 512];
1021 mixer.process(&mut output, &[], &transport);
1022 assert!(output.iter().all(|s| *s == 0.0), "infinite track should render as silence");
1023 }
1024
1025 #[test]
1029 fn limiter_is_bit_identical_below_the_ceiling() {
1030 let mut limiter = MasterLimiter::new(44_100);
1031
1032 let mut input: Vec<f32> = Vec::new();
1034 for i in 0..20_000u32 {
1035 let phase = i as f32 * 0.01;
1036 let amp = LIMITER_CEILING * (i as f32 / 20_000.0);
1037 input.push(phase.sin() * amp);
1038 input.push(phase.cos() * amp);
1039 }
1040 input.push(LIMITER_CEILING);
1041 input.push(-LIMITER_CEILING);
1042 input.push(0.0);
1043 input.push(-0.0);
1044 input.push(f32::MIN_POSITIVE);
1045 input.push(-f32::MIN_POSITIVE);
1046
1047 let mut output = input.clone();
1048 limiter.process(&mut output);
1049
1050 for (i, (a, b)) in input.iter().zip(output.iter()).enumerate() {
1051 assert_eq!(a.to_bits(), b.to_bits(), "limiter altered sample {i}: {a} -> {b}");
1052 }
1053 }
1054
1055 #[test]
1058 fn limiter_holds_the_ceiling_under_abuse() {
1059 let mut limiter = MasterLimiter::new(44_100);
1060 for amplitude in [1.0f32, 2.0, 10.0, 1.0e3, 1.0e6, 1.0e30] {
1061 let mut buf: Vec<f32> = (0..4_096)
1062 .map(|i| (i as f32 * 0.05).sin() * amplitude)
1063 .collect();
1064 limiter.process(&mut buf);
1065 for (i, &s) in buf.iter().enumerate() {
1066 assert!(s.is_finite(), "amplitude {amplitude}: sample {i} is {s}");
1067 assert!(
1068 s.abs() <= LIMITER_CEILING,
1069 "amplitude {amplitude}: sample {i} reached {s}, above the ceiling"
1070 );
1071 }
1072 }
1073 }
1074
1075 #[test]
1079 fn limiter_attack_has_no_overshoot() {
1080 let mut limiter = MasterLimiter::new(44_100);
1081 let mut buf = vec![0.0f32; 64];
1082 limiter.process(&mut buf);
1083 let mut step = vec![4.0f32; 64];
1084 limiter.process(&mut step);
1085 assert!(
1086 step[0].abs() <= LIMITER_CEILING,
1087 "first sample of the step overshot to {}",
1088 step[0]
1089 );
1090 }
1091
1092 #[test]
1095 fn limiter_release_is_gradual() {
1096 let mut limiter = MasterLimiter::new(44_100);
1097 let mut loud = vec![4.0f32; 64];
1098 limiter.process(&mut loud);
1099 let reduced = limiter.gain;
1100 assert!(reduced < 0.5, "limiter did not engage, gain={reduced}");
1101
1102 let mut quiet = vec![0.1f32; 441 * 2];
1105 limiter.process(&mut quiet);
1106 assert!(limiter.gain > reduced, "gain did not recover at all");
1107 assert!(
1108 limiter.gain < 1.0,
1109 "gain snapped back to unity within 10 ms, which is a click"
1110 );
1111
1112 let mut long = vec![0.1f32; 22_050 * 2];
1114 limiter.process(&mut long);
1115 assert!(
1116 (limiter.gain - 1.0).abs() < 1.0e-4,
1117 "gain never returned to unity: {}",
1118 limiter.gain
1119 );
1120 }
1121
1122 #[test]
1125 fn limiter_does_not_shift_the_stereo_image() {
1126 let mut limiter = MasterLimiter::new(44_100);
1127 let mut buf: Vec<f32> = Vec::new();
1129 for i in 0..1_024 {
1130 let phase = i as f32 * 0.05;
1131 buf.push(phase.sin() * 3.0);
1132 buf.push(phase.sin() * 1.5);
1133 }
1134 limiter.process(&mut buf);
1135 for frame in buf.chunks_exact(2) {
1136 if frame[1].abs() > 1.0e-4 {
1137 let ratio = frame[0] / frame[1];
1138 assert!(
1139 (ratio - 2.0).abs() < 1.0e-3,
1140 "channel balance moved: L/R = {ratio}"
1141 );
1142 }
1143 }
1144 }
1145
1146 fn loudest_dx7_voice() -> phosphor_dsp::dx7::Dx7Synth {
1153 use phosphor_dsp::dx7;
1154 let mut synth = dx7::Dx7Synth::new();
1155 let (bank, patch) = dx7::voice_knobs(147);
1156 synth.set_parameter(dx7::P_BANK, bank);
1157 synth.set_parameter(dx7::P_PATCH, patch);
1158 debug_assert_eq!(dx7::voice_name(147), "TIMPANI");
1159 synth
1160 }
1161
1162 #[test]
1166 fn master_limiter_bounds_four_loud_instrument_tracks() {
1167 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1168 for id in 0..4 {
1169 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1170 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1171 handle.config.set_volume(1.0);
1172 let synth = loudest_dx7_voice();
1173 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1174 tx.send(MixerCommand::SetInstrument {
1175 track_id: id,
1176 instrument: Box::new(synth),
1177 }).unwrap();
1178 }
1179 transport.play();
1180
1181 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1182 .iter()
1183 .map(|¬e| make_note_on(note, 127))
1184 .collect();
1185
1186 let mut output = vec![0.0f32; 512];
1187 let mut peak = 0.0f32;
1188 for block in 0..200 {
1189 output.fill(0.0);
1190 if block == 0 {
1191 mixer.process(&mut output, &chord, &transport);
1192 } else {
1193 mixer.process(&mut output, &[], &transport);
1194 }
1195 for (i, &s) in output.iter().enumerate() {
1196 assert!(s.is_finite(), "block {block} sample {i} is {s}");
1197 assert!(s.abs() <= 1.0, "block {block} sample {i} left the mixer at {s}");
1198 peak = peak.max(s.abs());
1199 }
1200 }
1201 assert!(peak > 0.5, "four loud tracks should be loud, peak={peak}");
1202 }
1203
1204 #[test]
1210 fn limiter_idle_for_the_worst_single_track() {
1211 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1212 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1213 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1214 handle.config.set_volume(1.0);
1215 let synth = loudest_dx7_voice();
1216 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1217 tx.send(MixerCommand::SetInstrument { track_id: 0, instrument: Box::new(synth) }).unwrap();
1218 transport.play();
1219
1220 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1221 .iter()
1222 .map(|¬e| make_note_on(note, 127))
1223 .collect();
1224
1225 let mut output = vec![0.0f32; 512];
1226 let mut peak = 0.0f32;
1227 for block in 0..200 {
1228 output.fill(0.0);
1229 if block == 0 {
1230 mixer.process(&mut output, &chord, &transport);
1231 } else {
1232 mixer.process(&mut output, &[], &transport);
1233 }
1234 peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0f32, f32::max));
1235 assert_eq!(
1236 mixer.limiter.gain, 1.0,
1237 "limiter engaged at block {block}, peak {peak}"
1238 );
1239 }
1240 assert!(peak > 0.3, "expected a loud chord, peak={peak}");
1241 }
1242
1243 fn worst_track_through_the_mixer(volume: f32) -> (f32, f32) {
1249 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1250 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1251 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1252 handle.config.set_volume(volume);
1253 let synth = loudest_dx7_voice();
1254 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1255 tx.send(MixerCommand::SetInstrument { track_id: 0, instrument: Box::new(synth) }).unwrap();
1256 transport.play();
1257
1258 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1259 .iter()
1260 .map(|¬e| make_note_on(note, 127))
1261 .collect();
1262
1263 let mut output = vec![0.0f32; 512];
1264 let mut peak = 0.0f32;
1265 let mut min_gain = 1.0f32;
1266 for block in 0..200 {
1267 output.fill(0.0);
1268 if block == 0 {
1269 mixer.process(&mut output, &chord, &transport);
1270 } else {
1271 mixer.process(&mut output, &[], &transport);
1272 }
1273 for &s in output.iter() {
1274 assert!(s.is_finite(), "block {block}: non-finite sample");
1275 assert!(s.abs() <= 1.0, "block {block}: sample left the mixer at {s}");
1276 peak = peak.max(s.abs());
1277 }
1278 min_gain = min_gain.min(mixer.limiter.gain);
1279 }
1280 (peak, min_gain)
1281 }
1282
1283 #[test]
1291 fn fader_below_unity_never_engages_the_limiter() {
1292 for volume in [
1293 0.25,
1294 TrackConfig::DEFAULT_VOLUME,
1295 TrackConfig::UNITY_VOLUME,
1296 ] {
1297 let (peak, min_gain) = worst_track_through_the_mixer(volume);
1298 assert_eq!(
1299 min_gain, 1.0,
1300 "limiter reduced by {:.2} dB at fader {volume} (peak {peak:.4})",
1301 20.0 * min_gain.log10()
1302 );
1303 }
1304 }
1305
1306 #[test]
1312 fn fader_makeup_gain_is_bounded_not_wasted() {
1313 let (unity_peak, _) = worst_track_through_the_mixer(TrackConfig::UNITY_VOLUME);
1314 let (max_peak, min_gain) = worst_track_through_the_mixer(TrackConfig::MAX_VOLUME);
1315
1316 assert!(
1317 max_peak <= LIMITER_CEILING,
1318 "fader at maximum let {max_peak:.4} through, above the ceiling"
1319 );
1320 assert!(
1321 max_peak >= unity_peak,
1322 "turning the fader up made the track quieter: {unity_peak:.4} -> {max_peak:.4}"
1323 );
1324 let reduction_db = -20.0 * min_gain.log10();
1327 let boost_db = 20.0 * (TrackConfig::MAX_VOLUME / TrackConfig::UNITY_VOLUME).log10();
1328 assert!(
1329 reduction_db <= boost_db,
1330 "limiter took {reduction_db:.2} dB off a {boost_db:.2} dB boost"
1331 );
1332 }
1333
1334 #[test]
1346 fn metronome_click_sits_with_the_music() {
1347 use phosphor_dsp::dx7;
1348
1349 fn render(with_track: bool, metronome: bool) -> f32 {
1350 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1351 let chord: Vec<MidiMessage> = if with_track {
1352 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1353 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1354 tx.send(MixerCommand::AddTrack {
1355 kind: TrackKind::Instrument,
1356 handle,
1357 })
1358 .unwrap();
1359 tx.send(MixerCommand::SetInstrument {
1360 track_id: 0,
1361 instrument: Box::new(dx7::Dx7Synth::new()),
1362 })
1363 .unwrap();
1364 [60u8, 64, 67].iter().map(|&n| make_note_on(n, 100)).collect()
1365 } else {
1366 Vec::new()
1367 };
1368 if metronome {
1369 transport.toggle_metronome();
1370 }
1371 transport.play();
1372
1373 let mut output = vec![0.0f32; 512];
1374 let mut peak = 0.0f32;
1375 for block in 0..200 {
1376 output.fill(0.0);
1377 if block == 0 {
1378 mixer.process(&mut output, &chord, &transport);
1379 } else {
1380 mixer.process(&mut output, &[], &transport);
1381 }
1382 peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0f32, f32::max));
1383 transport.advance(256, 44_100);
1384 }
1385 peak
1386 }
1387
1388 let music = render(true, false);
1389 let click = render(false, true);
1390 assert!(music > 0.0 && click > 0.0, "music {music}, click {click}");
1391
1392 let relative_db = 20.0 * (click / music).log10();
1393 assert!(
1394 (-12.0..=0.0).contains(&relative_db),
1395 "the click is {relative_db:.1} dB against a triad (click {click:.4}, \
1396 music {music:.4}); it has to be audible over the music without \
1397 being the loudest thing in the mix"
1398 );
1399 }
1400
1401 #[test]
1405 fn fader_scales_the_track() {
1406 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1407 let handle = add_fixed_track(&tx, 0, 0.25);
1408 transport.play();
1409
1410 let mut output = vec![0.0f32; 512];
1411 for (volume, expected) in [(0.0f32, 0.0f32), (0.5, 0.125), (1.0, 0.25), (2.0, 0.5)] {
1412 handle.config.set_volume(volume);
1413 output.fill(0.0);
1414 mixer.process(&mut output, &[], &transport);
1415 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1416 assert!(
1417 (peak - expected).abs() < 1.0e-6,
1418 "fader at {volume} gave {peak}, expected {expected}"
1419 );
1420 }
1421 }
1422}