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::pattern::{EventSink, PatternBlock, PatternEvent, PatternPlayer, PlaybackWindow};
17use crate::project::{TrackHandle, TrackKind};
18use crate::transport::Transport;
19
20#[allow(clippy::large_enum_variant)]
27pub enum MixerCommand {
28 AddTrack {
29 kind: TrackKind,
30 handle: Arc<TrackHandle>,
31 },
32 SetInstrument {
33 track_id: usize,
34 instrument: Box<dyn Plugin + Send>,
35 },
36 RemoveTrack {
37 track_id: usize,
38 },
39 SetParameter {
40 track_id: usize,
41 param_index: usize,
42 value: f32,
43 },
44 CreateClip {
46 track_id: usize,
47 start_tick: i64,
48 length_ticks: i64,
49 },
50 UpdateClip {
52 track_id: usize,
53 clip_index: usize,
54 events: Vec<ClipEvent>,
55 },
56 UpdateClipPosition {
58 track_id: usize,
59 clip_index: usize,
60 start_tick: i64,
61 length_ticks: i64,
62 },
63 RemoveClip {
65 track_id: usize,
66 clip_index: usize,
67 },
68 SetPattern {
79 track_id: usize,
80 slot: u8,
81 block: PatternBlock,
82 },
83}
84
85const HEAVY_COMMAND: u32 = 16;
104
105fn command_cost(cmd: &MixerCommand) -> u32 {
123 match cmd {
124 MixerCommand::SetParameter { .. } | MixerCommand::UpdateClipPosition { .. } => 1,
125 MixerCommand::AddTrack { .. }
126 | MixerCommand::SetInstrument { .. }
127 | MixerCommand::RemoveTrack { .. }
128 | MixerCommand::CreateClip { .. }
129 | MixerCommand::UpdateClip { .. }
130 | MixerCommand::RemoveClip { .. }
131 | MixerCommand::SetPattern { .. } => HEAVY_COMMAND,
138 }
139}
140
141const COMMAND_BUDGET: u32 = 64;
159
160const TRACK_CAPACITY: usize = 64;
168
169const LIMITER_CEILING: f32 = 0.891;
177
178const LIMITER_RELEASE_SECONDS: f32 = 0.050;
185
186struct MasterLimiter {
210 gain: f32,
212 release_coeff: f32,
214}
215
216impl MasterLimiter {
217 fn new(sample_rate: u32) -> Self {
218 let sr = (sample_rate as f32).max(1.0);
219 Self {
220 gain: 1.0,
221 release_coeff: 1.0 - (-1.0 / (LIMITER_RELEASE_SECONDS * sr)).exp(),
222 }
223 }
224
225 fn reset(&mut self) {
226 self.gain = 1.0;
227 }
228
229 fn process(&mut self, output: &mut [f32]) {
234 let mut frames = output.chunks_exact_mut(2);
235 for frame in frames.by_ref() {
236 let l = if frame[0].is_finite() { frame[0] } else { 0.0 };
241 let r = if frame[1].is_finite() { frame[1] } else { 0.0 };
242
243 let peak = l.abs().max(r.abs());
244 let target = if peak > LIMITER_CEILING {
250 (LIMITER_CEILING / peak) * (1.0 - 2.0 * f32::EPSILON)
251 } else {
252 1.0
253 };
254
255 if target < self.gain {
256 self.gain = target;
257 } else {
258 self.gain += (target - self.gain) * self.release_coeff;
259 }
260
261 frame[0] = (l * self.gain).clamp(-1.0, 1.0);
267 frame[1] = (r * self.gain).clamp(-1.0, 1.0);
268 }
269
270 for tail in frames.into_remainder() {
275 let s = if tail.is_finite() { *tail } else { 0.0 };
276 *tail = (s * self.gain).clamp(-LIMITER_CEILING, LIMITER_CEILING);
277 }
278 }
279}
280
281const PLUGIN_EVENT_CAPACITY: usize = 512;
293
294pub struct AudioTrack {
295 pub id: usize,
296 pub kind: TrackKind,
297 pub handle: Arc<TrackHandle>,
298 pub instrument: Option<Box<dyn Plugin>>,
299 pub clips: Vec<MidiClip>,
301 pattern: Option<Box<PatternPlayer>>,
307 record_buf: RecordBuffer,
309 was_recording: bool,
311 last_record_tick: i64,
313 buf_l: Vec<f32>,
314 buf_r: Vec<f32>,
315 plugin_events: Vec<MidiEvent>,
316}
317
318impl AudioTrack {
319 pub fn new(handle: Arc<TrackHandle>, max_buffer_size: usize) -> Self {
320 Self {
321 id: handle.id,
322 kind: handle.kind,
323 handle,
324 instrument: None,
325 clips: Vec::new(),
326 pattern: None,
327 record_buf: RecordBuffer::new(),
328 was_recording: false,
329 last_record_tick: -1,
330 buf_l: vec![0.0; max_buffer_size],
331 buf_r: vec![0.0; max_buffer_size],
332 plugin_events: Vec::with_capacity(PLUGIN_EVENT_CAPACITY),
333 }
334 }
335}
336
337struct TrackEventSink<'a> {
347 events: &'a mut Vec<MidiEvent>,
348 window: &'a PlaybackWindow,
349}
350
351impl EventSink for TrackEventSink<'_> {
352 fn accept(&mut self, event: PatternEvent) -> bool {
353 if self.events.len() >= self.events.capacity() {
354 return false;
355 }
356 self.events.push(MidiEvent {
357 sample_offset: self.window.sample_offset(event.tick),
358 status: event.status,
359 data1: event.data1,
360 data2: event.data2,
361 });
362 true
363 }
364}
365
366fn sort_events_by_offset(events: &mut [MidiEvent]) {
379 for i in 1..events.len() {
380 let mut j = i;
381 while j > 0 && events[j - 1].sample_offset > events[j].sample_offset {
382 events.swap(j - 1, j);
383 j -= 1;
384 }
385 }
386}
387
388pub struct Mixer {
391 tracks: Vec<AudioTrack>,
392 master_vu: Arc<VuLevels>,
393 command_rx: Receiver<MixerCommand>,
394 clip_tx: Sender<ClipSnapshot>,
395 metronome: Metronome,
396 sample_rate: u32,
397 max_buffer_size: usize,
398 scratch_l: Vec<f32>,
400 scratch_r: Vec<f32>,
401 live_events: Vec<MidiEvent>,
403 last_window: Option<PlaybackWindow>,
411 limiter: MasterLimiter,
413}
414
415impl Mixer {
416 pub fn new(
417 command_rx: Receiver<MixerCommand>,
418 master_vu: Arc<VuLevels>,
419 clip_tx: Sender<ClipSnapshot>,
420 sample_rate: u32,
421 max_buffer_size: usize,
422 ) -> Self {
423 Self {
424 tracks: Vec::with_capacity(TRACK_CAPACITY),
425 master_vu,
426 command_rx,
427 clip_tx,
428 metronome: Metronome::new(sample_rate as f64),
429 sample_rate,
430 max_buffer_size,
431 scratch_l: vec![0.0; max_buffer_size],
432 scratch_r: vec![0.0; max_buffer_size],
433 live_events: Vec::with_capacity(256),
434 last_window: None,
435 limiter: MasterLimiter::new(sample_rate),
436 }
437 }
438
439 pub fn process(&mut self, output: &mut [f32], midi_messages: &[MidiMessage], transport: &Transport) {
441 let _ = self.drain_commands();
444
445 let num_frames = output.len() / 2;
446 let playing = transport.is_playing();
447 let recording = transport.is_recording();
448 let looping = transport.is_looping();
449 let current_tick = transport.position_ticks();
450 let bpm = transport.tempo_bpm();
451 let ticks_per_sample = (bpm * Transport::PPQ as f64) / (60.0 * self.sample_rate as f64);
452 let loop_end = transport.loop_end();
453
454 let window = PlaybackWindow::for_block(
462 current_tick,
463 num_frames as u32,
464 ticks_per_sample,
465 looping.then(|| (transport.loop_start(), loop_end)),
466 self.last_window,
467 );
468 self.last_window = playing.then_some(window);
469
470 self.live_events.clear();
472 for msg in midi_messages {
473 if let Some(ev) = midi_to_plugin_event(msg) {
474 self.live_events.push(ev);
475 }
476 }
477
478 let any_solo = self.tracks.iter().any(|t| t.handle.config.is_soloed());
479
480 let mut master_l = std::mem::take(&mut self.scratch_l);
483 let mut master_r = std::mem::take(&mut self.scratch_r);
484 let live_events = std::mem::take(&mut self.live_events);
485 if master_l.len() < num_frames {
491 master_l.resize(num_frames, 0.0);
492 master_r.resize(num_frames, 0.0);
493 }
494 master_l[..num_frames].fill(0.0);
495 master_r[..num_frames].fill(0.0);
496
497 let clip_tx = &self.clip_tx;
498
499 for track in &mut self.tracks {
500 if track.buf_l.len() < num_frames {
501 track.buf_l.resize(num_frames, 0.0);
502 track.buf_r.resize(num_frames, 0.0);
503 }
504 track.buf_l[..num_frames].fill(0.0);
505 track.buf_r[..num_frames].fill(0.0);
506 track.plugin_events.clear();
507
508 let is_midi_active = track.kind == TrackKind::Instrument
509 && track.handle.config.is_midi_active();
510 let is_armed = track.handle.config.is_armed();
511 let should_record = playing && recording && is_armed && is_midi_active;
512
513 if should_record && !track.was_recording {
515 let rec_start = if looping { transport.loop_start() } else { current_tick };
518 track.record_buf.start(rec_start);
519 tracing::debug!("rec start track={} tick={}", track.id, current_tick);
520 }
521
522 if should_record && track.was_recording && looping
524 && track.record_buf.is_active() && track.last_record_tick >= 0
525 && current_tick < track.last_record_tick
526 {
527 commit_recording(track, loop_end, clip_tx);
528 track.record_buf.start(transport.loop_start());
531 }
532 if should_record {
533 track.last_record_tick = current_tick;
534 }
535
536 if !should_record && track.was_recording {
538 commit_recording(track, current_tick, clip_tx);
539 }
540 track.was_recording = should_record;
541
542 if is_midi_active {
544 for ev in &live_events {
545 track.plugin_events.push(*ev);
546 if should_record {
547 let event_tick = current_tick
548 + (ev.sample_offset as f64 * ticks_per_sample) as i64;
549 track.record_buf.record(event_tick, ev.status, ev.data1, ev.data2);
550 }
551 }
552 }
553
554 if let Some(ref mut player) = track.pattern {
560 let mut sink = TrackEventSink { events: &mut track.plugin_events, window: &window };
561 player.render(&window, playing, &mut sink);
562 track.handle.pattern.publish(
563 player.live_slot(),
564 player.queued_slot(),
565 player.current_step(),
566 playing && player.is_playing(),
567 );
568 }
569
570 if playing && !track.clips.is_empty() {
576 for clip in &track.clips {
577 for (tick, event) in clip.events_between(window.from(), window.to()) {
578 if track.plugin_events.len() >= track.plugin_events.capacity() {
579 break;
580 }
581 track.plugin_events.push(MidiEvent {
582 sample_offset: window.sample_offset(tick),
583 status: event.status,
584 data1: event.data1,
585 data2: event.data2,
586 });
587 }
588 }
589 }
590
591 if !track.plugin_events.is_empty() {
592 sort_events_by_offset(&mut track.plugin_events);
593 }
594
595 if playing {
597 track.last_record_tick = current_tick;
598 }
599
600 if let Some(ref mut instrument) = track.instrument {
602 let out_l = &mut track.buf_l[..num_frames];
603 let out_r = &mut track.buf_r[..num_frames];
604 let mut out_slices: [&mut [f32]; 2] = [out_l, out_r];
605 instrument.process(&[], &mut out_slices, &track.plugin_events);
606 }
607
608 let muted = track.handle.config.is_muted();
610 let soloed = track.handle.config.is_soloed();
611 let audible = !muted && (!any_solo || soloed);
612 let volume = track.handle.config.get_volume();
613
614 let mut peak_l = 0.0f32;
615 let mut peak_r = 0.0f32;
616 for i in 0..num_frames {
617 peak_l = peak_l.max(track.buf_l[i].abs());
618 peak_r = peak_r.max(track.buf_r[i].abs());
619 }
620
621 let (old_l, old_r) = track.handle.vu.get();
622 let decay = 0.85f32;
623 track.handle.vu.set(
624 if peak_l > old_l { peak_l } else { old_l * decay },
625 if peak_r > old_r { peak_r } else { old_r * decay },
626 );
627
628 if audible {
629 for i in 0..num_frames {
630 master_l[i] += track.buf_l[i] * volume;
631 master_r[i] += track.buf_r[i] * volume;
632 }
633 }
634 }
635
636 for i in 0..num_frames {
638 output[i * 2] = master_l[i];
639 output[i * 2 + 1] = master_r[i];
640 }
641
642 self.scratch_l = master_l;
644 self.scratch_r = master_r;
645 self.live_events = live_events;
646
647 self.metronome.process(output, transport);
649
650 self.limiter.process(output);
655
656 let mut mp_l = 0.0f32;
659 let mut mp_r = 0.0f32;
660 for i in 0..num_frames {
661 mp_l = mp_l.max(output[i * 2].abs());
662 mp_r = mp_r.max(output[i * 2 + 1].abs());
663 }
664
665 let (old_l, old_r) = self.master_vu.get();
666 let decay = 0.85f32;
667 self.master_vu.set(
668 if mp_l > old_l { mp_l } else { old_l * decay },
669 if mp_r > old_r { mp_r } else { old_r * decay },
670 );
671 }
672
673 pub fn reset_all(&mut self) {
674 let clip_tx = &self.clip_tx;
675 for track in &mut self.tracks {
676 if let Some(ref mut inst) = track.instrument {
677 inst.reset();
678 }
679 track.handle.vu.set(0.0, 0.0);
680 if track.record_buf.is_active() && track.was_recording {
682 let end_tick = track.last_record_tick.max(0);
683 commit_recording(track, end_tick, clip_tx);
684 } else if track.record_buf.is_active() {
685 track.record_buf.discard();
686 }
687 track.was_recording = false;
688 if let Some(ref mut player) = track.pattern {
693 player.silence();
694 }
695 }
696 self.last_window = None;
697 self.metronome.reset();
698 self.limiter.reset();
699 }
700
701 fn drain_commands(&mut self) -> u32 {
712 let mut spent = 0;
713 while spent < COMMAND_BUDGET {
714 let Ok(cmd) = self.command_rx.try_recv() else { break };
715 spent += command_cost(&cmd);
716 self.apply_command(cmd);
717 }
718 spent
719 }
720
721 fn apply_command(&mut self, cmd: MixerCommand) {
722 match cmd {
723 MixerCommand::AddTrack { kind: _, handle } => {
724 let track = AudioTrack::new(handle, self.max_buffer_size);
725 self.tracks.push(track);
726 }
727 MixerCommand::SetInstrument { track_id, mut instrument } => {
728 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
729 instrument.init(self.sample_rate as f64, self.max_buffer_size);
730 track.instrument = Some(instrument);
731 }
732 }
733 MixerCommand::RemoveTrack { track_id } => {
734 self.tracks.retain(|t| t.id != track_id);
735 }
736 MixerCommand::SetParameter { track_id, param_index, value } => {
737 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
738 if let Some(ref mut inst) = track.instrument {
739 inst.set_parameter(param_index, value);
740 }
741 }
742 }
743 MixerCommand::CreateClip { track_id, start_tick, length_ticks } => {
744 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
745 track.clips.push(MidiClip::new(start_tick, length_ticks, Vec::new()));
746 }
747 }
748 MixerCommand::UpdateClip { track_id, clip_index, events } => {
749 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
750 if let Some(clip) = track.clips.get_mut(clip_index) {
751 clip.events = events;
752 clip.events.sort_by_key(|e| e.tick);
753 }
754 }
755 }
756 MixerCommand::UpdateClipPosition { track_id, clip_index, start_tick, length_ticks } => {
757 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
758 if let Some(clip) = track.clips.get_mut(clip_index) {
759 clip.start_tick = start_tick;
760 clip.length_ticks = length_ticks;
761 }
762 }
763 }
764 MixerCommand::RemoveClip { track_id, clip_index } => {
765 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
766 if clip_index < track.clips.len() {
767 track.clips.remove(clip_index);
768 }
769 }
770 }
771 MixerCommand::SetPattern { track_id, slot, block } => {
772 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
773 let player = track.pattern.get_or_insert_with(|| Box::new(PatternPlayer::new()));
774 player.apply(slot, block);
775 }
776 }
777 }
778 }
779}
780
781fn commit_recording(track: &mut AudioTrack, end_tick: i64, clip_tx: &Sender<ClipSnapshot>) {
783 if let Some(clip) = track.record_buf.commit(end_tick) {
784 let idx = track.clips.len();
785 tracing::debug!(
786 "rec commit track={}: {} events, ticks {}..{}",
787 track.id, clip.events.len(), clip.start_tick, clip.end_tick()
788 );
789 let snapshot = ClipSnapshot::from_clip(track.id, idx, &clip);
790 track.clips.push(clip);
791 let _ = clip_tx.send(snapshot);
792 }
793}
794
795pub fn midi_to_plugin_event(msg: &MidiMessage) -> Option<MidiEvent> {
809 use phosphor_midi::message::MidiMessageType;
810 match msg.message_type {
811 MidiMessageType::NoteOn { .. }
812 | MidiMessageType::NoteOff { .. }
813 | MidiMessageType::ControlChange { .. }
814 | MidiMessageType::PitchBend { .. }
815 | MidiMessageType::ChannelPressure { .. } => Some(MidiEvent {
816 sample_offset: 0,
817 status: msg.raw[0],
818 data1: msg.raw[1],
819 data2: msg.raw[2],
820 }),
821 _ => None,
822 }
823}
824
825pub fn mixer_command_channel() -> (Sender<MixerCommand>, Receiver<MixerCommand>) {
826 crossbeam_channel::unbounded()
827}
828
829pub fn clip_snapshot_channel() -> (Sender<ClipSnapshot>, Receiver<ClipSnapshot>) {
831 crossbeam_channel::unbounded()
832}
833
834#[cfg(test)]
835mod tests {
836 use super::*;
837 use crate::cpal_backend::{Requested, StreamFormat};
838 use crate::project::TrackConfig;
839 use phosphor_dsp::synth::PhosphorSynth;
840 use phosphor_midi::message::{MidiMessage, MidiMessageType};
841
842 fn make_note_on(note: u8, vel: u8) -> MidiMessage {
843 MidiMessage {
844 timestamp: Some(0),
845 message_type: MidiMessageType::NoteOn { channel: 0, note, velocity: vel },
846 raw: [0x90, note, vel],
847 len: 3,
848 }
849 }
850
851 #[test]
854 fn channel_pressure_reaches_the_plugin_and_key_pressure_does_not() {
855 let pressure = MidiMessage {
856 timestamp: Some(0),
857 message_type: MidiMessageType::ChannelPressure { channel: 0, pressure: 96 },
858 raw: [0xD0, 96, 0],
859 len: 2,
860 };
861 let event = midi_to_plugin_event(&pressure).expect("channel pressure is dropped");
862 assert_eq!(event.status, 0xD0);
863 assert_eq!(event.data1, 96);
864
865 let key = MidiMessage::from_bytes(&[0xA0, 60, 96], 0).expect("parsed");
868 assert!(
869 midi_to_plugin_event(&key).is_none(),
870 "polyphonic key pressure has no destination in the rack"
871 );
872 }
873
874 fn make_note_off(note: u8) -> MidiMessage {
875 MidiMessage {
876 timestamp: Some(0),
877 message_type: MidiMessageType::NoteOff { channel: 0, note, velocity: 0 },
878 raw: [0x80, note, 0],
879 len: 3,
880 }
881 }
882
883 fn setup_mixer() -> (Mixer, Sender<MixerCommand>, Receiver<ClipSnapshot>, Arc<Transport>) {
884 let (tx, rx) = mixer_command_channel();
885 let (clip_tx, clip_rx) = clip_snapshot_channel();
886 let master_vu = Arc::new(VuLevels::new());
887 let transport = Arc::new(Transport::new(120.0));
888 let mixer = Mixer::new(rx, master_vu, clip_tx, 44100, 256);
889 (mixer, tx, clip_rx, transport)
890 }
891
892 fn add_armed_synth(tx: &Sender<MixerCommand>, id: usize) -> Arc<TrackHandle> {
893 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
894 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
895 handle.config.armed.store(true, std::sync::atomic::Ordering::Relaxed);
896 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
897 tx.send(MixerCommand::SetInstrument { track_id: id, instrument: Box::new(PhosphorSynth::new()) }).unwrap();
898 handle
899 }
900
901 #[test]
902 fn mixer_empty_output() {
903 let (mut mixer, _tx, _clip_rx, transport) = setup_mixer();
904 let mut output = vec![0.0f32; 128];
905 mixer.process(&mut output, &[], &transport);
906 assert!(output.iter().all(|&s| s == 0.0));
907 }
908
909 #[test]
910 fn mixer_live_midi_produces_sound() {
911 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
912 let _handle = add_armed_synth(&tx, 0);
913 transport.play();
914
915 let midi = vec![make_note_on(60, 100)];
916 let mut output = vec![0.0f32; 512];
917 mixer.process(&mut output, &midi, &transport);
918
919 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
920 assert!(peak > 0.001, "Should produce sound, peak={peak}");
923 }
924
925 #[test]
926 fn mixer_records_midi_clip() {
927 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
928 let _handle = add_armed_synth(&tx, 0);
929 transport.play();
930 transport.toggle_record();
931
932 let midi = vec![make_note_on(60, 100)];
934 let mut output = vec![0.0f32; 512];
935 mixer.process(&mut output, &midi, &transport);
936
937 let midi = vec![make_note_off(60)];
939 mixer.process(&mut output, &midi, &transport);
940
941 transport.toggle_record();
943 mixer.process(&mut output, &[], &transport);
944
945 let snap = clip_rx.try_recv().expect("Should receive clip snapshot");
947 assert_eq!(snap.track_id, 0);
948 assert!(snap.event_count >= 2, "Should have note on + off, got {}", snap.event_count);
949 assert!(!snap.notes.is_empty(), "Should have parsed notes");
950 }
951
952 #[test]
953 fn mixer_plays_back_recorded_clip() {
954 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
955 let _handle = add_armed_synth(&tx, 0);
956 transport.play();
957 transport.toggle_record();
958
959 let midi = vec![make_note_on(60, 100)];
961 let mut output = vec![0.0f32; 512];
962 mixer.process(&mut output, &midi, &transport);
963
964 let midi = vec![make_note_off(60)];
965 mixer.process(&mut output, &midi, &transport);
966
967 transport.toggle_record();
969 mixer.process(&mut output, &[], &transport);
970
971 transport.stop();
973
974 transport.play();
976 output.fill(0.0);
977 mixer.process(&mut output, &[], &transport);
978
979 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
980 assert!(peak > 0.001, "Playback should produce sound, peak={peak}");
981 }
982
983 #[test]
984 fn mixer_mute_silences() {
985 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
986 let handle = add_armed_synth(&tx, 0);
987 handle.config.muted.store(true, std::sync::atomic::Ordering::Relaxed);
988 transport.play();
989
990 let midi = vec![make_note_on(60, 100)];
991 let mut output = vec![0.0f32; 512];
992 mixer.process(&mut output, &midi, &transport);
993
994 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
995 assert!(peak == 0.0, "Muted track should be silent, peak={peak}");
996 }
997
998 #[test]
999 fn mixer_no_record_when_not_armed() {
1000 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
1001 let handle = add_armed_synth(&tx, 0);
1002 handle.config.armed.store(false, std::sync::atomic::Ordering::Relaxed);
1003 transport.play();
1004 transport.toggle_record();
1005
1006 let midi = vec![make_note_on(60, 100)];
1007 let mut output = vec![0.0f32; 512];
1008 mixer.process(&mut output, &midi, &transport);
1009
1010 transport.toggle_record();
1011 mixer.process(&mut output, &[], &transport);
1012
1013 assert!(clip_rx.try_recv().is_err(), "Should not record when not armed");
1014 }
1015
1016 #[test]
1017 fn mixer_reset_commits_recording() {
1018 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
1019 let _handle = add_armed_synth(&tx, 0);
1020 transport.play();
1021 transport.toggle_record();
1022
1023 let midi = vec![make_note_on(60, 100)];
1024 let mut output = vec![0.0f32; 512];
1025 mixer.process(&mut output, &midi, &transport);
1026
1027 mixer.reset_all();
1028
1029 assert!(clip_rx.try_recv().is_ok(), "Reset should commit active recording");
1031 }
1032
1033 #[test]
1034 fn end_to_end_record_and_playback() {
1035 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
1038 let _handle = add_armed_synth(&tx, 0);
1039 let sr = 44100u32;
1040 let buf_frames = 256;
1041 let buf_samples = buf_frames * 2; transport.toggle_record();
1045 transport.play();
1046
1047 let mut output = vec![0.0f32; buf_samples];
1049 for _ in 0..4 {
1050 mixer.process(&mut output, &[], &transport);
1051 transport.advance(buf_frames as u32, sr);
1052 }
1053
1054 let midi = vec![make_note_on(60, 100)];
1056 mixer.process(&mut output, &midi, &transport);
1057 let peak_during = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1058 assert!(peak_during > 0.001, "Should hear note during recording (monitoring)");
1059 transport.advance(buf_frames as u32, sr);
1060
1061 for _ in 0..8 {
1063 output.fill(0.0);
1064 mixer.process(&mut output, &[], &transport);
1065 transport.advance(buf_frames as u32, sr);
1066 }
1067
1068 let midi = vec![make_note_off(60)];
1070 mixer.process(&mut output, &midi, &transport);
1071 transport.advance(buf_frames as u32, sr);
1072
1073 for _ in 0..4 {
1075 output.fill(0.0);
1076 mixer.process(&mut output, &[], &transport);
1077 transport.advance(buf_frames as u32, sr);
1078 }
1079
1080 transport.toggle_record();
1082 mixer.process(&mut output, &[], &transport);
1083 transport.advance(buf_frames as u32, sr);
1084
1085 let snap = clip_rx.try_recv().expect("Should receive clip snapshot after stopping record");
1087 assert!(snap.event_count >= 2, "Clip should have note on + off");
1088 assert!(!snap.notes.is_empty(), "Clip should have parsed notes");
1089
1090 transport.stop();
1092
1093 transport.play();
1095
1096 for _ in 0..4 {
1099 output.fill(0.0);
1100 mixer.process(&mut output, &[], &transport);
1101 transport.advance(buf_frames as u32, sr);
1102 }
1103
1104 output.fill(0.0);
1106 mixer.process(&mut output, &[], &transport);
1107 let peak_playback = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1108 assert!(peak_playback > 0.001, "Playback should produce sound at the recorded position, peak={peak_playback}");
1109 }
1110
1111 #[test]
1112 fn loop_record_commits_on_wrap() {
1113 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
1114 let _handle = add_armed_synth(&tx, 0);
1115 let sr = 44100u32;
1116 let buf_frames = 256u32;
1117
1118 transport.set_loop_bars(1, 1);
1120 transport.start_loop_record();
1121
1122 let mut output = vec![0.0f32; buf_frames as usize * 2];
1123
1124 let midi = vec![make_note_on(60, 100)];
1126 mixer.process(&mut output, &midi, &transport);
1127 transport.advance(buf_frames, sr);
1128
1129 for _ in 0..5 {
1131 mixer.process(&mut output, &[], &transport);
1132 transport.advance(buf_frames, sr);
1133 }
1134 let midi = vec![make_note_off(60)];
1135 mixer.process(&mut output, &midi, &transport);
1136 transport.advance(buf_frames, sr);
1137
1138 for _ in 0..400 {
1141 mixer.process(&mut output, &[], &transport);
1142 transport.advance(buf_frames, sr);
1143
1144 if let Ok(snap) = clip_rx.try_recv() {
1145 assert!(snap.event_count >= 2, "Clip should have events, got {}", snap.event_count);
1146 assert!(!snap.notes.is_empty(), "Clip should have notes");
1147 transport.stop_loop_record();
1149 return;
1150 }
1151 }
1152
1153 panic!("Recording should have committed when the loop wrapped");
1154 }
1155
1156 #[test]
1157 fn loop_playback_after_record() {
1158 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
1159 let _handle = add_armed_synth(&tx, 0);
1160 let sr = 44100u32;
1161 let bf = 256u32;
1162
1163 transport.set_loop_bars(1, 1);
1165 transport.start_loop_record();
1166
1167 let mut output = vec![0.0f32; bf as usize * 2];
1168
1169 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
1171 transport.advance(bf, sr);
1172 for _ in 0..3 {
1173 mixer.process(&mut output, &[], &transport);
1174 transport.advance(bf, sr);
1175 }
1176 mixer.process(&mut output, &[make_note_off(60)], &transport);
1177 transport.advance(bf, sr);
1178
1179 for _ in 0..200 {
1181 mixer.process(&mut output, &[], &transport);
1182 transport.advance(bf, sr);
1183 if clip_rx.try_recv().is_ok() { break; }
1184 }
1185
1186 transport.stop_loop_record();
1188 transport.set_position(0);
1189
1190 transport.toggle_loop(); transport.play();
1193
1194 output.fill(0.0);
1195 mixer.process(&mut output, &[], &transport);
1196 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1197 assert!(peak > 0.001, "Should hear playback, peak={peak}");
1198 }
1199
1200 const WORST_CALLBACK: u32 = COMMAND_BUDGET - 1 + HEAVY_COMMAND;
1206
1207 #[derive(Clone)]
1214 struct ParamLog(Arc<std::sync::Mutex<Vec<(usize, f32)>>>);
1215
1216 impl ParamLog {
1217 fn new() -> Self {
1218 Self(Arc::new(std::sync::Mutex::new(Vec::new())))
1219 }
1220 fn seen(&self) -> Vec<(usize, f32)> {
1221 self.0.lock().unwrap().clone()
1222 }
1223 }
1224
1225 impl Plugin for ParamLog {
1226 fn info(&self) -> phosphor_plugin::PluginInfo {
1227 phosphor_plugin::PluginInfo {
1228 name: "ParamLog".into(),
1229 version: "0".into(),
1230 author: "test".into(),
1231 category: phosphor_plugin::PluginCategory::Instrument,
1232 }
1233 }
1234 fn init(&mut self, _sample_rate: f64, _max_buffer_size: usize) {}
1235 fn process(&mut self, _inputs: &[&[f32]], _outputs: &mut [&mut [f32]], _midi: &[MidiEvent]) {}
1236 fn parameter_count(&self) -> usize { 8 }
1237 fn parameter_info(&self, _index: usize) -> Option<phosphor_plugin::ParameterInfo> { None }
1238 fn get_parameter(&self, _index: usize) -> f32 { 0.0 }
1239 fn set_parameter(&mut self, index: usize, value: f32) {
1240 self.0.lock().unwrap().push((index, value));
1241 }
1242 fn reset(&mut self) {}
1243 }
1244
1245 fn add_logging_track(mixer: &mut Mixer, tx: &Sender<MixerCommand>, id: usize) -> ParamLog {
1247 let log = ParamLog::new();
1248 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1249 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1250 tx.send(MixerCommand::SetInstrument {
1251 track_id: id,
1252 instrument: Box::new(log.clone()),
1253 }).unwrap();
1254 mixer.drain_commands();
1255 log
1256 }
1257
1258 #[test]
1262 fn one_callback_applies_a_bounded_amount_of_work() {
1263 let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1264 let log = add_logging_track(&mut mixer, &tx, 0);
1265
1266 for i in 0..500 {
1267 tx.send(MixerCommand::SetParameter {
1268 track_id: 0,
1269 param_index: i % 8,
1270 value: i as f32,
1271 }).unwrap();
1272 }
1273
1274 let spent = mixer.drain_commands();
1275 assert!(
1276 spent <= WORST_CALLBACK,
1277 "one callback spent {spent} units, over the {WORST_CALLBACK} bound"
1278 );
1279 assert_eq!(
1280 log.seen().len(),
1281 COMMAND_BUDGET as usize,
1282 "a parameter costs one unit, so a full budget is exactly that many"
1283 );
1284 assert!(!mixer.command_rx.is_empty(), "the rest has to still be queued");
1285 }
1286
1287 #[test]
1290 fn nothing_is_lost_or_reordered_across_callbacks() {
1291 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1292 let log = add_logging_track(&mut mixer, &tx, 0);
1293
1294 let sent: Vec<(usize, f32)> = (0..500).map(|i| (i % 8, i as f32)).collect();
1295 for &(param_index, value) in &sent {
1296 tx.send(MixerCommand::SetParameter { track_id: 0, param_index, value }).unwrap();
1297 }
1298
1299 let mut output = vec![0.0f32; 128];
1303 let mut callbacks = 0;
1304 while !mixer.command_rx.is_empty() {
1305 mixer.process(&mut output, &[], &transport);
1306 callbacks += 1;
1307 assert!(callbacks < 100, "the drain is not making progress");
1308 }
1309 assert!(
1310 callbacks >= 500 / COMMAND_BUDGET as usize,
1311 "500 commands went through in {callbacks} callbacks, so the budget did not hold"
1312 );
1313 assert_eq!(log.seen(), sent, "the audio thread saw a different sequence");
1314 }
1315
1316 #[test]
1322 fn a_track_and_its_instrument_survive_a_budget_boundary() {
1323 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1324 let log = ParamLog::new();
1325
1326 for _ in 0..COMMAND_BUDGET {
1329 tx.send(MixerCommand::SetParameter { track_id: 99, param_index: 0, value: 0.0 })
1330 .unwrap();
1331 }
1332 let handle = Arc::new(TrackHandle::new(7, TrackKind::Instrument));
1333 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1334 tx.send(MixerCommand::SetInstrument {
1335 track_id: 7,
1336 instrument: Box::new(log.clone()),
1337 }).unwrap();
1338 tx.send(MixerCommand::SetParameter { track_id: 7, param_index: 3, value: 0.5 }).unwrap();
1339
1340 let mut output = vec![0.0f32; 128];
1341 mixer.process(&mut output, &[], &transport);
1342 assert!(mixer.tracks.is_empty(), "the budget did not stop at the parameters");
1343
1344 while !mixer.command_rx.is_empty() {
1345 mixer.process(&mut output, &[], &transport);
1346 }
1347 assert_eq!(mixer.tracks.len(), 1);
1348 assert!(mixer.tracks[0].instrument.is_some(), "the instrument never arrived");
1349 assert_eq!(
1350 log.seen(),
1351 vec![(3, 0.5)],
1352 "the parameter that follows the instrument did not reach it"
1353 );
1354 }
1355
1356 #[test]
1361 fn an_instrument_load_costs_more_than_a_parameter() {
1362 let param = MixerCommand::SetParameter { track_id: 0, param_index: 0, value: 0.0 };
1363 let load = MixerCommand::SetInstrument {
1364 track_id: 0,
1365 instrument: Box::new(FixedOutput(0.0)),
1366 };
1367 assert!(command_cost(&load) > command_cost(¶m));
1368
1369 let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1371 for id in 0..8 {
1372 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1373 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1374 }
1375 while !mixer.command_rx.is_empty() {
1376 mixer.drain_commands();
1377 }
1378 for id in 0..8 {
1379 tx.send(MixerCommand::SetInstrument {
1380 track_id: id,
1381 instrument: Box::new(FixedOutput(0.25)),
1382 }).unwrap();
1383 }
1384 mixer.drain_commands();
1385 let loaded = mixer.tracks.iter().filter(|t| t.instrument.is_some()).count();
1386 assert_eq!(loaded, (COMMAND_BUDGET / HEAVY_COMMAND) as usize);
1387 }
1388
1389 #[test]
1393 fn adding_tracks_does_not_grow_the_track_list() {
1394 let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
1395 let capacity = mixer.tracks.capacity();
1396 assert!(capacity >= TRACK_CAPACITY);
1397
1398 for id in 0..TRACK_CAPACITY {
1399 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1400 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1401 }
1402 while !mixer.command_rx.is_empty() {
1403 mixer.drain_commands();
1404 }
1405 assert_eq!(mixer.tracks.len(), TRACK_CAPACITY);
1406 assert_eq!(
1407 mixer.tracks.capacity(), capacity,
1408 "the track list reallocated on the audio thread"
1409 );
1410 }
1411
1412 struct FixedOutput(f32);
1417
1418 impl Plugin for FixedOutput {
1419 fn info(&self) -> phosphor_plugin::PluginInfo {
1420 phosphor_plugin::PluginInfo {
1421 name: "Fixed".into(),
1422 version: "0".into(),
1423 author: "test".into(),
1424 category: phosphor_plugin::PluginCategory::Instrument,
1425 }
1426 }
1427 fn init(&mut self, _sample_rate: f64, _max_buffer_size: usize) {}
1428 fn process(&mut self, _inputs: &[&[f32]], outputs: &mut [&mut [f32]], _midi: &[MidiEvent]) {
1429 for ch in outputs.iter_mut() {
1430 ch.fill(self.0);
1431 }
1432 }
1433 fn parameter_count(&self) -> usize { 0 }
1434 fn parameter_info(&self, _index: usize) -> Option<phosphor_plugin::ParameterInfo> { None }
1435 fn get_parameter(&self, _index: usize) -> f32 { 0.0 }
1436 fn set_parameter(&mut self, _index: usize, _value: f32) {}
1437 fn reset(&mut self) {}
1438 }
1439
1440 fn add_fixed_track(tx: &Sender<MixerCommand>, id: usize, value: f32) -> Arc<TrackHandle> {
1441 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1442 handle.config.set_volume(1.0);
1443 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
1444 tx.send(MixerCommand::SetInstrument {
1445 track_id: id,
1446 instrument: Box::new(FixedOutput(value)),
1447 }).unwrap();
1448 handle
1449 }
1450
1451 #[test]
1454 fn master_limiter_bounds_many_loud_tracks() {
1455 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1456 for id in 0..6 {
1457 add_fixed_track(&tx, id, 0.75);
1458 }
1459 transport.play();
1460
1461 let mut output = vec![0.0f32; 512];
1462 for _ in 0..8 {
1463 mixer.process(&mut output, &[], &transport);
1464 for (i, &s) in output.iter().enumerate() {
1465 assert!(s.is_finite(), "non-finite sample at {i}");
1466 assert!(s.abs() <= 1.0, "sample {i} left the mixer at {s}");
1467 }
1468 }
1469
1470 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1472 assert!(peak > 0.8, "limiter over-attenuated, peak={peak}");
1473 }
1474
1475 #[test]
1479 fn non_finite_track_output_becomes_silence() {
1480 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1481 add_fixed_track(&tx, 0, f32::NAN);
1482 transport.play();
1483
1484 let mut output = vec![0.0f32; 512];
1485 mixer.process(&mut output, &[], &transport);
1486 assert!(output.iter().all(|s| *s == 0.0), "NaN track should render as silence");
1487
1488 tx.send(MixerCommand::RemoveTrack { track_id: 0 }).unwrap();
1491 add_fixed_track(&tx, 1, 0.5);
1492 mixer.process(&mut output, &[], &transport);
1493 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1494 assert!((peak - 0.5).abs() < 1.0e-6, "mixer did not recover, peak={peak}");
1495 }
1496
1497 #[test]
1498 fn infinite_track_output_becomes_silence() {
1499 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1500 add_fixed_track(&tx, 0, f32::INFINITY);
1501 transport.play();
1502
1503 let mut output = vec![0.0f32; 512];
1504 mixer.process(&mut output, &[], &transport);
1505 assert!(output.iter().all(|s| *s == 0.0), "infinite track should render as silence");
1506 }
1507
1508 #[test]
1512 fn limiter_is_bit_identical_below_the_ceiling() {
1513 let mut limiter = MasterLimiter::new(44_100);
1514
1515 let mut input: Vec<f32> = Vec::new();
1517 for i in 0..20_000u32 {
1518 let phase = i as f32 * 0.01;
1519 let amp = LIMITER_CEILING * (i as f32 / 20_000.0);
1520 input.push(phase.sin() * amp);
1521 input.push(phase.cos() * amp);
1522 }
1523 input.push(LIMITER_CEILING);
1524 input.push(-LIMITER_CEILING);
1525 input.push(0.0);
1526 input.push(-0.0);
1527 input.push(f32::MIN_POSITIVE);
1528 input.push(-f32::MIN_POSITIVE);
1529
1530 let mut output = input.clone();
1531 limiter.process(&mut output);
1532
1533 for (i, (a, b)) in input.iter().zip(output.iter()).enumerate() {
1534 assert_eq!(a.to_bits(), b.to_bits(), "limiter altered sample {i}: {a} -> {b}");
1535 }
1536 }
1537
1538 #[test]
1541 fn limiter_holds_the_ceiling_under_abuse() {
1542 let mut limiter = MasterLimiter::new(44_100);
1543 for amplitude in [1.0f32, 2.0, 10.0, 1.0e3, 1.0e6, 1.0e30] {
1544 let mut buf: Vec<f32> = (0..4_096)
1545 .map(|i| (i as f32 * 0.05).sin() * amplitude)
1546 .collect();
1547 limiter.process(&mut buf);
1548 for (i, &s) in buf.iter().enumerate() {
1549 assert!(s.is_finite(), "amplitude {amplitude}: sample {i} is {s}");
1550 assert!(
1551 s.abs() <= LIMITER_CEILING,
1552 "amplitude {amplitude}: sample {i} reached {s}, above the ceiling"
1553 );
1554 }
1555 }
1556 }
1557
1558 #[test]
1562 fn limiter_attack_has_no_overshoot() {
1563 let mut limiter = MasterLimiter::new(44_100);
1564 let mut buf = vec![0.0f32; 64];
1565 limiter.process(&mut buf);
1566 let mut step = vec![4.0f32; 64];
1567 limiter.process(&mut step);
1568 assert!(
1569 step[0].abs() <= LIMITER_CEILING,
1570 "first sample of the step overshot to {}",
1571 step[0]
1572 );
1573 }
1574
1575 #[test]
1578 fn limiter_release_is_gradual() {
1579 let mut limiter = MasterLimiter::new(44_100);
1580 let mut loud = vec![4.0f32; 64];
1581 limiter.process(&mut loud);
1582 let reduced = limiter.gain;
1583 assert!(reduced < 0.5, "limiter did not engage, gain={reduced}");
1584
1585 let mut quiet = vec![0.1f32; 441 * 2];
1588 limiter.process(&mut quiet);
1589 assert!(limiter.gain > reduced, "gain did not recover at all");
1590 assert!(
1591 limiter.gain < 1.0,
1592 "gain snapped back to unity within 10 ms, which is a click"
1593 );
1594
1595 let mut long = vec![0.1f32; 22_050 * 2];
1597 limiter.process(&mut long);
1598 assert!(
1599 (limiter.gain - 1.0).abs() < 1.0e-4,
1600 "gain never returned to unity: {}",
1601 limiter.gain
1602 );
1603 }
1604
1605 #[test]
1608 fn limiter_does_not_shift_the_stereo_image() {
1609 let mut limiter = MasterLimiter::new(44_100);
1610 let mut buf: Vec<f32> = Vec::new();
1612 for i in 0..1_024 {
1613 let phase = i as f32 * 0.05;
1614 buf.push(phase.sin() * 3.0);
1615 buf.push(phase.sin() * 1.5);
1616 }
1617 limiter.process(&mut buf);
1618 for frame in buf.chunks_exact(2) {
1619 if frame[1].abs() > 1.0e-4 {
1620 let ratio = frame[0] / frame[1];
1621 assert!(
1622 (ratio - 2.0).abs() < 1.0e-3,
1623 "channel balance moved: L/R = {ratio}"
1624 );
1625 }
1626 }
1627 }
1628
1629 fn loudest_dx7_voice() -> phosphor_dsp::dx7::Dx7Synth {
1636 use phosphor_dsp::dx7;
1637 let mut synth = dx7::Dx7Synth::new();
1638 let (bank, patch) = dx7::voice_knobs(147);
1639 synth.set_parameter(dx7::P_BANK, bank);
1640 synth.set_parameter(dx7::P_PATCH, patch);
1641 debug_assert_eq!(dx7::voice_name(147), "TIMPANI");
1642 synth
1643 }
1644
1645 #[test]
1649 fn master_limiter_bounds_four_loud_instrument_tracks() {
1650 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1651 for id in 0..4 {
1652 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
1653 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1654 handle.config.set_volume(1.0);
1655 let synth = loudest_dx7_voice();
1656 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1657 tx.send(MixerCommand::SetInstrument {
1658 track_id: id,
1659 instrument: Box::new(synth),
1660 }).unwrap();
1661 }
1662 transport.play();
1663
1664 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1665 .iter()
1666 .map(|¬e| make_note_on(note, 127))
1667 .collect();
1668
1669 let mut output = vec![0.0f32; 512];
1670 let mut peak = 0.0f32;
1671 for block in 0..200 {
1672 output.fill(0.0);
1673 if block == 0 {
1674 mixer.process(&mut output, &chord, &transport);
1675 } else {
1676 mixer.process(&mut output, &[], &transport);
1677 }
1678 for (i, &s) in output.iter().enumerate() {
1679 assert!(s.is_finite(), "block {block} sample {i} is {s}");
1680 assert!(s.abs() <= 1.0, "block {block} sample {i} left the mixer at {s}");
1681 peak = peak.max(s.abs());
1682 }
1683 }
1684 assert!(peak > 0.5, "four loud tracks should be loud, peak={peak}");
1685 }
1686
1687 #[test]
1693 fn limiter_idle_for_the_worst_single_track() {
1694 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1695 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1696 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1697 handle.config.set_volume(1.0);
1698 let synth = loudest_dx7_voice();
1699 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1700 tx.send(MixerCommand::SetInstrument { track_id: 0, instrument: Box::new(synth) }).unwrap();
1701 transport.play();
1702
1703 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1704 .iter()
1705 .map(|¬e| make_note_on(note, 127))
1706 .collect();
1707
1708 let mut output = vec![0.0f32; 512];
1709 let mut peak = 0.0f32;
1710 for block in 0..200 {
1711 output.fill(0.0);
1712 if block == 0 {
1713 mixer.process(&mut output, &chord, &transport);
1714 } else {
1715 mixer.process(&mut output, &[], &transport);
1716 }
1717 peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0f32, f32::max));
1718 assert_eq!(
1719 mixer.limiter.gain, 1.0,
1720 "limiter engaged at block {block}, peak {peak}"
1721 );
1722 }
1723 assert!(peak > 0.3, "expected a loud chord, peak={peak}");
1724 }
1725
1726 fn worst_track_through_the_mixer(volume: f32) -> (f32, f32) {
1732 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1733 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1734 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1735 handle.config.set_volume(volume);
1736 let synth = loudest_dx7_voice();
1737 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
1738 tx.send(MixerCommand::SetInstrument { track_id: 0, instrument: Box::new(synth) }).unwrap();
1739 transport.play();
1740
1741 let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
1742 .iter()
1743 .map(|¬e| make_note_on(note, 127))
1744 .collect();
1745
1746 let mut output = vec![0.0f32; 512];
1747 let mut peak = 0.0f32;
1748 let mut min_gain = 1.0f32;
1749 for block in 0..200 {
1750 output.fill(0.0);
1751 if block == 0 {
1752 mixer.process(&mut output, &chord, &transport);
1753 } else {
1754 mixer.process(&mut output, &[], &transport);
1755 }
1756 for &s in output.iter() {
1757 assert!(s.is_finite(), "block {block}: non-finite sample");
1758 assert!(s.abs() <= 1.0, "block {block}: sample left the mixer at {s}");
1759 peak = peak.max(s.abs());
1760 }
1761 min_gain = min_gain.min(mixer.limiter.gain);
1762 }
1763 (peak, min_gain)
1764 }
1765
1766 #[test]
1774 fn fader_below_unity_never_engages_the_limiter() {
1775 for volume in [
1776 0.25,
1777 TrackConfig::DEFAULT_VOLUME,
1778 TrackConfig::UNITY_VOLUME,
1779 ] {
1780 let (peak, min_gain) = worst_track_through_the_mixer(volume);
1781 assert_eq!(
1782 min_gain, 1.0,
1783 "limiter reduced by {:.2} dB at fader {volume} (peak {peak:.4})",
1784 20.0 * min_gain.log10()
1785 );
1786 }
1787 }
1788
1789 #[test]
1795 fn fader_makeup_gain_is_bounded_not_wasted() {
1796 let (unity_peak, _) = worst_track_through_the_mixer(TrackConfig::UNITY_VOLUME);
1797 let (max_peak, min_gain) = worst_track_through_the_mixer(TrackConfig::MAX_VOLUME);
1798
1799 assert!(
1800 max_peak <= LIMITER_CEILING,
1801 "fader at maximum let {max_peak:.4} through, above the ceiling"
1802 );
1803 assert!(
1804 max_peak >= unity_peak,
1805 "turning the fader up made the track quieter: {unity_peak:.4} -> {max_peak:.4}"
1806 );
1807 let reduction_db = -20.0 * min_gain.log10();
1810 let boost_db = 20.0 * (TrackConfig::MAX_VOLUME / TrackConfig::UNITY_VOLUME).log10();
1811 assert!(
1812 reduction_db <= boost_db,
1813 "limiter took {reduction_db:.2} dB off a {boost_db:.2} dB boost"
1814 );
1815 }
1816
1817 #[test]
1829 fn metronome_click_sits_with_the_music() {
1830 use phosphor_dsp::dx7;
1831
1832 fn render(with_track: bool, metronome: bool) -> f32 {
1833 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1834 let chord: Vec<MidiMessage> = if with_track {
1835 let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
1836 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
1837 tx.send(MixerCommand::AddTrack {
1838 kind: TrackKind::Instrument,
1839 handle,
1840 })
1841 .unwrap();
1842 tx.send(MixerCommand::SetInstrument {
1843 track_id: 0,
1844 instrument: Box::new(dx7::Dx7Synth::new()),
1845 })
1846 .unwrap();
1847 [60u8, 64, 67].iter().map(|&n| make_note_on(n, 100)).collect()
1848 } else {
1849 Vec::new()
1850 };
1851 if metronome {
1852 transport.toggle_metronome();
1853 }
1854 transport.play();
1855
1856 let mut output = vec![0.0f32; 512];
1857 let mut peak = 0.0f32;
1858 for block in 0..200 {
1859 output.fill(0.0);
1860 if block == 0 {
1861 mixer.process(&mut output, &chord, &transport);
1862 } else {
1863 mixer.process(&mut output, &[], &transport);
1864 }
1865 peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0f32, f32::max));
1866 transport.advance(256, 44_100);
1867 }
1868 peak
1869 }
1870
1871 let music = render(true, false);
1872 let click = render(false, true);
1873 assert!(music > 0.0 && click > 0.0, "music {music}, click {click}");
1874
1875 let relative_db = 20.0 * (click / music).log10();
1876 assert!(
1877 (-12.0..=0.0).contains(&relative_db),
1878 "the click is {relative_db:.1} dB against a triad (click {click:.4}, \
1879 music {music:.4}); it has to be audible over the music without \
1880 being the loudest thing in the mix"
1881 );
1882 }
1883
1884 #[test]
1888 fn fader_scales_the_track() {
1889 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
1890 let handle = add_fixed_track(&tx, 0, 0.25);
1891 transport.play();
1892
1893 let mut output = vec![0.0f32; 512];
1894 for (volume, expected) in [(0.0f32, 0.0f32), (0.5, 0.125), (1.0, 0.25), (2.0, 0.5)] {
1895 handle.config.set_volume(volume);
1896 output.fill(0.0);
1897 mixer.process(&mut output, &[], &transport);
1898 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1899 assert!(
1900 (peak - expected).abs() < 1.0e-6,
1901 "fader at {volume} gave {peak}, expected {expected}"
1902 );
1903 }
1904 }
1905
1906 fn refused(asked: u32, sample_rate: u32, max_buffer_frames: u32) -> StreamFormat {
1910 StreamFormat {
1911 sample_rate,
1912 buffer_size: Some(64),
1913 max_buffer_frames,
1914 channels: 2,
1915 sample_rate_request: Requested::Refused(asked),
1916 buffer_size_request: Requested::Granted,
1917 }
1918 }
1919
1920 #[test]
1925 fn the_mixer_runs_at_the_rate_the_device_granted() {
1926 let requested = crate::EngineConfig { buffer_size: 64, sample_rate: 44100 };
1927 let format = refused(44100, 48000, 4096);
1928 let effective = crate::EngineConfig::from(format);
1929
1930 let (_tx, rx) = mixer_command_channel();
1931 let (clip_tx, _clip_rx) = clip_snapshot_channel();
1932 let mixer = Mixer::new(
1933 rx,
1934 Arc::new(VuLevels::new()),
1935 clip_tx,
1936 effective.sample_rate,
1937 format.max_buffer_frames as usize,
1938 );
1939
1940 assert_eq!(mixer.sample_rate, 48000, "mixer must adopt the device's rate");
1941 assert_ne!(
1942 mixer.sample_rate, requested.sample_rate,
1943 "the request was 44100 and the device said 48000; taking the \
1944 request here is the 8.84%-sharp bug"
1945 );
1946 assert_eq!(mixer.max_buffer_size, 4096);
1947 }
1948
1949 #[test]
1951 fn a_device_that_agrees_leaves_the_request_alone() {
1952 let requested = crate::EngineConfig { buffer_size: 64, sample_rate: 44100 };
1953 let format = StreamFormat {
1954 sample_rate: 44100,
1955 buffer_size: Some(64),
1956 max_buffer_frames: 4096,
1957 channels: 2,
1958 sample_rate_request: Requested::Granted,
1959 buffer_size_request: Requested::Granted,
1960 };
1961 assert_eq!(crate::EngineConfig::from(format), requested);
1962 }
1963
1964 #[test]
1968 fn asking_for_nothing_builds_the_mixer_at_the_devices_rate() {
1969 let format = StreamFormat {
1970 sample_rate: 48000,
1971 buffer_size: None,
1972 max_buffer_frames: 4096,
1973 channels: 2,
1974 sample_rate_request: Requested::Unasked,
1975 buffer_size_request: Requested::Unasked,
1976 };
1977 let effective = crate::EngineConfig::from(format);
1978
1979 let (_tx, rx) = mixer_command_channel();
1980 let (clip_tx, _clip_rx) = clip_snapshot_channel();
1981 let mixer = Mixer::new(
1982 rx,
1983 Arc::new(VuLevels::new()),
1984 clip_tx,
1985 effective.sample_rate,
1986 format.max_buffer_frames as usize,
1987 );
1988 assert_eq!(mixer.sample_rate, 48000);
1989 assert_eq!(mixer.max_buffer_size, 4096);
1990 assert!(format.divergence_notice().is_none(), "following the device is not news");
1991 }
1992
1993 #[test]
1997 fn the_largest_block_the_device_promised_never_grows_a_buffer() {
1998 let max_frames = 512usize;
1999 let (tx, rx) = mixer_command_channel();
2000 let (clip_tx, _clip_rx) = clip_snapshot_channel();
2001 let mut mixer = Mixer::new(
2002 rx,
2003 Arc::new(VuLevels::new()),
2004 clip_tx,
2005 48000,
2006 max_frames,
2007 );
2008 let transport = Arc::new(Transport::new(120.0));
2009 let _handle = add_armed_synth(&tx, 0);
2010 mixer.drain_commands();
2011
2012 let before = (
2015 mixer.scratch_l.capacity(),
2016 mixer.scratch_r.capacity(),
2017 mixer.tracks[0].buf_l.capacity(),
2018 mixer.tracks[0].buf_r.capacity(),
2019 );
2020
2021 transport.play();
2022 let mut output = vec![0.0f32; max_frames * 2];
2023 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
2024
2025 let after = (
2026 mixer.scratch_l.capacity(),
2027 mixer.scratch_r.capacity(),
2028 mixer.tracks[0].buf_l.capacity(),
2029 mixer.tracks[0].buf_r.capacity(),
2030 );
2031 assert_eq!(
2032 before, after,
2033 "a block the size the device promised must fit the buffers as \
2034 allocated; growing one means the audio thread called the allocator"
2035 );
2036 }
2037
2038 #[test]
2042 fn a_steady_state_callback_does_not_allocate() {
2043 let max_frames = 512usize;
2044 let (tx, rx) = mixer_command_channel();
2045 let (clip_tx, _clip_rx) = clip_snapshot_channel();
2046 let mut mixer = Mixer::new(rx, Arc::new(VuLevels::new()), clip_tx, 48000, max_frames);
2047 let transport = Arc::new(Transport::new(120.0));
2048 let _handle = add_armed_synth(&tx, 0);
2049 mixer.drain_commands();
2050 transport.play();
2051
2052 let mut output = vec![0.0f32; max_frames * 2];
2053 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
2057
2058 let allocations = crate::alloc_count::allocations_during(|| {
2059 for _ in 0..8 {
2060 mixer.process(&mut output, &[], &transport);
2061 }
2062 });
2063 assert_eq!(allocations, 0, "Mixer::process reached the allocator");
2064 }
2065
2066 use crate::pattern::{ChainEntry, Lane, PatternEvent, Rate, Step};
2069
2070 fn bare_mixer(
2072 sample_rate: u32,
2073 max_frames: usize,
2074 ) -> (Mixer, Sender<MixerCommand>, Arc<Transport>) {
2075 let (tx, rx) = mixer_command_channel();
2076 let (clip_tx, _clip_rx) = clip_snapshot_channel();
2077 let mixer = Mixer::new(rx, Arc::new(VuLevels::new()), clip_tx, sample_rate, max_frames);
2078 (mixer, tx, Arc::new(Transport::new(120.0)))
2079 }
2080
2081 fn kick_pattern(on: &[usize]) -> PatternBlock {
2083 let mut block = PatternBlock::empty();
2084 block.playing = true;
2085 block.lanes[0] = Lane::drum(36);
2086 for &index in on {
2087 block.lanes[0].steps[index].on = true;
2088 }
2089 block
2090 }
2091
2092 fn add_track(tx: &Sender<MixerCommand>, id: usize) -> Arc<TrackHandle> {
2093 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
2094 tx.send(MixerCommand::AddTrack {
2095 kind: TrackKind::Instrument,
2096 handle: handle.clone(),
2097 })
2098 .unwrap();
2099 handle
2100 }
2101
2102 fn apply_all(mixer: &mut Mixer) {
2103 while !mixer.command_rx.is_empty() {
2104 mixer.drain_commands();
2105 }
2106 }
2107
2108 fn note_ons(track: &AudioTrack) -> impl Iterator<Item = &MidiEvent> {
2109 track.plugin_events.iter().filter(|e| e.status == 0x90 && e.data2 > 0)
2110 }
2111
2112 #[test]
2121 fn a_pattern_step_and_a_clip_note_land_on_the_same_sample() {
2122 for sample_rate in [44_100u32, 48_000, 96_000] {
2123 for frames in [64usize, 256, 470] {
2124 let (mut mixer, tx, transport) = bare_mixer(sample_rate, 512);
2125
2126 let _clip_track = add_track(&tx, 0);
2128 tx.send(MixerCommand::CreateClip {
2129 track_id: 0,
2130 start_tick: 0,
2131 length_ticks: 3840,
2132 })
2133 .unwrap();
2134 tx.send(MixerCommand::UpdateClip {
2135 track_id: 0,
2136 clip_index: 0,
2137 events: vec![ClipEvent { tick: 960, status: 0x90, data1: 60, data2: 100 }],
2138 })
2139 .unwrap();
2140
2141 let _seq_track = add_track(&tx, 1);
2143 tx.send(MixerCommand::SetPattern {
2144 track_id: 1,
2145 slot: 0,
2146 block: kick_pattern(&[4]),
2147 })
2148 .unwrap();
2149 apply_all(&mut mixer);
2150
2151 transport.play();
2152 let mut output = vec![0.0f32; frames * 2];
2153 let mut landed = None;
2154 while transport.position_ticks() < 1_200 {
2155 mixer.process(&mut output, &[], &transport);
2156 let clip_note = note_ons(&mixer.tracks[0]).find(|e| e.data1 == 60);
2157 let step_note = note_ons(&mixer.tracks[1]).find(|e| e.data1 == 36);
2158 match (clip_note, step_note) {
2159 (Some(c), Some(s)) => {
2160 landed = Some((c.sample_offset, s.sample_offset));
2161 break;
2162 }
2163 (None, None) => {}
2164 (clip, step) => panic!(
2165 "at {sample_rate} Hz / {frames} frames only one of them fired: \
2166 clip={clip:?} step={step:?}"
2167 ),
2168 }
2169 transport.advance(frames as u32, sample_rate);
2170 }
2171 let (clip_at, step_at) =
2172 landed.unwrap_or_else(|| panic!("nothing fired at {sample_rate}/{frames}"));
2173 assert_eq!(
2174 clip_at, step_at,
2175 "at {sample_rate} Hz / {frames} frames the clip note landed on sample \
2176 {clip_at} and the step on {step_at}"
2177 );
2178 }
2179 }
2180 }
2181
2182 #[test]
2185 fn step_timing_is_the_same_at_every_sample_rate() {
2186 let frames = 256usize;
2187 for sample_rate in [44_100u32, 48_000, 96_000] {
2188 let (mut mixer, tx, transport) = bare_mixer(sample_rate, 512);
2189 let _track = add_track(&tx, 0);
2190 tx.send(MixerCommand::SetPattern {
2191 track_id: 0,
2192 slot: 0,
2193 block: kick_pattern(&[0, 4, 8, 12]),
2194 })
2195 .unwrap();
2196 apply_all(&mut mixer);
2197
2198 transport.play();
2199 let mut output = vec![0.0f32; frames * 2];
2200 let mut seconds = Vec::new();
2201 let mut block = 0usize;
2202 while seconds.len() < 4 && transport.position_ticks() < 3_600 {
2203 mixer.process(&mut output, &[], &transport);
2204 for event in note_ons(&mixer.tracks[0]) {
2205 let sample = block * frames + event.sample_offset as usize;
2206 seconds.push(sample as f64 / f64::from(sample_rate));
2207 }
2208 transport.advance(frames as u32, sample_rate);
2209 block += 1;
2210 }
2211
2212 assert_eq!(seconds.len(), 4, "at {sample_rate} Hz");
2214 for (index, at) in seconds.iter().enumerate() {
2215 let expected = index as f64 * 0.5;
2216 assert!(
2217 (at - expected).abs() < 0.002,
2218 "at {sample_rate} Hz step {index} landed at {at:.4}s, expected {expected:.4}s"
2219 );
2220 }
2221 }
2222 }
2223
2224 #[test]
2230 fn a_loop_wrap_neither_drops_nor_doubles_the_first_step() {
2231 let frames = 256usize;
2232 let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
2233 let _track = add_track(&tx, 0);
2234 let all_sixteen: Vec<usize> = (0..16).collect();
2235 tx.send(MixerCommand::SetPattern {
2236 track_id: 0,
2237 slot: 0,
2238 block: kick_pattern(&all_sixteen),
2239 })
2240 .unwrap();
2241 apply_all(&mut mixer);
2242
2243 transport.set_loop_bars(1, 1);
2244 transport.toggle_loop();
2245 transport.play();
2246
2247 let mut output = vec![0.0f32; frames * 2];
2248 let mut fired = 0usize;
2249 let mut wraps = 0usize;
2250 let mut last = transport.position_ticks();
2251 for _ in 0..4_000 {
2252 mixer.process(&mut output, &[], &transport);
2253 fired += note_ons(&mixer.tracks[0]).count();
2254 transport.advance(frames as u32, 44_100);
2255 let now = transport.position_ticks();
2256 if now < last {
2257 wraps += 1;
2258 if wraps == 4 {
2259 break;
2260 }
2261 }
2262 last = now;
2263 }
2264 assert_eq!(wraps, 4, "the transport did not loop");
2265 assert_eq!(fired, 64, "four times round a 16-step pattern is 64 onsets");
2266 }
2267
2268 #[test]
2272 fn a_sequencer_track_plays_its_child_instrument() {
2273 let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
2274 let handle = add_track(&tx, 0);
2275 handle.config.set_volume(1.0);
2276 tx.send(MixerCommand::SetInstrument {
2277 track_id: 0,
2278 instrument: Box::new(PhosphorSynth::new()),
2279 })
2280 .unwrap();
2281 let mut block = PatternBlock::empty();
2282 block.playing = true;
2283 block.lanes[0].steps[0].on = true;
2284 block.lanes[0].steps[0].gate = 200;
2285 tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block }).unwrap();
2286 apply_all(&mut mixer);
2287
2288 transport.play();
2289 let mut output = vec![0.0f32; 512 * 2];
2290 let mut peak = 0.0f32;
2291 for _ in 0..8 {
2292 mixer.process(&mut output, &[], &transport);
2293 peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0, f32::max));
2294 transport.advance(512, 44_100);
2295 }
2296 assert!(peak > 0.001, "the child instrument never sounded, peak={peak}");
2297 }
2298
2299 #[test]
2303 fn stopping_the_transport_ends_every_pattern_note() {
2304 let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
2305 let _track = add_track(&tx, 0);
2306 let mut block = kick_pattern(&[0]);
2307 block.lanes[0].steps[0].gate = Step::TIE;
2308 tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block }).unwrap();
2309 apply_all(&mut mixer);
2310
2311 transport.play();
2312 let mut output = vec![0.0f32; 256 * 2];
2313 mixer.process(&mut output, &[], &transport);
2314 assert_eq!(note_ons(&mixer.tracks[0]).count(), 1);
2315 transport.advance(256, 44_100);
2316
2317 transport.pause();
2318 mixer.process(&mut output, &[], &transport);
2319 let offs: Vec<u8> = mixer.tracks[0]
2320 .plugin_events
2321 .iter()
2322 .filter(|e| e.status == 0x80)
2323 .map(|e| e.data1)
2324 .collect();
2325 assert_eq!(offs, vec![36], "the tied note was left sounding");
2326
2327 mixer.process(&mut output, &[], &transport);
2329 assert!(mixer.tracks[0].plugin_events.is_empty());
2330 }
2331
2332 #[test]
2336 fn a_panic_leaves_the_sequencer_holding_nothing() {
2337 let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
2338 let _track = add_track(&tx, 0);
2339 let mut block = kick_pattern(&[0]);
2340 block.lanes[0].steps[0].gate = Step::TIE;
2341 tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block }).unwrap();
2342 apply_all(&mut mixer);
2343
2344 transport.play();
2345 let mut output = vec![0.0f32; 256 * 2];
2346 mixer.process(&mut output, &[], &transport);
2347 assert!(mixer.tracks[0].pattern.as_ref().unwrap().held_notes() > 0);
2348
2349 mixer.reset_all();
2350 assert_eq!(mixer.tracks[0].pattern.as_ref().unwrap().held_notes(), 0);
2351 }
2352
2353 #[test]
2363 fn a_bounced_pattern_renders_identically_to_the_live_one() {
2364 const SWING: u8 = 62;
2365 let mut block = PatternBlock::empty();
2366 block.playing = true;
2367 block.swing = SWING;
2368 block.rate = Rate::Sixteenth;
2369 for (index, (key, chord, gate)) in [
2370 (0usize, 0u8, 5u8, 50u8),
2371 (3, 3, 6, 90),
2372 (5, 7, 1, 25),
2373 (9, 5, 14, 75),
2374 (11, 10, 12, 40),
2375 (14, 0, 15, 60),
2376 ]
2377 .iter()
2378 .map(|(i, k, c, g)| (*i, (*k, *c, *g)))
2379 {
2380 let step = &mut block.lanes[0].steps[index];
2381 step.on = true;
2382 step.key = key;
2383 step.chord = chord;
2384 step.gate = gate;
2385 step.accent = index % 2 == 1;
2386 }
2387
2388 let cycle = block.length_ticks();
2389 let blocks = 24; let live = {
2393 let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
2394 let handle = add_track(&tx, 0);
2395 handle.config.set_volume(1.0);
2396 tx.send(MixerCommand::SetInstrument {
2397 track_id: 0,
2398 instrument: Box::new(PhosphorSynth::new()),
2399 })
2400 .unwrap();
2401 tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block }).unwrap();
2402 apply_all(&mut mixer);
2403 transport.play();
2404
2405 let mut rendered = Vec::new();
2406 let mut output = vec![0.0f32; 512 * 2];
2407 for _ in 0..blocks {
2408 mixer.process(&mut output, &[], &transport);
2409 rendered.extend_from_slice(&output);
2410 transport.advance(512, 44_100);
2411 }
2412 rendered
2413 };
2414
2415 let bounced = {
2417 let mut events = Vec::new();
2418 crate::pattern::compile_cycle(&block, 0, &mut events);
2419 assert!(!events.is_empty());
2420 let clip_events: Vec<ClipEvent> = events
2421 .iter()
2422 .map(|e: &PatternEvent| ClipEvent {
2423 tick: e.tick,
2424 status: e.status,
2425 data1: e.data1,
2426 data2: e.data2,
2427 })
2428 .collect();
2429
2430 let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
2431 let handle = add_track(&tx, 0);
2432 handle.config.set_volume(1.0);
2433 tx.send(MixerCommand::SetInstrument {
2434 track_id: 0,
2435 instrument: Box::new(PhosphorSynth::new()),
2436 })
2437 .unwrap();
2438 tx.send(MixerCommand::CreateClip {
2439 track_id: 0,
2440 start_tick: 0,
2441 length_ticks: cycle,
2442 })
2443 .unwrap();
2444 tx.send(MixerCommand::UpdateClip {
2445 track_id: 0,
2446 clip_index: 0,
2447 events: clip_events,
2448 })
2449 .unwrap();
2450 apply_all(&mut mixer);
2451 transport.play();
2452
2453 let mut rendered = Vec::new();
2454 let mut output = vec![0.0f32; 512 * 2];
2455 for _ in 0..blocks {
2456 mixer.process(&mut output, &[], &transport);
2457 rendered.extend_from_slice(&output);
2458 transport.advance(512, 44_100);
2459 }
2460 rendered
2461 };
2462
2463 assert_eq!(live.len(), bounced.len());
2464 let peak = live.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
2465 assert!(peak > 0.001, "the live render was silent, so this proves nothing");
2466 for (i, (a, b)) in live.iter().zip(&bounced).enumerate() {
2467 assert_eq!(
2468 a.to_bits(),
2469 b.to_bits(),
2470 "sample {i} differs: live {a} bounced {b} at {SWING}% swing"
2471 );
2472 }
2473 }
2474
2475 #[test]
2480 fn pattern_playback_does_not_allocate() {
2481 let (mut mixer, tx, transport) = bare_mixer(48_000, 512);
2482 let _track = add_track(&tx, 0);
2483 tx.send(MixerCommand::SetInstrument {
2484 track_id: 0,
2485 instrument: Box::new(PhosphorSynth::new()),
2486 })
2487 .unwrap();
2488
2489 let mut chords = PatternBlock::empty();
2491 chords.playing = true;
2492 chords.mode = crate::pattern::Mode::Aeolian;
2493 for index in 0..16 {
2494 let step = &mut chords.lanes[0].steps[index];
2495 step.on = true;
2496 step.chord = 4; step.voicing = 1 | Step::ROOT_BELOW;
2498 step.key = (index as u8 * 2) % 12;
2499 }
2500 let drums = kick_pattern(&[0, 4, 8, 12]);
2501
2502 tx.send(MixerCommand::SetPattern { track_id: 0, slot: 1, block: drums }).unwrap();
2503 tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block: chords }).unwrap();
2504 tx.send(MixerCommand::CreateClip { track_id: 0, start_tick: 0, length_ticks: 3840 })
2507 .unwrap();
2508 tx.send(MixerCommand::UpdateClip {
2509 track_id: 0,
2510 clip_index: 0,
2511 events: (0..16)
2512 .flat_map(|i| {
2513 [
2514 ClipEvent { tick: i * 240, status: 0x90, data1: 40, data2: 90 },
2515 ClipEvent { tick: i * 240 + 120, status: 0x80, data1: 40, data2: 0 },
2516 ]
2517 })
2518 .collect(),
2519 })
2520 .unwrap();
2521 apply_all(&mut mixer);
2522 transport.play();
2523
2524 let mut output = vec![0.0f32; 512 * 2];
2525 for _ in 0..2 {
2527 mixer.process(&mut output, &[], &transport);
2528 transport.advance(512, 48_000);
2529 }
2530
2531 let mut queued = chords;
2532 queued.pending_slot = Some(1);
2533 let mut chained = chords;
2534 chained.chain[0] = ChainEntry { slot: 0, repeats: 1 };
2535 chained.chain[1] = ChainEntry { slot: 1, repeats: 1 };
2536 chained.chain_len = 2;
2537
2538 let allocations = crate::alloc_count::allocations_during(|| {
2539 for block in 0..400 {
2540 if block == 20 {
2541 tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block: queued })
2542 .unwrap();
2543 }
2544 if block == 120 {
2545 tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block: chained })
2546 .unwrap();
2547 }
2548 mixer.process(&mut output, &[], &transport);
2549 transport.advance(512, 48_000);
2550 }
2551 transport.pause();
2552 mixer.process(&mut output, &[], &transport);
2553 });
2554 assert_eq!(allocations, 0, "the sequencer reached the allocator");
2555 }
2556
2557 #[test]
2566 fn a_command_is_as_wide_as_a_pattern() {
2567 assert_eq!(
2568 std::mem::size_of::<MixerCommand>(),
2569 crate::pattern::PatternBlock::SIZE + 11
2570 );
2571 }
2572
2573 #[test]
2576 fn the_track_handle_reports_where_the_pattern_is() {
2577 let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
2578 let handle = add_track(&tx, 0);
2579 let all_sixteen: Vec<usize> = (0..16).collect();
2580 let block = kick_pattern(&all_sixteen);
2581 tx.send(MixerCommand::SetPattern { track_id: 0, slot: 1, block }).unwrap();
2582 let mut queued = block;
2583 queued.pending_slot = Some(1);
2584 tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block: queued }).unwrap();
2585 apply_all(&mut mixer);
2586
2587 transport.set_position(240);
2590 transport.play();
2591 let mut output = vec![0.0f32; 256 * 2];
2592 mixer.process(&mut output, &[], &transport);
2593 assert_eq!(handle.pattern.live_slot(), 0);
2594 assert_eq!(handle.pattern.queued_slot(), Some(1));
2595 assert_eq!(handle.pattern.step(), 1);
2596 assert!(handle.pattern.is_running());
2597
2598 transport.set_position(1920);
2600 mixer.process(&mut output, &[], &transport);
2601 assert_eq!(handle.pattern.step(), 8);
2602 assert_eq!(handle.pattern.live_slot(), 0);
2603
2604 transport.set_position(3840);
2606 mixer.process(&mut output, &[], &transport);
2607 assert_eq!(handle.pattern.live_slot(), 1);
2608 assert_eq!(handle.pattern.queued_slot(), None);
2609 }
2610
2611 #[test]
2614 fn a_short_callback_does_not_allocate_either() {
2615 let max_frames = 512usize;
2616 let (tx, rx) = mixer_command_channel();
2617 let (clip_tx, _clip_rx) = clip_snapshot_channel();
2618 let mut mixer = Mixer::new(rx, Arc::new(VuLevels::new()), clip_tx, 48000, max_frames);
2619 let transport = Arc::new(Transport::new(120.0));
2620 let _handle = add_armed_synth(&tx, 0);
2621 mixer.drain_commands();
2622 transport.play();
2623
2624 let mut output = vec![0.0f32; 64 * 2];
2625 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
2626
2627 let allocations = crate::alloc_count::allocations_during(|| {
2628 for _ in 0..8 {
2629 mixer.process(&mut output, &[], &transport);
2630 }
2631 });
2632 assert_eq!(allocations, 0, "Mixer::process reached the allocator");
2633 }
2634}