use std::sync::Arc;
use crossbeam_channel::{Receiver, Sender};
use phosphor_midi::message::MidiMessage;
use phosphor_plugin::{MidiEvent, Plugin};
use crate::clip::{ClipEvent, ClipSnapshot, MidiClip, RecordBuffer};
use crate::engine::VuLevels;
use crate::metronome::Metronome;
use crate::pattern::{EventSink, PatternBlock, PatternEvent, PatternPlayer, PlaybackWindow};
use crate::project::{TrackHandle, TrackKind};
use crate::transport::Transport;
#[allow(clippy::large_enum_variant)]
pub enum MixerCommand {
AddTrack {
kind: TrackKind,
handle: Arc<TrackHandle>,
},
SetInstrument {
track_id: usize,
instrument: Box<dyn Plugin + Send>,
},
RemoveTrack {
track_id: usize,
},
SetParameter {
track_id: usize,
param_index: usize,
value: f32,
},
CreateClip {
track_id: usize,
start_tick: i64,
length_ticks: i64,
},
UpdateClip {
track_id: usize,
clip_index: usize,
events: Vec<ClipEvent>,
},
UpdateClipPosition {
track_id: usize,
clip_index: usize,
start_tick: i64,
length_ticks: i64,
},
RemoveClip {
track_id: usize,
clip_index: usize,
},
SetPattern {
track_id: usize,
slot: u8,
block: PatternBlock,
},
}
const HEAVY_COMMAND: u32 = 16;
fn command_cost(cmd: &MixerCommand) -> u32 {
match cmd {
MixerCommand::SetParameter { .. } | MixerCommand::UpdateClipPosition { .. } => 1,
MixerCommand::AddTrack { .. }
| MixerCommand::SetInstrument { .. }
| MixerCommand::RemoveTrack { .. }
| MixerCommand::CreateClip { .. }
| MixerCommand::UpdateClip { .. }
| MixerCommand::RemoveClip { .. }
| MixerCommand::SetPattern { .. } => HEAVY_COMMAND,
}
}
const COMMAND_BUDGET: u32 = 64;
const TRACK_CAPACITY: usize = 64;
const LIMITER_CEILING: f32 = 0.891;
const LIMITER_RELEASE_SECONDS: f32 = 0.050;
struct MasterLimiter {
gain: f32,
release_coeff: f32,
}
impl MasterLimiter {
fn new(sample_rate: u32) -> Self {
let sr = (sample_rate as f32).max(1.0);
Self {
gain: 1.0,
release_coeff: 1.0 - (-1.0 / (LIMITER_RELEASE_SECONDS * sr)).exp(),
}
}
fn reset(&mut self) {
self.gain = 1.0;
}
fn process(&mut self, output: &mut [f32]) {
let mut frames = output.chunks_exact_mut(2);
for frame in frames.by_ref() {
let l = if frame[0].is_finite() { frame[0] } else { 0.0 };
let r = if frame[1].is_finite() { frame[1] } else { 0.0 };
let peak = l.abs().max(r.abs());
let target = if peak > LIMITER_CEILING {
(LIMITER_CEILING / peak) * (1.0 - 2.0 * f32::EPSILON)
} else {
1.0
};
if target < self.gain {
self.gain = target;
} else {
self.gain += (target - self.gain) * self.release_coeff;
}
frame[0] = (l * self.gain).clamp(-1.0, 1.0);
frame[1] = (r * self.gain).clamp(-1.0, 1.0);
}
for tail in frames.into_remainder() {
let s = if tail.is_finite() { *tail } else { 0.0 };
*tail = (s * self.gain).clamp(-LIMITER_CEILING, LIMITER_CEILING);
}
}
}
const PLUGIN_EVENT_CAPACITY: usize = 512;
pub struct AudioTrack {
pub id: usize,
pub kind: TrackKind,
pub handle: Arc<TrackHandle>,
pub instrument: Option<Box<dyn Plugin>>,
pub clips: Vec<MidiClip>,
pattern: Option<Box<PatternPlayer>>,
record_buf: RecordBuffer,
was_recording: bool,
last_record_tick: i64,
buf_l: Vec<f32>,
buf_r: Vec<f32>,
plugin_events: Vec<MidiEvent>,
}
impl AudioTrack {
pub fn new(handle: Arc<TrackHandle>, max_buffer_size: usize) -> Self {
Self {
id: handle.id,
kind: handle.kind,
handle,
instrument: None,
clips: Vec::new(),
pattern: None,
record_buf: RecordBuffer::new(),
was_recording: false,
last_record_tick: -1,
buf_l: vec![0.0; max_buffer_size],
buf_r: vec![0.0; max_buffer_size],
plugin_events: Vec::with_capacity(PLUGIN_EVENT_CAPACITY),
}
}
}
struct TrackEventSink<'a> {
events: &'a mut Vec<MidiEvent>,
window: &'a PlaybackWindow,
}
impl EventSink for TrackEventSink<'_> {
fn accept(&mut self, event: PatternEvent) -> bool {
if self.events.len() >= self.events.capacity() {
return false;
}
self.events.push(MidiEvent {
sample_offset: self.window.sample_offset(event.tick),
status: event.status,
data1: event.data1,
data2: event.data2,
});
true
}
}
fn sort_events_by_offset(events: &mut [MidiEvent]) {
for i in 1..events.len() {
let mut j = i;
while j > 0 && events[j - 1].sample_offset > events[j].sample_offset {
events.swap(j - 1, j);
j -= 1;
}
}
}
pub struct Mixer {
tracks: Vec<AudioTrack>,
master_vu: Arc<VuLevels>,
command_rx: Receiver<MixerCommand>,
clip_tx: Sender<ClipSnapshot>,
metronome: Metronome,
sample_rate: u32,
max_buffer_size: usize,
scratch_l: Vec<f32>,
scratch_r: Vec<f32>,
live_events: Vec<MidiEvent>,
last_window: Option<PlaybackWindow>,
limiter: MasterLimiter,
}
impl Mixer {
pub fn new(
command_rx: Receiver<MixerCommand>,
master_vu: Arc<VuLevels>,
clip_tx: Sender<ClipSnapshot>,
sample_rate: u32,
max_buffer_size: usize,
) -> Self {
Self {
tracks: Vec::with_capacity(TRACK_CAPACITY),
master_vu,
command_rx,
clip_tx,
metronome: Metronome::new(sample_rate as f64),
sample_rate,
max_buffer_size,
scratch_l: vec![0.0; max_buffer_size],
scratch_r: vec![0.0; max_buffer_size],
live_events: Vec::with_capacity(256),
last_window: None,
limiter: MasterLimiter::new(sample_rate),
}
}
pub fn process(&mut self, output: &mut [f32], midi_messages: &[MidiMessage], transport: &Transport) {
let _ = self.drain_commands();
let num_frames = output.len() / 2;
let playing = transport.is_playing();
let recording = transport.is_recording();
let looping = transport.is_looping();
let current_tick = transport.position_ticks();
let bpm = transport.tempo_bpm();
let ticks_per_sample = (bpm * Transport::PPQ as f64) / (60.0 * self.sample_rate as f64);
let loop_end = transport.loop_end();
let window = PlaybackWindow::for_block(
current_tick,
num_frames as u32,
ticks_per_sample,
looping.then(|| (transport.loop_start(), loop_end)),
self.last_window,
);
self.last_window = playing.then_some(window);
self.live_events.clear();
for msg in midi_messages {
if let Some(ev) = midi_to_plugin_event(msg) {
self.live_events.push(ev);
}
}
let any_solo = self.tracks.iter().any(|t| t.handle.config.is_soloed());
let mut master_l = std::mem::take(&mut self.scratch_l);
let mut master_r = std::mem::take(&mut self.scratch_r);
let live_events = std::mem::take(&mut self.live_events);
if master_l.len() < num_frames {
master_l.resize(num_frames, 0.0);
master_r.resize(num_frames, 0.0);
}
master_l[..num_frames].fill(0.0);
master_r[..num_frames].fill(0.0);
let clip_tx = &self.clip_tx;
for track in &mut self.tracks {
if track.buf_l.len() < num_frames {
track.buf_l.resize(num_frames, 0.0);
track.buf_r.resize(num_frames, 0.0);
}
track.buf_l[..num_frames].fill(0.0);
track.buf_r[..num_frames].fill(0.0);
track.plugin_events.clear();
let is_midi_active = track.kind == TrackKind::Instrument
&& track.handle.config.is_midi_active();
let is_armed = track.handle.config.is_armed();
let should_record = playing && recording && is_armed && is_midi_active;
if should_record && !track.was_recording {
let rec_start = if looping { transport.loop_start() } else { current_tick };
track.record_buf.start(rec_start);
tracing::debug!("rec start track={} tick={}", track.id, current_tick);
}
if should_record && track.was_recording && looping
&& track.record_buf.is_active() && track.last_record_tick >= 0
&& current_tick < track.last_record_tick
{
commit_recording(track, loop_end, clip_tx);
track.record_buf.start(transport.loop_start());
}
if should_record {
track.last_record_tick = current_tick;
}
if !should_record && track.was_recording {
commit_recording(track, current_tick, clip_tx);
}
track.was_recording = should_record;
if is_midi_active {
for ev in &live_events {
track.plugin_events.push(*ev);
if should_record {
let event_tick = current_tick
+ (ev.sample_offset as f64 * ticks_per_sample) as i64;
track.record_buf.record(event_tick, ev.status, ev.data1, ev.data2);
}
}
}
if let Some(ref mut player) = track.pattern {
let mut sink = TrackEventSink { events: &mut track.plugin_events, window: &window };
player.render(&window, playing, &mut sink);
track.handle.pattern.publish(
player.live_slot(),
player.queued_slot(),
player.current_step(),
playing && player.is_playing(),
);
}
if playing && !track.clips.is_empty() {
for clip in &track.clips {
for (tick, event) in clip.events_between(window.from(), window.to()) {
if track.plugin_events.len() >= track.plugin_events.capacity() {
break;
}
track.plugin_events.push(MidiEvent {
sample_offset: window.sample_offset(tick),
status: event.status,
data1: event.data1,
data2: event.data2,
});
}
}
}
if !track.plugin_events.is_empty() {
sort_events_by_offset(&mut track.plugin_events);
}
if playing {
track.last_record_tick = current_tick;
}
if let Some(ref mut instrument) = track.instrument {
let out_l = &mut track.buf_l[..num_frames];
let out_r = &mut track.buf_r[..num_frames];
let mut out_slices: [&mut [f32]; 2] = [out_l, out_r];
instrument.process(&[], &mut out_slices, &track.plugin_events);
}
let muted = track.handle.config.is_muted();
let soloed = track.handle.config.is_soloed();
let audible = !muted && (!any_solo || soloed);
let volume = track.handle.config.get_volume();
let mut peak_l = 0.0f32;
let mut peak_r = 0.0f32;
for i in 0..num_frames {
peak_l = peak_l.max(track.buf_l[i].abs());
peak_r = peak_r.max(track.buf_r[i].abs());
}
let (old_l, old_r) = track.handle.vu.get();
let decay = 0.85f32;
track.handle.vu.set(
if peak_l > old_l { peak_l } else { old_l * decay },
if peak_r > old_r { peak_r } else { old_r * decay },
);
if audible {
for i in 0..num_frames {
master_l[i] += track.buf_l[i] * volume;
master_r[i] += track.buf_r[i] * volume;
}
}
}
for i in 0..num_frames {
output[i * 2] = master_l[i];
output[i * 2 + 1] = master_r[i];
}
self.scratch_l = master_l;
self.scratch_r = master_r;
self.live_events = live_events;
self.metronome.process(output, transport);
self.limiter.process(output);
let mut mp_l = 0.0f32;
let mut mp_r = 0.0f32;
for i in 0..num_frames {
mp_l = mp_l.max(output[i * 2].abs());
mp_r = mp_r.max(output[i * 2 + 1].abs());
}
let (old_l, old_r) = self.master_vu.get();
let decay = 0.85f32;
self.master_vu.set(
if mp_l > old_l { mp_l } else { old_l * decay },
if mp_r > old_r { mp_r } else { old_r * decay },
);
}
pub fn reset_all(&mut self) {
let clip_tx = &self.clip_tx;
for track in &mut self.tracks {
if let Some(ref mut inst) = track.instrument {
inst.reset();
}
track.handle.vu.set(0.0, 0.0);
if track.record_buf.is_active() && track.was_recording {
let end_tick = track.last_record_tick.max(0);
commit_recording(track, end_tick, clip_tx);
} else if track.record_buf.is_active() {
track.record_buf.discard();
}
track.was_recording = false;
if let Some(ref mut player) = track.pattern {
player.silence();
}
}
self.last_window = None;
self.metronome.reset();
self.limiter.reset();
}
fn drain_commands(&mut self) -> u32 {
let mut spent = 0;
while spent < COMMAND_BUDGET {
let Ok(cmd) = self.command_rx.try_recv() else { break };
spent += command_cost(&cmd);
self.apply_command(cmd);
}
spent
}
fn apply_command(&mut self, cmd: MixerCommand) {
match cmd {
MixerCommand::AddTrack { kind: _, handle } => {
let track = AudioTrack::new(handle, self.max_buffer_size);
self.tracks.push(track);
}
MixerCommand::SetInstrument { track_id, mut instrument } => {
if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
instrument.init(self.sample_rate as f64, self.max_buffer_size);
track.instrument = Some(instrument);
}
}
MixerCommand::RemoveTrack { track_id } => {
self.tracks.retain(|t| t.id != track_id);
}
MixerCommand::SetParameter { track_id, param_index, value } => {
if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
if let Some(ref mut inst) = track.instrument {
inst.set_parameter(param_index, value);
}
}
}
MixerCommand::CreateClip { track_id, start_tick, length_ticks } => {
if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
track.clips.push(MidiClip::new(start_tick, length_ticks, Vec::new()));
}
}
MixerCommand::UpdateClip { track_id, clip_index, events } => {
if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
if let Some(clip) = track.clips.get_mut(clip_index) {
clip.events = events;
clip.events.sort_by_key(|e| e.tick);
}
}
}
MixerCommand::UpdateClipPosition { track_id, clip_index, start_tick, length_ticks } => {
if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
if let Some(clip) = track.clips.get_mut(clip_index) {
clip.start_tick = start_tick;
clip.length_ticks = length_ticks;
}
}
}
MixerCommand::RemoveClip { track_id, clip_index } => {
if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
if clip_index < track.clips.len() {
track.clips.remove(clip_index);
}
}
}
MixerCommand::SetPattern { track_id, slot, block } => {
if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
let player = track.pattern.get_or_insert_with(|| Box::new(PatternPlayer::new()));
player.apply(slot, block);
}
}
}
}
}
fn commit_recording(track: &mut AudioTrack, end_tick: i64, clip_tx: &Sender<ClipSnapshot>) {
if let Some(clip) = track.record_buf.commit(end_tick) {
let idx = track.clips.len();
tracing::debug!(
"rec commit track={}: {} events, ticks {}..{}",
track.id, clip.events.len(), clip.start_tick, clip.end_tick()
);
let snapshot = ClipSnapshot::from_clip(track.id, idx, &clip);
track.clips.push(clip);
let _ = clip_tx.send(snapshot);
}
}
pub fn midi_to_plugin_event(msg: &MidiMessage) -> Option<MidiEvent> {
use phosphor_midi::message::MidiMessageType;
match msg.message_type {
MidiMessageType::NoteOn { .. }
| MidiMessageType::NoteOff { .. }
| MidiMessageType::ControlChange { .. }
| MidiMessageType::PitchBend { .. }
| MidiMessageType::ChannelPressure { .. } => Some(MidiEvent {
sample_offset: 0,
status: msg.raw[0],
data1: msg.raw[1],
data2: msg.raw[2],
}),
_ => None,
}
}
pub fn mixer_command_channel() -> (Sender<MixerCommand>, Receiver<MixerCommand>) {
crossbeam_channel::unbounded()
}
pub fn clip_snapshot_channel() -> (Sender<ClipSnapshot>, Receiver<ClipSnapshot>) {
crossbeam_channel::unbounded()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cpal_backend::{Requested, StreamFormat};
use crate::project::TrackConfig;
use phosphor_dsp::synth::PhosphorSynth;
use phosphor_midi::message::{MidiMessage, MidiMessageType};
fn make_note_on(note: u8, vel: u8) -> MidiMessage {
MidiMessage {
timestamp: Some(0),
message_type: MidiMessageType::NoteOn { channel: 0, note, velocity: vel },
raw: [0x90, note, vel],
len: 3,
}
}
#[test]
fn channel_pressure_reaches_the_plugin_and_key_pressure_does_not() {
let pressure = MidiMessage {
timestamp: Some(0),
message_type: MidiMessageType::ChannelPressure { channel: 0, pressure: 96 },
raw: [0xD0, 96, 0],
len: 2,
};
let event = midi_to_plugin_event(&pressure).expect("channel pressure is dropped");
assert_eq!(event.status, 0xD0);
assert_eq!(event.data1, 96);
let key = MidiMessage::from_bytes(&[0xA0, 60, 96], 0).expect("parsed");
assert!(
midi_to_plugin_event(&key).is_none(),
"polyphonic key pressure has no destination in the rack"
);
}
fn make_note_off(note: u8) -> MidiMessage {
MidiMessage {
timestamp: Some(0),
message_type: MidiMessageType::NoteOff { channel: 0, note, velocity: 0 },
raw: [0x80, note, 0],
len: 3,
}
}
fn setup_mixer() -> (Mixer, Sender<MixerCommand>, Receiver<ClipSnapshot>, Arc<Transport>) {
let (tx, rx) = mixer_command_channel();
let (clip_tx, clip_rx) = clip_snapshot_channel();
let master_vu = Arc::new(VuLevels::new());
let transport = Arc::new(Transport::new(120.0));
let mixer = Mixer::new(rx, master_vu, clip_tx, 44100, 256);
(mixer, tx, clip_rx, transport)
}
fn add_armed_synth(tx: &Sender<MixerCommand>, id: usize) -> Arc<TrackHandle> {
let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
handle.config.armed.store(true, std::sync::atomic::Ordering::Relaxed);
tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
tx.send(MixerCommand::SetInstrument { track_id: id, instrument: Box::new(PhosphorSynth::new()) }).unwrap();
handle
}
#[test]
fn mixer_empty_output() {
let (mut mixer, _tx, _clip_rx, transport) = setup_mixer();
let mut output = vec![0.0f32; 128];
mixer.process(&mut output, &[], &transport);
assert!(output.iter().all(|&s| s == 0.0));
}
#[test]
fn mixer_live_midi_produces_sound() {
let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
let _handle = add_armed_synth(&tx, 0);
transport.play();
let midi = vec![make_note_on(60, 100)];
let mut output = vec![0.0f32; 512];
mixer.process(&mut output, &midi, &transport);
let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
assert!(peak > 0.001, "Should produce sound, peak={peak}");
}
#[test]
fn mixer_records_midi_clip() {
let (mut mixer, tx, clip_rx, transport) = setup_mixer();
let _handle = add_armed_synth(&tx, 0);
transport.play();
transport.toggle_record();
let midi = vec![make_note_on(60, 100)];
let mut output = vec![0.0f32; 512];
mixer.process(&mut output, &midi, &transport);
let midi = vec![make_note_off(60)];
mixer.process(&mut output, &midi, &transport);
transport.toggle_record();
mixer.process(&mut output, &[], &transport);
let snap = clip_rx.try_recv().expect("Should receive clip snapshot");
assert_eq!(snap.track_id, 0);
assert!(snap.event_count >= 2, "Should have note on + off, got {}", snap.event_count);
assert!(!snap.notes.is_empty(), "Should have parsed notes");
}
#[test]
fn mixer_plays_back_recorded_clip() {
let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
let _handle = add_armed_synth(&tx, 0);
transport.play();
transport.toggle_record();
let midi = vec![make_note_on(60, 100)];
let mut output = vec![0.0f32; 512];
mixer.process(&mut output, &midi, &transport);
let midi = vec![make_note_off(60)];
mixer.process(&mut output, &midi, &transport);
transport.toggle_record();
mixer.process(&mut output, &[], &transport);
transport.stop();
transport.play();
output.fill(0.0);
mixer.process(&mut output, &[], &transport);
let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
assert!(peak > 0.001, "Playback should produce sound, peak={peak}");
}
#[test]
fn mixer_mute_silences() {
let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
let handle = add_armed_synth(&tx, 0);
handle.config.muted.store(true, std::sync::atomic::Ordering::Relaxed);
transport.play();
let midi = vec![make_note_on(60, 100)];
let mut output = vec![0.0f32; 512];
mixer.process(&mut output, &midi, &transport);
let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
assert!(peak == 0.0, "Muted track should be silent, peak={peak}");
}
#[test]
fn mixer_no_record_when_not_armed() {
let (mut mixer, tx, clip_rx, transport) = setup_mixer();
let handle = add_armed_synth(&tx, 0);
handle.config.armed.store(false, std::sync::atomic::Ordering::Relaxed);
transport.play();
transport.toggle_record();
let midi = vec![make_note_on(60, 100)];
let mut output = vec![0.0f32; 512];
mixer.process(&mut output, &midi, &transport);
transport.toggle_record();
mixer.process(&mut output, &[], &transport);
assert!(clip_rx.try_recv().is_err(), "Should not record when not armed");
}
#[test]
fn mixer_reset_commits_recording() {
let (mut mixer, tx, clip_rx, transport) = setup_mixer();
let _handle = add_armed_synth(&tx, 0);
transport.play();
transport.toggle_record();
let midi = vec![make_note_on(60, 100)];
let mut output = vec![0.0f32; 512];
mixer.process(&mut output, &midi, &transport);
mixer.reset_all();
assert!(clip_rx.try_recv().is_ok(), "Reset should commit active recording");
}
#[test]
fn end_to_end_record_and_playback() {
let (mut mixer, tx, clip_rx, transport) = setup_mixer();
let _handle = add_armed_synth(&tx, 0);
let sr = 44100u32;
let buf_frames = 256;
let buf_samples = buf_frames * 2;
transport.toggle_record();
transport.play();
let mut output = vec![0.0f32; buf_samples];
for _ in 0..4 {
mixer.process(&mut output, &[], &transport);
transport.advance(buf_frames as u32, sr);
}
let midi = vec![make_note_on(60, 100)];
mixer.process(&mut output, &midi, &transport);
let peak_during = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
assert!(peak_during > 0.001, "Should hear note during recording (monitoring)");
transport.advance(buf_frames as u32, sr);
for _ in 0..8 {
output.fill(0.0);
mixer.process(&mut output, &[], &transport);
transport.advance(buf_frames as u32, sr);
}
let midi = vec![make_note_off(60)];
mixer.process(&mut output, &midi, &transport);
transport.advance(buf_frames as u32, sr);
for _ in 0..4 {
output.fill(0.0);
mixer.process(&mut output, &[], &transport);
transport.advance(buf_frames as u32, sr);
}
transport.toggle_record();
mixer.process(&mut output, &[], &transport);
transport.advance(buf_frames as u32, sr);
let snap = clip_rx.try_recv().expect("Should receive clip snapshot after stopping record");
assert!(snap.event_count >= 2, "Clip should have note on + off");
assert!(!snap.notes.is_empty(), "Clip should have parsed notes");
transport.stop();
transport.play();
for _ in 0..4 {
output.fill(0.0);
mixer.process(&mut output, &[], &transport);
transport.advance(buf_frames as u32, sr);
}
output.fill(0.0);
mixer.process(&mut output, &[], &transport);
let peak_playback = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
assert!(peak_playback > 0.001, "Playback should produce sound at the recorded position, peak={peak_playback}");
}
#[test]
fn loop_record_commits_on_wrap() {
let (mut mixer, tx, clip_rx, transport) = setup_mixer();
let _handle = add_armed_synth(&tx, 0);
let sr = 44100u32;
let buf_frames = 256u32;
transport.set_loop_bars(1, 1);
transport.start_loop_record();
let mut output = vec![0.0f32; buf_frames as usize * 2];
let midi = vec![make_note_on(60, 100)];
mixer.process(&mut output, &midi, &transport);
transport.advance(buf_frames, sr);
for _ in 0..5 {
mixer.process(&mut output, &[], &transport);
transport.advance(buf_frames, sr);
}
let midi = vec![make_note_off(60)];
mixer.process(&mut output, &midi, &transport);
transport.advance(buf_frames, sr);
for _ in 0..400 {
mixer.process(&mut output, &[], &transport);
transport.advance(buf_frames, sr);
if let Ok(snap) = clip_rx.try_recv() {
assert!(snap.event_count >= 2, "Clip should have events, got {}", snap.event_count);
assert!(!snap.notes.is_empty(), "Clip should have notes");
transport.stop_loop_record();
return;
}
}
panic!("Recording should have committed when the loop wrapped");
}
#[test]
fn loop_playback_after_record() {
let (mut mixer, tx, clip_rx, transport) = setup_mixer();
let _handle = add_armed_synth(&tx, 0);
let sr = 44100u32;
let bf = 256u32;
transport.set_loop_bars(1, 1);
transport.start_loop_record();
let mut output = vec![0.0f32; bf as usize * 2];
mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
transport.advance(bf, sr);
for _ in 0..3 {
mixer.process(&mut output, &[], &transport);
transport.advance(bf, sr);
}
mixer.process(&mut output, &[make_note_off(60)], &transport);
transport.advance(bf, sr);
for _ in 0..200 {
mixer.process(&mut output, &[], &transport);
transport.advance(bf, sr);
if clip_rx.try_recv().is_ok() { break; }
}
transport.stop_loop_record();
transport.set_position(0);
transport.toggle_loop(); transport.play();
output.fill(0.0);
mixer.process(&mut output, &[], &transport);
let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
assert!(peak > 0.001, "Should hear playback, peak={peak}");
}
const WORST_CALLBACK: u32 = COMMAND_BUDGET - 1 + HEAVY_COMMAND;
#[derive(Clone)]
struct ParamLog(Arc<std::sync::Mutex<Vec<(usize, f32)>>>);
impl ParamLog {
fn new() -> Self {
Self(Arc::new(std::sync::Mutex::new(Vec::new())))
}
fn seen(&self) -> Vec<(usize, f32)> {
self.0.lock().unwrap().clone()
}
}
impl Plugin for ParamLog {
fn info(&self) -> phosphor_plugin::PluginInfo {
phosphor_plugin::PluginInfo {
name: "ParamLog".into(),
version: "0".into(),
author: "test".into(),
category: phosphor_plugin::PluginCategory::Instrument,
}
}
fn init(&mut self, _sample_rate: f64, _max_buffer_size: usize) {}
fn process(&mut self, _inputs: &[&[f32]], _outputs: &mut [&mut [f32]], _midi: &[MidiEvent]) {}
fn parameter_count(&self) -> usize { 8 }
fn parameter_info(&self, _index: usize) -> Option<phosphor_plugin::ParameterInfo> { None }
fn get_parameter(&self, _index: usize) -> f32 { 0.0 }
fn set_parameter(&mut self, index: usize, value: f32) {
self.0.lock().unwrap().push((index, value));
}
fn reset(&mut self) {}
}
fn add_logging_track(mixer: &mut Mixer, tx: &Sender<MixerCommand>, id: usize) -> ParamLog {
let log = ParamLog::new();
let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
tx.send(MixerCommand::SetInstrument {
track_id: id,
instrument: Box::new(log.clone()),
}).unwrap();
mixer.drain_commands();
log
}
#[test]
fn one_callback_applies_a_bounded_amount_of_work() {
let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
let log = add_logging_track(&mut mixer, &tx, 0);
for i in 0..500 {
tx.send(MixerCommand::SetParameter {
track_id: 0,
param_index: i % 8,
value: i as f32,
}).unwrap();
}
let spent = mixer.drain_commands();
assert!(
spent <= WORST_CALLBACK,
"one callback spent {spent} units, over the {WORST_CALLBACK} bound"
);
assert_eq!(
log.seen().len(),
COMMAND_BUDGET as usize,
"a parameter costs one unit, so a full budget is exactly that many"
);
assert!(!mixer.command_rx.is_empty(), "the rest has to still be queued");
}
#[test]
fn nothing_is_lost_or_reordered_across_callbacks() {
let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
let log = add_logging_track(&mut mixer, &tx, 0);
let sent: Vec<(usize, f32)> = (0..500).map(|i| (i % 8, i as f32)).collect();
for &(param_index, value) in &sent {
tx.send(MixerCommand::SetParameter { track_id: 0, param_index, value }).unwrap();
}
let mut output = vec![0.0f32; 128];
let mut callbacks = 0;
while !mixer.command_rx.is_empty() {
mixer.process(&mut output, &[], &transport);
callbacks += 1;
assert!(callbacks < 100, "the drain is not making progress");
}
assert!(
callbacks >= 500 / COMMAND_BUDGET as usize,
"500 commands went through in {callbacks} callbacks, so the budget did not hold"
);
assert_eq!(log.seen(), sent, "the audio thread saw a different sequence");
}
#[test]
fn a_track_and_its_instrument_survive_a_budget_boundary() {
let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
let log = ParamLog::new();
for _ in 0..COMMAND_BUDGET {
tx.send(MixerCommand::SetParameter { track_id: 99, param_index: 0, value: 0.0 })
.unwrap();
}
let handle = Arc::new(TrackHandle::new(7, TrackKind::Instrument));
tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
tx.send(MixerCommand::SetInstrument {
track_id: 7,
instrument: Box::new(log.clone()),
}).unwrap();
tx.send(MixerCommand::SetParameter { track_id: 7, param_index: 3, value: 0.5 }).unwrap();
let mut output = vec![0.0f32; 128];
mixer.process(&mut output, &[], &transport);
assert!(mixer.tracks.is_empty(), "the budget did not stop at the parameters");
while !mixer.command_rx.is_empty() {
mixer.process(&mut output, &[], &transport);
}
assert_eq!(mixer.tracks.len(), 1);
assert!(mixer.tracks[0].instrument.is_some(), "the instrument never arrived");
assert_eq!(
log.seen(),
vec![(3, 0.5)],
"the parameter that follows the instrument did not reach it"
);
}
#[test]
fn an_instrument_load_costs_more_than_a_parameter() {
let param = MixerCommand::SetParameter { track_id: 0, param_index: 0, value: 0.0 };
let load = MixerCommand::SetInstrument {
track_id: 0,
instrument: Box::new(FixedOutput(0.0)),
};
assert!(command_cost(&load) > command_cost(¶m));
let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
for id in 0..8 {
let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
}
while !mixer.command_rx.is_empty() {
mixer.drain_commands();
}
for id in 0..8 {
tx.send(MixerCommand::SetInstrument {
track_id: id,
instrument: Box::new(FixedOutput(0.25)),
}).unwrap();
}
mixer.drain_commands();
let loaded = mixer.tracks.iter().filter(|t| t.instrument.is_some()).count();
assert_eq!(loaded, (COMMAND_BUDGET / HEAVY_COMMAND) as usize);
}
#[test]
fn adding_tracks_does_not_grow_the_track_list() {
let (mut mixer, tx, _clip_rx, _transport) = setup_mixer();
let capacity = mixer.tracks.capacity();
assert!(capacity >= TRACK_CAPACITY);
for id in 0..TRACK_CAPACITY {
let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
}
while !mixer.command_rx.is_empty() {
mixer.drain_commands();
}
assert_eq!(mixer.tracks.len(), TRACK_CAPACITY);
assert_eq!(
mixer.tracks.capacity(), capacity,
"the track list reallocated on the audio thread"
);
}
struct FixedOutput(f32);
impl Plugin for FixedOutput {
fn info(&self) -> phosphor_plugin::PluginInfo {
phosphor_plugin::PluginInfo {
name: "Fixed".into(),
version: "0".into(),
author: "test".into(),
category: phosphor_plugin::PluginCategory::Instrument,
}
}
fn init(&mut self, _sample_rate: f64, _max_buffer_size: usize) {}
fn process(&mut self, _inputs: &[&[f32]], outputs: &mut [&mut [f32]], _midi: &[MidiEvent]) {
for ch in outputs.iter_mut() {
ch.fill(self.0);
}
}
fn parameter_count(&self) -> usize { 0 }
fn parameter_info(&self, _index: usize) -> Option<phosphor_plugin::ParameterInfo> { None }
fn get_parameter(&self, _index: usize) -> f32 { 0.0 }
fn set_parameter(&mut self, _index: usize, _value: f32) {}
fn reset(&mut self) {}
}
fn add_fixed_track(tx: &Sender<MixerCommand>, id: usize, value: f32) -> Arc<TrackHandle> {
let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
handle.config.set_volume(1.0);
tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
tx.send(MixerCommand::SetInstrument {
track_id: id,
instrument: Box::new(FixedOutput(value)),
}).unwrap();
handle
}
#[test]
fn master_limiter_bounds_many_loud_tracks() {
let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
for id in 0..6 {
add_fixed_track(&tx, id, 0.75);
}
transport.play();
let mut output = vec![0.0f32; 512];
for _ in 0..8 {
mixer.process(&mut output, &[], &transport);
for (i, &s) in output.iter().enumerate() {
assert!(s.is_finite(), "non-finite sample at {i}");
assert!(s.abs() <= 1.0, "sample {i} left the mixer at {s}");
}
}
let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
assert!(peak > 0.8, "limiter over-attenuated, peak={peak}");
}
#[test]
fn non_finite_track_output_becomes_silence() {
let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
add_fixed_track(&tx, 0, f32::NAN);
transport.play();
let mut output = vec![0.0f32; 512];
mixer.process(&mut output, &[], &transport);
assert!(output.iter().all(|s| *s == 0.0), "NaN track should render as silence");
tx.send(MixerCommand::RemoveTrack { track_id: 0 }).unwrap();
add_fixed_track(&tx, 1, 0.5);
mixer.process(&mut output, &[], &transport);
let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
assert!((peak - 0.5).abs() < 1.0e-6, "mixer did not recover, peak={peak}");
}
#[test]
fn infinite_track_output_becomes_silence() {
let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
add_fixed_track(&tx, 0, f32::INFINITY);
transport.play();
let mut output = vec![0.0f32; 512];
mixer.process(&mut output, &[], &transport);
assert!(output.iter().all(|s| *s == 0.0), "infinite track should render as silence");
}
#[test]
fn limiter_is_bit_identical_below_the_ceiling() {
let mut limiter = MasterLimiter::new(44_100);
let mut input: Vec<f32> = Vec::new();
for i in 0..20_000u32 {
let phase = i as f32 * 0.01;
let amp = LIMITER_CEILING * (i as f32 / 20_000.0);
input.push(phase.sin() * amp);
input.push(phase.cos() * amp);
}
input.push(LIMITER_CEILING);
input.push(-LIMITER_CEILING);
input.push(0.0);
input.push(-0.0);
input.push(f32::MIN_POSITIVE);
input.push(-f32::MIN_POSITIVE);
let mut output = input.clone();
limiter.process(&mut output);
for (i, (a, b)) in input.iter().zip(output.iter()).enumerate() {
assert_eq!(a.to_bits(), b.to_bits(), "limiter altered sample {i}: {a} -> {b}");
}
}
#[test]
fn limiter_holds_the_ceiling_under_abuse() {
let mut limiter = MasterLimiter::new(44_100);
for amplitude in [1.0f32, 2.0, 10.0, 1.0e3, 1.0e6, 1.0e30] {
let mut buf: Vec<f32> = (0..4_096)
.map(|i| (i as f32 * 0.05).sin() * amplitude)
.collect();
limiter.process(&mut buf);
for (i, &s) in buf.iter().enumerate() {
assert!(s.is_finite(), "amplitude {amplitude}: sample {i} is {s}");
assert!(
s.abs() <= LIMITER_CEILING,
"amplitude {amplitude}: sample {i} reached {s}, above the ceiling"
);
}
}
}
#[test]
fn limiter_attack_has_no_overshoot() {
let mut limiter = MasterLimiter::new(44_100);
let mut buf = vec![0.0f32; 64];
limiter.process(&mut buf);
let mut step = vec![4.0f32; 64];
limiter.process(&mut step);
assert!(
step[0].abs() <= LIMITER_CEILING,
"first sample of the step overshot to {}",
step[0]
);
}
#[test]
fn limiter_release_is_gradual() {
let mut limiter = MasterLimiter::new(44_100);
let mut loud = vec![4.0f32; 64];
limiter.process(&mut loud);
let reduced = limiter.gain;
assert!(reduced < 0.5, "limiter did not engage, gain={reduced}");
let mut quiet = vec![0.1f32; 441 * 2];
limiter.process(&mut quiet);
assert!(limiter.gain > reduced, "gain did not recover at all");
assert!(
limiter.gain < 1.0,
"gain snapped back to unity within 10 ms, which is a click"
);
let mut long = vec![0.1f32; 22_050 * 2];
limiter.process(&mut long);
assert!(
(limiter.gain - 1.0).abs() < 1.0e-4,
"gain never returned to unity: {}",
limiter.gain
);
}
#[test]
fn limiter_does_not_shift_the_stereo_image() {
let mut limiter = MasterLimiter::new(44_100);
let mut buf: Vec<f32> = Vec::new();
for i in 0..1_024 {
let phase = i as f32 * 0.05;
buf.push(phase.sin() * 3.0);
buf.push(phase.sin() * 1.5);
}
limiter.process(&mut buf);
for frame in buf.chunks_exact(2) {
if frame[1].abs() > 1.0e-4 {
let ratio = frame[0] / frame[1];
assert!(
(ratio - 2.0).abs() < 1.0e-3,
"channel balance moved: L/R = {ratio}"
);
}
}
}
fn loudest_dx7_voice() -> phosphor_dsp::dx7::Dx7Synth {
use phosphor_dsp::dx7;
let mut synth = dx7::Dx7Synth::new();
let (bank, patch) = dx7::voice_knobs(147);
synth.set_parameter(dx7::P_BANK, bank);
synth.set_parameter(dx7::P_PATCH, patch);
debug_assert_eq!(dx7::voice_name(147), "TIMPANI");
synth
}
#[test]
fn master_limiter_bounds_four_loud_instrument_tracks() {
let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
for id in 0..4 {
let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
handle.config.set_volume(1.0);
let synth = loudest_dx7_voice();
tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
tx.send(MixerCommand::SetInstrument {
track_id: id,
instrument: Box::new(synth),
}).unwrap();
}
transport.play();
let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
.iter()
.map(|¬e| make_note_on(note, 127))
.collect();
let mut output = vec![0.0f32; 512];
let mut peak = 0.0f32;
for block in 0..200 {
output.fill(0.0);
if block == 0 {
mixer.process(&mut output, &chord, &transport);
} else {
mixer.process(&mut output, &[], &transport);
}
for (i, &s) in output.iter().enumerate() {
assert!(s.is_finite(), "block {block} sample {i} is {s}");
assert!(s.abs() <= 1.0, "block {block} sample {i} left the mixer at {s}");
peak = peak.max(s.abs());
}
}
assert!(peak > 0.5, "four loud tracks should be loud, peak={peak}");
}
#[test]
fn limiter_idle_for_the_worst_single_track() {
let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
handle.config.set_volume(1.0);
let synth = loudest_dx7_voice();
tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
tx.send(MixerCommand::SetInstrument { track_id: 0, instrument: Box::new(synth) }).unwrap();
transport.play();
let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
.iter()
.map(|¬e| make_note_on(note, 127))
.collect();
let mut output = vec![0.0f32; 512];
let mut peak = 0.0f32;
for block in 0..200 {
output.fill(0.0);
if block == 0 {
mixer.process(&mut output, &chord, &transport);
} else {
mixer.process(&mut output, &[], &transport);
}
peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0f32, f32::max));
assert_eq!(
mixer.limiter.gain, 1.0,
"limiter engaged at block {block}, peak {peak}"
);
}
assert!(peak > 0.3, "expected a loud chord, peak={peak}");
}
fn worst_track_through_the_mixer(volume: f32) -> (f32, f32) {
let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
handle.config.set_volume(volume);
let synth = loudest_dx7_voice();
tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle }).unwrap();
tx.send(MixerCommand::SetInstrument { track_id: 0, instrument: Box::new(synth) }).unwrap();
transport.play();
let chord: Vec<MidiMessage> = [36u8, 43, 48, 55, 60, 64, 67, 72]
.iter()
.map(|¬e| make_note_on(note, 127))
.collect();
let mut output = vec![0.0f32; 512];
let mut peak = 0.0f32;
let mut min_gain = 1.0f32;
for block in 0..200 {
output.fill(0.0);
if block == 0 {
mixer.process(&mut output, &chord, &transport);
} else {
mixer.process(&mut output, &[], &transport);
}
for &s in output.iter() {
assert!(s.is_finite(), "block {block}: non-finite sample");
assert!(s.abs() <= 1.0, "block {block}: sample left the mixer at {s}");
peak = peak.max(s.abs());
}
min_gain = min_gain.min(mixer.limiter.gain);
}
(peak, min_gain)
}
#[test]
fn fader_below_unity_never_engages_the_limiter() {
for volume in [
0.25,
TrackConfig::DEFAULT_VOLUME,
TrackConfig::UNITY_VOLUME,
] {
let (peak, min_gain) = worst_track_through_the_mixer(volume);
assert_eq!(
min_gain, 1.0,
"limiter reduced by {:.2} dB at fader {volume} (peak {peak:.4})",
20.0 * min_gain.log10()
);
}
}
#[test]
fn fader_makeup_gain_is_bounded_not_wasted() {
let (unity_peak, _) = worst_track_through_the_mixer(TrackConfig::UNITY_VOLUME);
let (max_peak, min_gain) = worst_track_through_the_mixer(TrackConfig::MAX_VOLUME);
assert!(
max_peak <= LIMITER_CEILING,
"fader at maximum let {max_peak:.4} through, above the ceiling"
);
assert!(
max_peak >= unity_peak,
"turning the fader up made the track quieter: {unity_peak:.4} -> {max_peak:.4}"
);
let reduction_db = -20.0 * min_gain.log10();
let boost_db = 20.0 * (TrackConfig::MAX_VOLUME / TrackConfig::UNITY_VOLUME).log10();
assert!(
reduction_db <= boost_db,
"limiter took {reduction_db:.2} dB off a {boost_db:.2} dB boost"
);
}
#[test]
fn metronome_click_sits_with_the_music() {
use phosphor_dsp::dx7;
fn render(with_track: bool, metronome: bool) -> f32 {
let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
let chord: Vec<MidiMessage> = if with_track {
let handle = Arc::new(TrackHandle::new(0, TrackKind::Instrument));
handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
tx.send(MixerCommand::AddTrack {
kind: TrackKind::Instrument,
handle,
})
.unwrap();
tx.send(MixerCommand::SetInstrument {
track_id: 0,
instrument: Box::new(dx7::Dx7Synth::new()),
})
.unwrap();
[60u8, 64, 67].iter().map(|&n| make_note_on(n, 100)).collect()
} else {
Vec::new()
};
if metronome {
transport.toggle_metronome();
}
transport.play();
let mut output = vec![0.0f32; 512];
let mut peak = 0.0f32;
for block in 0..200 {
output.fill(0.0);
if block == 0 {
mixer.process(&mut output, &chord, &transport);
} else {
mixer.process(&mut output, &[], &transport);
}
peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0f32, f32::max));
transport.advance(256, 44_100);
}
peak
}
let music = render(true, false);
let click = render(false, true);
assert!(music > 0.0 && click > 0.0, "music {music}, click {click}");
let relative_db = 20.0 * (click / music).log10();
assert!(
(-12.0..=0.0).contains(&relative_db),
"the click is {relative_db:.1} dB against a triad (click {click:.4}, \
music {music:.4}); it has to be audible over the music without \
being the loudest thing in the mix"
);
}
#[test]
fn fader_scales_the_track() {
let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
let handle = add_fixed_track(&tx, 0, 0.25);
transport.play();
let mut output = vec![0.0f32; 512];
for (volume, expected) in [(0.0f32, 0.0f32), (0.5, 0.125), (1.0, 0.25), (2.0, 0.5)] {
handle.config.set_volume(volume);
output.fill(0.0);
mixer.process(&mut output, &[], &transport);
let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
assert!(
(peak - expected).abs() < 1.0e-6,
"fader at {volume} gave {peak}, expected {expected}"
);
}
}
fn refused(asked: u32, sample_rate: u32, max_buffer_frames: u32) -> StreamFormat {
StreamFormat {
sample_rate,
buffer_size: Some(64),
max_buffer_frames,
channels: 2,
sample_rate_request: Requested::Refused(asked),
buffer_size_request: Requested::Granted,
}
}
#[test]
fn the_mixer_runs_at_the_rate_the_device_granted() {
let requested = crate::EngineConfig { buffer_size: 64, sample_rate: 44100 };
let format = refused(44100, 48000, 4096);
let effective = crate::EngineConfig::from(format);
let (_tx, rx) = mixer_command_channel();
let (clip_tx, _clip_rx) = clip_snapshot_channel();
let mixer = Mixer::new(
rx,
Arc::new(VuLevels::new()),
clip_tx,
effective.sample_rate,
format.max_buffer_frames as usize,
);
assert_eq!(mixer.sample_rate, 48000, "mixer must adopt the device's rate");
assert_ne!(
mixer.sample_rate, requested.sample_rate,
"the request was 44100 and the device said 48000; taking the \
request here is the 8.84%-sharp bug"
);
assert_eq!(mixer.max_buffer_size, 4096);
}
#[test]
fn a_device_that_agrees_leaves_the_request_alone() {
let requested = crate::EngineConfig { buffer_size: 64, sample_rate: 44100 };
let format = StreamFormat {
sample_rate: 44100,
buffer_size: Some(64),
max_buffer_frames: 4096,
channels: 2,
sample_rate_request: Requested::Granted,
buffer_size_request: Requested::Granted,
};
assert_eq!(crate::EngineConfig::from(format), requested);
}
#[test]
fn asking_for_nothing_builds_the_mixer_at_the_devices_rate() {
let format = StreamFormat {
sample_rate: 48000,
buffer_size: None,
max_buffer_frames: 4096,
channels: 2,
sample_rate_request: Requested::Unasked,
buffer_size_request: Requested::Unasked,
};
let effective = crate::EngineConfig::from(format);
let (_tx, rx) = mixer_command_channel();
let (clip_tx, _clip_rx) = clip_snapshot_channel();
let mixer = Mixer::new(
rx,
Arc::new(VuLevels::new()),
clip_tx,
effective.sample_rate,
format.max_buffer_frames as usize,
);
assert_eq!(mixer.sample_rate, 48000);
assert_eq!(mixer.max_buffer_size, 4096);
assert!(format.divergence_notice().is_none(), "following the device is not news");
}
#[test]
fn the_largest_block_the_device_promised_never_grows_a_buffer() {
let max_frames = 512usize;
let (tx, rx) = mixer_command_channel();
let (clip_tx, _clip_rx) = clip_snapshot_channel();
let mut mixer = Mixer::new(
rx,
Arc::new(VuLevels::new()),
clip_tx,
48000,
max_frames,
);
let transport = Arc::new(Transport::new(120.0));
let _handle = add_armed_synth(&tx, 0);
mixer.drain_commands();
let before = (
mixer.scratch_l.capacity(),
mixer.scratch_r.capacity(),
mixer.tracks[0].buf_l.capacity(),
mixer.tracks[0].buf_r.capacity(),
);
transport.play();
let mut output = vec![0.0f32; max_frames * 2];
mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
let after = (
mixer.scratch_l.capacity(),
mixer.scratch_r.capacity(),
mixer.tracks[0].buf_l.capacity(),
mixer.tracks[0].buf_r.capacity(),
);
assert_eq!(
before, after,
"a block the size the device promised must fit the buffers as \
allocated; growing one means the audio thread called the allocator"
);
}
#[test]
fn a_steady_state_callback_does_not_allocate() {
let max_frames = 512usize;
let (tx, rx) = mixer_command_channel();
let (clip_tx, _clip_rx) = clip_snapshot_channel();
let mut mixer = Mixer::new(rx, Arc::new(VuLevels::new()), clip_tx, 48000, max_frames);
let transport = Arc::new(Transport::new(120.0));
let _handle = add_armed_synth(&tx, 0);
mixer.drain_commands();
transport.play();
let mut output = vec![0.0f32; max_frames * 2];
mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
let allocations = crate::alloc_count::allocations_during(|| {
for _ in 0..8 {
mixer.process(&mut output, &[], &transport);
}
});
assert_eq!(allocations, 0, "Mixer::process reached the allocator");
}
use crate::pattern::{ChainEntry, Lane, PatternEvent, Rate, Step};
fn bare_mixer(
sample_rate: u32,
max_frames: usize,
) -> (Mixer, Sender<MixerCommand>, Arc<Transport>) {
let (tx, rx) = mixer_command_channel();
let (clip_tx, _clip_rx) = clip_snapshot_channel();
let mixer = Mixer::new(rx, Arc::new(VuLevels::new()), clip_tx, sample_rate, max_frames);
(mixer, tx, Arc::new(Transport::new(120.0)))
}
fn kick_pattern(on: &[usize]) -> PatternBlock {
let mut block = PatternBlock::empty();
block.playing = true;
block.lanes[0] = Lane::drum(36);
for &index in on {
block.lanes[0].steps[index].on = true;
}
block
}
fn add_track(tx: &Sender<MixerCommand>, id: usize) -> Arc<TrackHandle> {
let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
tx.send(MixerCommand::AddTrack {
kind: TrackKind::Instrument,
handle: handle.clone(),
})
.unwrap();
handle
}
fn apply_all(mixer: &mut Mixer) {
while !mixer.command_rx.is_empty() {
mixer.drain_commands();
}
}
fn note_ons(track: &AudioTrack) -> impl Iterator<Item = &MidiEvent> {
track.plugin_events.iter().filter(|e| e.status == 0x90 && e.data2 > 0)
}
#[test]
fn a_pattern_step_and_a_clip_note_land_on_the_same_sample() {
for sample_rate in [44_100u32, 48_000, 96_000] {
for frames in [64usize, 256, 470] {
let (mut mixer, tx, transport) = bare_mixer(sample_rate, 512);
let _clip_track = add_track(&tx, 0);
tx.send(MixerCommand::CreateClip {
track_id: 0,
start_tick: 0,
length_ticks: 3840,
})
.unwrap();
tx.send(MixerCommand::UpdateClip {
track_id: 0,
clip_index: 0,
events: vec![ClipEvent { tick: 960, status: 0x90, data1: 60, data2: 100 }],
})
.unwrap();
let _seq_track = add_track(&tx, 1);
tx.send(MixerCommand::SetPattern {
track_id: 1,
slot: 0,
block: kick_pattern(&[4]),
})
.unwrap();
apply_all(&mut mixer);
transport.play();
let mut output = vec![0.0f32; frames * 2];
let mut landed = None;
while transport.position_ticks() < 1_200 {
mixer.process(&mut output, &[], &transport);
let clip_note = note_ons(&mixer.tracks[0]).find(|e| e.data1 == 60);
let step_note = note_ons(&mixer.tracks[1]).find(|e| e.data1 == 36);
match (clip_note, step_note) {
(Some(c), Some(s)) => {
landed = Some((c.sample_offset, s.sample_offset));
break;
}
(None, None) => {}
(clip, step) => panic!(
"at {sample_rate} Hz / {frames} frames only one of them fired: \
clip={clip:?} step={step:?}"
),
}
transport.advance(frames as u32, sample_rate);
}
let (clip_at, step_at) =
landed.unwrap_or_else(|| panic!("nothing fired at {sample_rate}/{frames}"));
assert_eq!(
clip_at, step_at,
"at {sample_rate} Hz / {frames} frames the clip note landed on sample \
{clip_at} and the step on {step_at}"
);
}
}
}
#[test]
fn step_timing_is_the_same_at_every_sample_rate() {
let frames = 256usize;
for sample_rate in [44_100u32, 48_000, 96_000] {
let (mut mixer, tx, transport) = bare_mixer(sample_rate, 512);
let _track = add_track(&tx, 0);
tx.send(MixerCommand::SetPattern {
track_id: 0,
slot: 0,
block: kick_pattern(&[0, 4, 8, 12]),
})
.unwrap();
apply_all(&mut mixer);
transport.play();
let mut output = vec![0.0f32; frames * 2];
let mut seconds = Vec::new();
let mut block = 0usize;
while seconds.len() < 4 && transport.position_ticks() < 3_600 {
mixer.process(&mut output, &[], &transport);
for event in note_ons(&mixer.tracks[0]) {
let sample = block * frames + event.sample_offset as usize;
seconds.push(sample as f64 / f64::from(sample_rate));
}
transport.advance(frames as u32, sample_rate);
block += 1;
}
assert_eq!(seconds.len(), 4, "at {sample_rate} Hz");
for (index, at) in seconds.iter().enumerate() {
let expected = index as f64 * 0.5;
assert!(
(at - expected).abs() < 0.002,
"at {sample_rate} Hz step {index} landed at {at:.4}s, expected {expected:.4}s"
);
}
}
}
#[test]
fn a_loop_wrap_neither_drops_nor_doubles_the_first_step() {
let frames = 256usize;
let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
let _track = add_track(&tx, 0);
let all_sixteen: Vec<usize> = (0..16).collect();
tx.send(MixerCommand::SetPattern {
track_id: 0,
slot: 0,
block: kick_pattern(&all_sixteen),
})
.unwrap();
apply_all(&mut mixer);
transport.set_loop_bars(1, 1);
transport.toggle_loop();
transport.play();
let mut output = vec![0.0f32; frames * 2];
let mut fired = 0usize;
let mut wraps = 0usize;
let mut last = transport.position_ticks();
for _ in 0..4_000 {
mixer.process(&mut output, &[], &transport);
fired += note_ons(&mixer.tracks[0]).count();
transport.advance(frames as u32, 44_100);
let now = transport.position_ticks();
if now < last {
wraps += 1;
if wraps == 4 {
break;
}
}
last = now;
}
assert_eq!(wraps, 4, "the transport did not loop");
assert_eq!(fired, 64, "four times round a 16-step pattern is 64 onsets");
}
#[test]
fn a_sequencer_track_plays_its_child_instrument() {
let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
let handle = add_track(&tx, 0);
handle.config.set_volume(1.0);
tx.send(MixerCommand::SetInstrument {
track_id: 0,
instrument: Box::new(PhosphorSynth::new()),
})
.unwrap();
let mut block = PatternBlock::empty();
block.playing = true;
block.lanes[0].steps[0].on = true;
block.lanes[0].steps[0].gate = 200;
tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block }).unwrap();
apply_all(&mut mixer);
transport.play();
let mut output = vec![0.0f32; 512 * 2];
let mut peak = 0.0f32;
for _ in 0..8 {
mixer.process(&mut output, &[], &transport);
peak = peak.max(output.iter().map(|s| s.abs()).fold(0.0, f32::max));
transport.advance(512, 44_100);
}
assert!(peak > 0.001, "the child instrument never sounded, peak={peak}");
}
#[test]
fn stopping_the_transport_ends_every_pattern_note() {
let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
let _track = add_track(&tx, 0);
let mut block = kick_pattern(&[0]);
block.lanes[0].steps[0].gate = Step::TIE;
tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block }).unwrap();
apply_all(&mut mixer);
transport.play();
let mut output = vec![0.0f32; 256 * 2];
mixer.process(&mut output, &[], &transport);
assert_eq!(note_ons(&mixer.tracks[0]).count(), 1);
transport.advance(256, 44_100);
transport.pause();
mixer.process(&mut output, &[], &transport);
let offs: Vec<u8> = mixer.tracks[0]
.plugin_events
.iter()
.filter(|e| e.status == 0x80)
.map(|e| e.data1)
.collect();
assert_eq!(offs, vec![36], "the tied note was left sounding");
mixer.process(&mut output, &[], &transport);
assert!(mixer.tracks[0].plugin_events.is_empty());
}
#[test]
fn a_panic_leaves_the_sequencer_holding_nothing() {
let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
let _track = add_track(&tx, 0);
let mut block = kick_pattern(&[0]);
block.lanes[0].steps[0].gate = Step::TIE;
tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block }).unwrap();
apply_all(&mut mixer);
transport.play();
let mut output = vec![0.0f32; 256 * 2];
mixer.process(&mut output, &[], &transport);
assert!(mixer.tracks[0].pattern.as_ref().unwrap().held_notes() > 0);
mixer.reset_all();
assert_eq!(mixer.tracks[0].pattern.as_ref().unwrap().held_notes(), 0);
}
#[test]
fn a_bounced_pattern_renders_identically_to_the_live_one() {
const SWING: u8 = 62;
let mut block = PatternBlock::empty();
block.playing = true;
block.swing = SWING;
block.rate = Rate::Sixteenth;
for (index, (key, chord, gate)) in [
(0usize, 0u8, 5u8, 50u8),
(3, 3, 6, 90),
(5, 7, 1, 25),
(9, 5, 14, 75),
(11, 10, 12, 40),
(14, 0, 15, 60),
]
.iter()
.map(|(i, k, c, g)| (*i, (*k, *c, *g)))
{
let step = &mut block.lanes[0].steps[index];
step.on = true;
step.key = key;
step.chord = chord;
step.gate = gate;
step.accent = index % 2 == 1;
}
let cycle = block.length_ticks();
let blocks = 24;
let live = {
let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
let handle = add_track(&tx, 0);
handle.config.set_volume(1.0);
tx.send(MixerCommand::SetInstrument {
track_id: 0,
instrument: Box::new(PhosphorSynth::new()),
})
.unwrap();
tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block }).unwrap();
apply_all(&mut mixer);
transport.play();
let mut rendered = Vec::new();
let mut output = vec![0.0f32; 512 * 2];
for _ in 0..blocks {
mixer.process(&mut output, &[], &transport);
rendered.extend_from_slice(&output);
transport.advance(512, 44_100);
}
rendered
};
let bounced = {
let mut events = Vec::new();
crate::pattern::compile_cycle(&block, 0, &mut events);
assert!(!events.is_empty());
let clip_events: Vec<ClipEvent> = events
.iter()
.map(|e: &PatternEvent| ClipEvent {
tick: e.tick,
status: e.status,
data1: e.data1,
data2: e.data2,
})
.collect();
let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
let handle = add_track(&tx, 0);
handle.config.set_volume(1.0);
tx.send(MixerCommand::SetInstrument {
track_id: 0,
instrument: Box::new(PhosphorSynth::new()),
})
.unwrap();
tx.send(MixerCommand::CreateClip {
track_id: 0,
start_tick: 0,
length_ticks: cycle,
})
.unwrap();
tx.send(MixerCommand::UpdateClip {
track_id: 0,
clip_index: 0,
events: clip_events,
})
.unwrap();
apply_all(&mut mixer);
transport.play();
let mut rendered = Vec::new();
let mut output = vec![0.0f32; 512 * 2];
for _ in 0..blocks {
mixer.process(&mut output, &[], &transport);
rendered.extend_from_slice(&output);
transport.advance(512, 44_100);
}
rendered
};
assert_eq!(live.len(), bounced.len());
let peak = live.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
assert!(peak > 0.001, "the live render was silent, so this proves nothing");
for (i, (a, b)) in live.iter().zip(&bounced).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"sample {i} differs: live {a} bounced {b} at {SWING}% swing"
);
}
}
#[test]
fn pattern_playback_does_not_allocate() {
let (mut mixer, tx, transport) = bare_mixer(48_000, 512);
let _track = add_track(&tx, 0);
tx.send(MixerCommand::SetInstrument {
track_id: 0,
instrument: Box::new(PhosphorSynth::new()),
})
.unwrap();
let mut chords = PatternBlock::empty();
chords.playing = true;
chords.mode = crate::pattern::Mode::Aeolian;
for index in 0..16 {
let step = &mut chords.lanes[0].steps[index];
step.on = true;
step.chord = 4; step.voicing = 1 | Step::ROOT_BELOW;
step.key = (index as u8 * 2) % 12;
}
let drums = kick_pattern(&[0, 4, 8, 12]);
tx.send(MixerCommand::SetPattern { track_id: 0, slot: 1, block: drums }).unwrap();
tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block: chords }).unwrap();
tx.send(MixerCommand::CreateClip { track_id: 0, start_tick: 0, length_ticks: 3840 })
.unwrap();
tx.send(MixerCommand::UpdateClip {
track_id: 0,
clip_index: 0,
events: (0..16)
.flat_map(|i| {
[
ClipEvent { tick: i * 240, status: 0x90, data1: 40, data2: 90 },
ClipEvent { tick: i * 240 + 120, status: 0x80, data1: 40, data2: 0 },
]
})
.collect(),
})
.unwrap();
apply_all(&mut mixer);
transport.play();
let mut output = vec![0.0f32; 512 * 2];
for _ in 0..2 {
mixer.process(&mut output, &[], &transport);
transport.advance(512, 48_000);
}
let mut queued = chords;
queued.pending_slot = Some(1);
let mut chained = chords;
chained.chain[0] = ChainEntry { slot: 0, repeats: 1 };
chained.chain[1] = ChainEntry { slot: 1, repeats: 1 };
chained.chain_len = 2;
let allocations = crate::alloc_count::allocations_during(|| {
for block in 0..400 {
if block == 20 {
tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block: queued })
.unwrap();
}
if block == 120 {
tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block: chained })
.unwrap();
}
mixer.process(&mut output, &[], &transport);
transport.advance(512, 48_000);
}
transport.pause();
mixer.process(&mut output, &[], &transport);
});
assert_eq!(allocations, 0, "the sequencer reached the allocator");
}
#[test]
fn a_command_is_as_wide_as_a_pattern() {
assert_eq!(
std::mem::size_of::<MixerCommand>(),
crate::pattern::PatternBlock::SIZE + 11
);
}
#[test]
fn the_track_handle_reports_where_the_pattern_is() {
let (mut mixer, tx, transport) = bare_mixer(44_100, 512);
let handle = add_track(&tx, 0);
let all_sixteen: Vec<usize> = (0..16).collect();
let block = kick_pattern(&all_sixteen);
tx.send(MixerCommand::SetPattern { track_id: 0, slot: 1, block }).unwrap();
let mut queued = block;
queued.pending_slot = Some(1);
tx.send(MixerCommand::SetPattern { track_id: 0, slot: 0, block: queued }).unwrap();
apply_all(&mut mixer);
transport.set_position(240);
transport.play();
let mut output = vec![0.0f32; 256 * 2];
mixer.process(&mut output, &[], &transport);
assert_eq!(handle.pattern.live_slot(), 0);
assert_eq!(handle.pattern.queued_slot(), Some(1));
assert_eq!(handle.pattern.step(), 1);
assert!(handle.pattern.is_running());
transport.set_position(1920);
mixer.process(&mut output, &[], &transport);
assert_eq!(handle.pattern.step(), 8);
assert_eq!(handle.pattern.live_slot(), 0);
transport.set_position(3840);
mixer.process(&mut output, &[], &transport);
assert_eq!(handle.pattern.live_slot(), 1);
assert_eq!(handle.pattern.queued_slot(), None);
}
#[test]
fn a_short_callback_does_not_allocate_either() {
let max_frames = 512usize;
let (tx, rx) = mixer_command_channel();
let (clip_tx, _clip_rx) = clip_snapshot_channel();
let mut mixer = Mixer::new(rx, Arc::new(VuLevels::new()), clip_tx, 48000, max_frames);
let transport = Arc::new(Transport::new(120.0));
let _handle = add_armed_synth(&tx, 0);
mixer.drain_commands();
transport.play();
let mut output = vec![0.0f32; 64 * 2];
mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
let allocations = crate::alloc_count::allocations_during(|| {
for _ in 0..8 {
mixer.process(&mut output, &[], &transport);
}
});
assert_eq!(allocations, 0, "Mixer::process reached the allocator");
}
}