1use std::sync::Arc;
8
9use crossbeam_channel::{Receiver, Sender};
10use phosphor_midi::message::MidiMessage;
11use phosphor_plugin::{MidiEvent, Plugin};
12
13use crate::clip::{ClipEvent, ClipSnapshot, MidiClip, RecordBuffer};
14use crate::engine::VuLevels;
15use crate::metronome::Metronome;
16use crate::project::{TrackHandle, TrackKind};
17use crate::transport::Transport;
18
19pub enum MixerCommand {
22 AddTrack {
23 kind: TrackKind,
24 handle: Arc<TrackHandle>,
25 },
26 SetInstrument {
27 track_id: usize,
28 instrument: Box<dyn Plugin + Send>,
29 },
30 RemoveTrack {
31 track_id: usize,
32 },
33 SetParameter {
34 track_id: usize,
35 param_index: usize,
36 value: f32,
37 },
38 CreateClip {
40 track_id: usize,
41 start_tick: i64,
42 length_ticks: i64,
43 },
44 UpdateClip {
46 track_id: usize,
47 clip_index: usize,
48 events: Vec<ClipEvent>,
49 },
50 UpdateClipPosition {
52 track_id: usize,
53 clip_index: usize,
54 start_tick: i64,
55 length_ticks: i64,
56 },
57 RemoveClip {
59 track_id: usize,
60 clip_index: usize,
61 },
62}
63
64pub struct AudioTrack {
67 pub id: usize,
68 pub kind: TrackKind,
69 pub handle: Arc<TrackHandle>,
70 pub instrument: Option<Box<dyn Plugin>>,
71 pub clips: Vec<MidiClip>,
73 record_buf: RecordBuffer,
75 was_recording: bool,
77 last_record_tick: i64,
79 last_playback_tick: i64,
81 buf_l: Vec<f32>,
82 buf_r: Vec<f32>,
83 plugin_events: Vec<MidiEvent>,
84}
85
86impl AudioTrack {
87 pub fn new(handle: Arc<TrackHandle>, max_buffer_size: usize) -> Self {
88 Self {
89 id: handle.id,
90 kind: handle.kind,
91 handle,
92 instrument: None,
93 clips: Vec::new(),
94 record_buf: RecordBuffer::new(),
95 was_recording: false,
96 last_record_tick: -1,
97 last_playback_tick: -1,
98 buf_l: vec![0.0; max_buffer_size],
99 buf_r: vec![0.0; max_buffer_size],
100 plugin_events: Vec::with_capacity(256),
101 }
102 }
103}
104
105pub struct Mixer {
108 tracks: Vec<AudioTrack>,
109 master_vu: Arc<VuLevels>,
110 command_rx: Receiver<MixerCommand>,
111 clip_tx: Sender<ClipSnapshot>,
112 metronome: Metronome,
113 sample_rate: u32,
114 max_buffer_size: usize,
115 scratch_l: Vec<f32>,
117 scratch_r: Vec<f32>,
118 live_events: Vec<MidiEvent>,
120}
121
122impl Mixer {
123 pub fn new(
124 command_rx: Receiver<MixerCommand>,
125 master_vu: Arc<VuLevels>,
126 clip_tx: Sender<ClipSnapshot>,
127 sample_rate: u32,
128 max_buffer_size: usize,
129 ) -> Self {
130 Self {
131 tracks: Vec::new(),
132 master_vu,
133 command_rx,
134 clip_tx,
135 metronome: Metronome::new(sample_rate as f64),
136 sample_rate,
137 max_buffer_size,
138 scratch_l: vec![0.0; max_buffer_size],
139 scratch_r: vec![0.0; max_buffer_size],
140 live_events: Vec::with_capacity(256),
141 }
142 }
143
144 pub fn process(&mut self, output: &mut [f32], midi_messages: &[MidiMessage], transport: &Transport) {
146 self.drain_commands();
147
148 let num_frames = output.len() / 2;
149 let playing = transport.is_playing();
150 let recording = transport.is_recording();
151 let looping = transport.is_looping();
152 let current_tick = transport.position_ticks();
153 let bpm = transport.tempo_bpm();
154 let ticks_per_sample = (bpm * Transport::PPQ as f64) / (60.0 * self.sample_rate as f64);
155 let buffer_ticks = (num_frames as f64 * ticks_per_sample) as i64;
156 let loop_end = transport.loop_end();
157
158 self.live_events.clear();
160 for msg in midi_messages {
161 if let Some(ev) = midi_to_plugin_event(msg) {
162 self.live_events.push(ev);
163 }
164 }
165
166 let any_solo = self.tracks.iter().any(|t| t.handle.config.is_soloed());
167
168 let mut master_l = std::mem::take(&mut self.scratch_l);
171 let mut master_r = std::mem::take(&mut self.scratch_r);
172 let live_events = std::mem::take(&mut self.live_events);
173 if master_l.len() < num_frames {
174 master_l.resize(num_frames, 0.0);
175 master_r.resize(num_frames, 0.0);
176 }
177 master_l[..num_frames].fill(0.0);
178 master_r[..num_frames].fill(0.0);
179
180 let clip_tx = &self.clip_tx;
181
182 for track in &mut self.tracks {
183 if track.buf_l.len() < num_frames {
184 track.buf_l.resize(num_frames, 0.0);
185 track.buf_r.resize(num_frames, 0.0);
186 }
187 track.buf_l[..num_frames].fill(0.0);
188 track.buf_r[..num_frames].fill(0.0);
189 track.plugin_events.clear();
190
191 let is_midi_active = track.kind == TrackKind::Instrument
192 && track.handle.config.is_midi_active();
193 let is_armed = track.handle.config.is_armed();
194 let should_record = playing && recording && is_armed && is_midi_active;
195
196 if should_record && !track.was_recording {
198 let rec_start = if looping { transport.loop_start() } else { current_tick };
201 track.record_buf.start(rec_start);
202 tracing::debug!("rec start track={} tick={}", track.id, current_tick);
203 }
204
205 if should_record && track.was_recording && looping
207 && track.record_buf.is_active() && track.last_record_tick >= 0
208 && current_tick < track.last_record_tick
209 {
210 commit_recording(track, loop_end, clip_tx);
211 track.record_buf.start(transport.loop_start());
214 }
215 if should_record {
216 track.last_record_tick = current_tick;
217 }
218
219 if !should_record && track.was_recording {
221 commit_recording(track, current_tick, clip_tx);
222 }
223 track.was_recording = should_record;
224
225 if is_midi_active {
227 for ev in &live_events {
228 track.plugin_events.push(*ev);
229 if should_record {
230 let event_tick = current_tick
231 + (ev.sample_offset as f64 * ticks_per_sample) as i64;
232 track.record_buf.record(event_tick, ev.status, ev.data1, ev.data2);
233 }
234 }
235 }
236
237 if playing && !track.clips.is_empty() {
239 let from = current_tick;
240 let to = current_tick + buffer_ticks;
241
242 let just_wrapped = looping && track.last_playback_tick >= 0
245 && current_tick < track.last_playback_tick;
246 track.last_playback_tick = current_tick;
247
248 if just_wrapped {
249 let wrap_start = transport.loop_start();
251 for clip in &track.clips {
252 for (tick_offset, event) in clip.events_in_range(wrap_start, to) {
253 let sample_offset = (tick_offset as f64 / ticks_per_sample) as u32;
254 track.plugin_events.push(MidiEvent {
255 sample_offset: sample_offset.min(num_frames as u32 - 1),
256 status: event.status,
257 data1: event.data1,
258 data2: event.data2,
259 });
260 }
261 }
262 } else {
263 for clip in &track.clips {
264 for (tick_offset, event) in clip.events_in_range(from, to) {
265 let sample_offset = (tick_offset as f64 / ticks_per_sample) as u32;
266 track.plugin_events.push(MidiEvent {
267 sample_offset: sample_offset.min(num_frames as u32 - 1),
268 status: event.status,
269 data1: event.data1,
270 data2: event.data2,
271 });
272 }
273 }
274 }
275 track.plugin_events.sort_by_key(|e| e.sample_offset);
276 }
277
278 if playing {
280 track.last_record_tick = current_tick;
281 }
282
283 if let Some(ref mut instrument) = track.instrument {
285 let out_l = &mut track.buf_l[..num_frames];
286 let out_r = &mut track.buf_r[..num_frames];
287 let mut out_slices: [&mut [f32]; 2] = [out_l, out_r];
288 instrument.process(&[], &mut out_slices, &track.plugin_events);
289 }
290
291 let muted = track.handle.config.is_muted();
293 let soloed = track.handle.config.is_soloed();
294 let audible = !muted && (!any_solo || soloed);
295 let volume = track.handle.config.get_volume();
296
297 let mut peak_l = 0.0f32;
298 let mut peak_r = 0.0f32;
299 for i in 0..num_frames {
300 peak_l = peak_l.max(track.buf_l[i].abs());
301 peak_r = peak_r.max(track.buf_r[i].abs());
302 }
303
304 let (old_l, old_r) = track.handle.vu.get();
305 let decay = 0.85f32;
306 track.handle.vu.set(
307 if peak_l > old_l { peak_l } else { old_l * decay },
308 if peak_r > old_r { peak_r } else { old_r * decay },
309 );
310
311 if audible {
312 for i in 0..num_frames {
313 master_l[i] += track.buf_l[i] * volume;
314 master_r[i] += track.buf_r[i] * volume;
315 }
316 }
317 }
318
319 for i in 0..num_frames {
321 output[i * 2] = master_l[i];
322 output[i * 2 + 1] = master_r[i];
323 }
324
325 self.scratch_l = master_l;
327 self.scratch_r = master_r;
328 self.live_events = live_events;
329
330 self.metronome.process(output, transport);
332
333 let mut mp_l = 0.0f32;
335 let mut mp_r = 0.0f32;
336 for i in 0..num_frames {
337 mp_l = mp_l.max(output[i * 2].abs());
338 mp_r = mp_r.max(output[i * 2 + 1].abs());
339 }
340
341 let (old_l, old_r) = self.master_vu.get();
342 let decay = 0.85f32;
343 self.master_vu.set(
344 if mp_l > old_l { mp_l } else { old_l * decay },
345 if mp_r > old_r { mp_r } else { old_r * decay },
346 );
347 }
348
349 pub fn reset_all(&mut self) {
350 let clip_tx = &self.clip_tx;
351 for track in &mut self.tracks {
352 if let Some(ref mut inst) = track.instrument {
353 inst.reset();
354 }
355 track.handle.vu.set(0.0, 0.0);
356 if track.record_buf.is_active() && track.was_recording {
358 let end_tick = track.last_record_tick.max(0);
359 commit_recording(track, end_tick, clip_tx);
360 } else if track.record_buf.is_active() {
361 track.record_buf.discard();
362 }
363 track.was_recording = false;
364 track.last_playback_tick = -1;
365 }
366 self.metronome.reset();
367 }
368
369 fn drain_commands(&mut self) {
370 while let Ok(cmd) = self.command_rx.try_recv() {
371 match cmd {
372 MixerCommand::AddTrack { kind: _, handle } => {
373 let track = AudioTrack::new(handle, self.max_buffer_size);
374 self.tracks.push(track);
375 }
376 MixerCommand::SetInstrument { track_id, mut instrument } => {
377 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
378 instrument.init(self.sample_rate as f64, self.max_buffer_size);
379 track.instrument = Some(instrument);
380 }
381 }
382 MixerCommand::RemoveTrack { track_id } => {
383 self.tracks.retain(|t| t.id != track_id);
384 }
385 MixerCommand::SetParameter { track_id, param_index, value } => {
386 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
387 if let Some(ref mut inst) = track.instrument {
388 inst.set_parameter(param_index, value);
389 }
390 }
391 }
392 MixerCommand::CreateClip { track_id, start_tick, length_ticks } => {
393 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
394 track.clips.push(MidiClip::new(start_tick, length_ticks, Vec::new()));
395 }
396 }
397 MixerCommand::UpdateClip { track_id, clip_index, events } => {
398 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
399 if let Some(clip) = track.clips.get_mut(clip_index) {
400 clip.events = events;
401 clip.events.sort_by_key(|e| e.tick);
402 }
403 }
404 }
405 MixerCommand::UpdateClipPosition { track_id, clip_index, start_tick, length_ticks } => {
406 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
407 if let Some(clip) = track.clips.get_mut(clip_index) {
408 clip.start_tick = start_tick;
409 clip.length_ticks = length_ticks;
410 }
411 }
412 }
413 MixerCommand::RemoveClip { track_id, clip_index } => {
414 if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
415 if clip_index < track.clips.len() {
416 track.clips.remove(clip_index);
417 }
418 }
419 }
420 }
421 }
422 }
423}
424
425fn commit_recording(track: &mut AudioTrack, end_tick: i64, clip_tx: &Sender<ClipSnapshot>) {
427 if let Some(clip) = track.record_buf.commit(end_tick) {
428 let idx = track.clips.len();
429 tracing::debug!(
430 "rec commit track={}: {} events, ticks {}..{}",
431 track.id, clip.events.len(), clip.start_tick, clip.end_tick()
432 );
433 let snapshot = ClipSnapshot::from_clip(track.id, idx, &clip);
434 track.clips.push(clip);
435 let _ = clip_tx.send(snapshot);
436 }
437}
438
439fn midi_to_plugin_event(msg: &MidiMessage) -> Option<MidiEvent> {
440 use phosphor_midi::message::MidiMessageType;
441 match msg.message_type {
442 MidiMessageType::NoteOn { .. }
443 | MidiMessageType::NoteOff { .. }
444 | MidiMessageType::ControlChange { .. }
445 | MidiMessageType::PitchBend { .. } => Some(MidiEvent {
446 sample_offset: 0,
447 status: msg.raw[0],
448 data1: msg.raw[1],
449 data2: msg.raw[2],
450 }),
451 _ => None,
452 }
453}
454
455pub fn mixer_command_channel() -> (Sender<MixerCommand>, Receiver<MixerCommand>) {
456 crossbeam_channel::unbounded()
457}
458
459pub fn clip_snapshot_channel() -> (Sender<ClipSnapshot>, Receiver<ClipSnapshot>) {
461 crossbeam_channel::unbounded()
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use phosphor_dsp::synth::PhosphorSynth;
468 use phosphor_midi::message::{MidiMessage, MidiMessageType};
469
470 fn make_note_on(note: u8, vel: u8) -> MidiMessage {
471 MidiMessage {
472 timestamp: Some(0),
473 message_type: MidiMessageType::NoteOn { channel: 0, note, velocity: vel },
474 raw: [0x90, note, vel],
475 len: 3,
476 }
477 }
478
479 fn make_note_off(note: u8) -> MidiMessage {
480 MidiMessage {
481 timestamp: Some(0),
482 message_type: MidiMessageType::NoteOff { channel: 0, note, velocity: 0 },
483 raw: [0x80, note, 0],
484 len: 3,
485 }
486 }
487
488 fn setup_mixer() -> (Mixer, Sender<MixerCommand>, Receiver<ClipSnapshot>, Arc<Transport>) {
489 let (tx, rx) = mixer_command_channel();
490 let (clip_tx, clip_rx) = clip_snapshot_channel();
491 let master_vu = Arc::new(VuLevels::new());
492 let transport = Arc::new(Transport::new(120.0));
493 let mixer = Mixer::new(rx, master_vu, clip_tx, 44100, 256);
494 (mixer, tx, clip_rx, transport)
495 }
496
497 fn add_armed_synth(tx: &Sender<MixerCommand>, id: usize) -> Arc<TrackHandle> {
498 let handle = Arc::new(TrackHandle::new(id, TrackKind::Instrument));
499 handle.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
500 handle.config.armed.store(true, std::sync::atomic::Ordering::Relaxed);
501 tx.send(MixerCommand::AddTrack { kind: TrackKind::Instrument, handle: handle.clone() }).unwrap();
502 tx.send(MixerCommand::SetInstrument { track_id: id, instrument: Box::new(PhosphorSynth::new()) }).unwrap();
503 handle
504 }
505
506 #[test]
507 fn mixer_empty_output() {
508 let (mut mixer, _tx, _clip_rx, transport) = setup_mixer();
509 let mut output = vec![0.0f32; 128];
510 mixer.process(&mut output, &[], &transport);
511 assert!(output.iter().all(|&s| s == 0.0));
512 }
513
514 #[test]
515 fn mixer_live_midi_produces_sound() {
516 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
517 let _handle = add_armed_synth(&tx, 0);
518 transport.play();
519
520 let midi = vec![make_note_on(60, 100)];
521 let mut output = vec![0.0f32; 512];
522 mixer.process(&mut output, &midi, &transport);
523
524 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
525 assert!(peak > 0.01, "Should produce sound, peak={peak}");
526 }
527
528 #[test]
529 fn mixer_records_midi_clip() {
530 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
531 let _handle = add_armed_synth(&tx, 0);
532 transport.play();
533 transport.toggle_record();
534
535 let midi = vec![make_note_on(60, 100)];
537 let mut output = vec![0.0f32; 512];
538 mixer.process(&mut output, &midi, &transport);
539
540 let midi = vec![make_note_off(60)];
542 mixer.process(&mut output, &midi, &transport);
543
544 transport.toggle_record();
546 mixer.process(&mut output, &[], &transport);
547
548 let snap = clip_rx.try_recv().expect("Should receive clip snapshot");
550 assert_eq!(snap.track_id, 0);
551 assert!(snap.event_count >= 2, "Should have note on + off, got {}", snap.event_count);
552 assert!(!snap.notes.is_empty(), "Should have parsed notes");
553 }
554
555 #[test]
556 fn mixer_plays_back_recorded_clip() {
557 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
558 let _handle = add_armed_synth(&tx, 0);
559 transport.play();
560 transport.toggle_record();
561
562 let midi = vec![make_note_on(60, 100)];
564 let mut output = vec![0.0f32; 512];
565 mixer.process(&mut output, &midi, &transport);
566
567 let midi = vec![make_note_off(60)];
568 mixer.process(&mut output, &midi, &transport);
569
570 transport.toggle_record();
572 mixer.process(&mut output, &[], &transport);
573
574 transport.stop();
576
577 transport.play();
579 output.fill(0.0);
580 mixer.process(&mut output, &[], &transport);
581
582 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
583 assert!(peak > 0.01, "Playback should produce sound, peak={peak}");
584 }
585
586 #[test]
587 fn mixer_mute_silences() {
588 let (mut mixer, tx, _clip_rx, transport) = setup_mixer();
589 let handle = add_armed_synth(&tx, 0);
590 handle.config.muted.store(true, std::sync::atomic::Ordering::Relaxed);
591 transport.play();
592
593 let midi = vec![make_note_on(60, 100)];
594 let mut output = vec![0.0f32; 512];
595 mixer.process(&mut output, &midi, &transport);
596
597 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
598 assert!(peak == 0.0, "Muted track should be silent, peak={peak}");
599 }
600
601 #[test]
602 fn mixer_no_record_when_not_armed() {
603 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
604 let handle = add_armed_synth(&tx, 0);
605 handle.config.armed.store(false, std::sync::atomic::Ordering::Relaxed);
606 transport.play();
607 transport.toggle_record();
608
609 let midi = vec![make_note_on(60, 100)];
610 let mut output = vec![0.0f32; 512];
611 mixer.process(&mut output, &midi, &transport);
612
613 transport.toggle_record();
614 mixer.process(&mut output, &[], &transport);
615
616 assert!(clip_rx.try_recv().is_err(), "Should not record when not armed");
617 }
618
619 #[test]
620 fn mixer_reset_commits_recording() {
621 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
622 let _handle = add_armed_synth(&tx, 0);
623 transport.play();
624 transport.toggle_record();
625
626 let midi = vec![make_note_on(60, 100)];
627 let mut output = vec![0.0f32; 512];
628 mixer.process(&mut output, &midi, &transport);
629
630 mixer.reset_all();
631
632 assert!(clip_rx.try_recv().is_ok(), "Reset should commit active recording");
634 }
635
636 #[test]
637 fn end_to_end_record_and_playback() {
638 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
641 let _handle = add_armed_synth(&tx, 0);
642 let sr = 44100u32;
643 let buf_frames = 256;
644 let buf_samples = buf_frames * 2; transport.toggle_record();
648 transport.play();
649
650 let mut output = vec![0.0f32; buf_samples];
652 for _ in 0..4 {
653 mixer.process(&mut output, &[], &transport);
654 transport.advance(buf_frames as u32, sr);
655 }
656
657 let midi = vec![make_note_on(60, 100)];
659 mixer.process(&mut output, &midi, &transport);
660 let peak_during = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
661 assert!(peak_during > 0.01, "Should hear note during recording (monitoring)");
662 transport.advance(buf_frames as u32, sr);
663
664 for _ in 0..8 {
666 output.fill(0.0);
667 mixer.process(&mut output, &[], &transport);
668 transport.advance(buf_frames as u32, sr);
669 }
670
671 let midi = vec![make_note_off(60)];
673 mixer.process(&mut output, &midi, &transport);
674 transport.advance(buf_frames as u32, sr);
675
676 for _ in 0..4 {
678 output.fill(0.0);
679 mixer.process(&mut output, &[], &transport);
680 transport.advance(buf_frames as u32, sr);
681 }
682
683 transport.toggle_record();
685 mixer.process(&mut output, &[], &transport);
686 transport.advance(buf_frames as u32, sr);
687
688 let snap = clip_rx.try_recv().expect("Should receive clip snapshot after stopping record");
690 assert!(snap.event_count >= 2, "Clip should have note on + off");
691 assert!(!snap.notes.is_empty(), "Clip should have parsed notes");
692
693 transport.stop();
695
696 transport.play();
698
699 for _ in 0..4 {
702 output.fill(0.0);
703 mixer.process(&mut output, &[], &transport);
704 transport.advance(buf_frames as u32, sr);
705 }
706
707 output.fill(0.0);
709 mixer.process(&mut output, &[], &transport);
710 let peak_playback = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
711 assert!(peak_playback > 0.001, "Playback should produce sound at the recorded position, peak={peak_playback}");
712 }
713
714 #[test]
715 fn loop_record_commits_on_wrap() {
716 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
717 let _handle = add_armed_synth(&tx, 0);
718 let sr = 44100u32;
719 let buf_frames = 256u32;
720
721 transport.set_loop_bars(1, 1);
723 transport.start_loop_record();
724
725 let mut output = vec![0.0f32; buf_frames as usize * 2];
726
727 let midi = vec![make_note_on(60, 100)];
729 mixer.process(&mut output, &midi, &transport);
730 transport.advance(buf_frames, sr);
731
732 for _ in 0..5 {
734 mixer.process(&mut output, &[], &transport);
735 transport.advance(buf_frames, sr);
736 }
737 let midi = vec![make_note_off(60)];
738 mixer.process(&mut output, &midi, &transport);
739 transport.advance(buf_frames, sr);
740
741 for _ in 0..400 {
744 mixer.process(&mut output, &[], &transport);
745 transport.advance(buf_frames, sr);
746
747 if let Ok(snap) = clip_rx.try_recv() {
748 assert!(snap.event_count >= 2, "Clip should have events, got {}", snap.event_count);
749 assert!(!snap.notes.is_empty(), "Clip should have notes");
750 transport.stop_loop_record();
752 return;
753 }
754 }
755
756 panic!("Recording should have committed when the loop wrapped");
757 }
758
759 #[test]
760 fn loop_playback_after_record() {
761 let (mut mixer, tx, clip_rx, transport) = setup_mixer();
762 let _handle = add_armed_synth(&tx, 0);
763 let sr = 44100u32;
764 let bf = 256u32;
765
766 transport.set_loop_bars(1, 1);
768 transport.start_loop_record();
769
770 let mut output = vec![0.0f32; bf as usize * 2];
771
772 mixer.process(&mut output, &[make_note_on(60, 100)], &transport);
774 transport.advance(bf, sr);
775 for _ in 0..3 {
776 mixer.process(&mut output, &[], &transport);
777 transport.advance(bf, sr);
778 }
779 mixer.process(&mut output, &[make_note_off(60)], &transport);
780 transport.advance(bf, sr);
781
782 for _ in 0..200 {
784 mixer.process(&mut output, &[], &transport);
785 transport.advance(bf, sr);
786 if clip_rx.try_recv().is_ok() { break; }
787 }
788
789 transport.stop_loop_record();
791 transport.set_position(0);
792
793 transport.toggle_loop(); transport.play();
796
797 output.fill(0.0);
798 mixer.process(&mut output, &[], &transport);
799 let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
800 assert!(peak > 0.001, "Should hear playback, peak={peak}");
801 }
802}