1use std::collections::VecDeque;
17use std::sync::Arc;
18
19use super::engine::Ramp;
20use super::{AudioSource, SCRATCH_FRAMES, StreamSource, Transport, TransportState, Tween};
21use crate::dsl::SoundDoc;
22use crate::program::Program;
23
24pub const COMMAND_QUEUE_CAP: usize = 4096;
28
29const SWAP_FADE_FRAMES: usize = 1024;
32
33const GAIN_RAMP_FRAMES: usize = 256;
36
37#[derive(Debug, Clone, PartialEq)]
41pub enum At {
42 Immediate,
44 Frame(u64),
46 Beat(f64),
48 Bar(u32),
50 NextBeat,
52 NextBar,
54 Marker(String),
56 Section(String),
58}
59
60#[derive(Debug, Clone)]
62pub enum Command {
63 Play,
65 Pause,
67 Stop,
69 SeekBeat(f64),
71 SeekBar(u32),
73 SeekSection(String),
75 SetLoopBars(u32, u32),
77 ClearLoop,
79 SetGain(f32),
81 Swap(Arc<Program>),
84 Stinger {
87 samples: Arc<[f32]>,
91 gain: f32,
93 },
94}
95
96#[derive(Debug, Clone)]
99pub struct TimestampedCommand {
100 pub at_frame: u64,
102 pub seq: u64,
104 pub command: Command,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum PerformanceError {
111 QueueFull,
113 UnknownPosition(String),
115 BadProgram(String),
118}
119
120impl std::fmt::Display for PerformanceError {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 match self {
123 PerformanceError::QueueFull => {
124 f.write_str("the command queue is full — the command was rejected and counted")
125 }
126 PerformanceError::UnknownPosition(name) => {
127 write!(f, "no marker or section named '{name}'")
128 }
129 PerformanceError::BadProgram(why) => write!(f, "program rejected: {why}"),
130 }
131 }
132}
133
134impl std::error::Error for PerformanceError {}
135
136#[derive(Debug, Clone, Default, PartialEq, Eq)]
139pub struct PerformanceMetrics {
140 pub frames_rendered: u64,
142 pub commands_executed: u64,
144 pub commands_dropped: u64,
146 pub queue_depth_max: usize,
148 pub swaps: u64,
150 pub stingers_fired: u64,
152}
153
154enum SongSource {
159 Stream {
160 doc: Box<SoundDoc>,
161 source: Box<StreamSource>,
162 inter: Vec<f32>,
163 },
164 Buffer {
165 left: Vec<f32>,
166 right: Vec<f32>,
167 pos: usize,
168 },
169}
170
171impl SongSource {
172 fn build(program: &Program) -> SongSource {
175 if let Some(source) = StreamSource::from_doc(&program.doc) {
176 SongSource::Stream {
177 doc: Box::new(program.doc.clone()),
178 source: Box::new(source),
179 inter: vec![0.0; SCRATCH_FRAMES * 2],
180 }
181 } else {
182 let (left, right) = program.render_stereo();
183 SongSource::Buffer {
184 left,
185 right,
186 pos: 0,
187 }
188 }
189 }
190
191 fn fill(&mut self, left: &mut [f32], right: &mut [f32]) {
192 match self {
193 SongSource::Stream { source, inter, .. } => {
194 let n = left.len();
195 if inter.len() < n * 2 {
196 inter.resize(n * 2, 0.0);
197 }
198 source.fill(&mut inter[..n * 2]);
199 for i in 0..n {
200 left[i] = inter[i * 2];
201 right[i] = inter[i * 2 + 1];
202 }
203 }
204 SongSource::Buffer {
205 left: l,
206 right: r,
207 pos,
208 } => {
209 let n = left.len();
210 let avail = l.len().saturating_sub(*pos);
211 let take = avail.min(n);
212 left[..take].copy_from_slice(&l[*pos..*pos + take]);
213 right[..take].copy_from_slice(&r[*pos..*pos + take]);
214 for i in take..n {
215 left[i] = 0.0;
216 right[i] = 0.0;
217 }
218 *pos += take;
219 }
220 }
221 }
222
223 fn seek(&mut self, frame: usize) {
224 match self {
225 SongSource::Stream { doc, source, .. } => {
226 **source = StreamSource::from_doc(doc).expect("was streamable");
227 let mut remaining = frame;
228 let mut block = [0.0f32; 2048];
229 while remaining > 0 {
230 let take = remaining.min(1024);
231 source.fill(&mut block[..take * 2]);
232 remaining -= take;
233 }
234 }
235 SongSource::Buffer { pos, .. } => *pos = frame,
236 }
237 }
238}
239
240struct ActiveStinger {
244 buf: Arc<[f32]>,
246 pos: usize,
248 gain: Ramp,
251}
252
253pub struct Performance {
255 program: Arc<Program>,
256 song: SongSource,
257 transport: Transport,
258 queue: VecDeque<TimestampedCommand>,
259 swap_sources: std::collections::HashMap<u64, SongSource>,
263 clock: u64,
264 seq: u64,
265 master_gain: f32,
266 gain_ramp: Option<(f32, f32, usize)>, fade: Option<(SongSource, usize)>, metrics: PerformanceMetrics,
269 capture: Option<Vec<TimestampedCommand>>,
270 sample_rate: u32,
271 stingers: Vec<ActiveStinger>,
273 scratch_l: Vec<f32>,
274 scratch_r: Vec<f32>,
275 scratch_fl: Vec<f32>,
276 scratch_fr: Vec<f32>,
277 scratch_e: Vec<f32>,
278}
279
280impl Performance {
281 pub fn new(program: Arc<Program>) -> Self {
284 let sr = program.meta.sample_rate;
285 let transport = Transport::for_program(&program.meta);
286 let song = SongSource::build(&program);
287 Performance {
288 program,
289 song,
290 transport,
291 queue: VecDeque::with_capacity(COMMAND_QUEUE_CAP.min(64)),
292 swap_sources: std::collections::HashMap::new(),
293 clock: 0,
294 seq: 0,
295 master_gain: 1.0,
296 gain_ramp: None,
297 fade: None,
298 metrics: PerformanceMetrics::default(),
299 capture: None,
300 sample_rate: sr,
301 stingers: Vec::new(),
302 scratch_l: vec![0.0; SCRATCH_FRAMES],
303 scratch_r: vec![0.0; SCRATCH_FRAMES],
304 scratch_fl: vec![0.0; SCRATCH_FRAMES],
305 scratch_fr: vec![0.0; SCRATCH_FRAMES],
306 scratch_e: vec![0.0; SCRATCH_FRAMES * 2],
307 }
308 }
309
310 pub fn program(&self) -> &Arc<Program> {
312 &self.program
313 }
314
315 pub fn transport(&self) -> &Transport {
317 &self.transport
318 }
319
320 pub fn master_gain(&self) -> f32 {
322 self.master_gain
323 }
324
325 pub fn metrics(&self) -> PerformanceMetrics {
327 self.metrics.clone()
328 }
329
330 pub fn queue_depth(&self) -> usize {
332 self.queue.len()
333 }
334
335 pub fn clock(&self) -> u64 {
337 self.clock
338 }
339
340 fn resolve(&self, at: &At) -> Result<u64, PerformanceError> {
342 Ok(match at {
343 At::Immediate => self.clock,
344 At::Frame(f) => *f,
345 At::Beat(b) => self.deadline_for_transport_frame(self.transport.frame_at_beat(*b)),
346 At::Bar(b) => self.deadline_for_transport_frame(self.transport.frame_at_bar(*b)),
347 At::NextBeat => {
348 let pos = self.transport.position_beats();
349 self.deadline_for_transport_frame(self.transport.frame_at_beat(pos.floor() + 1.0))
350 }
351 At::NextBar => self.deadline_for_transport_frame(
352 self.transport
353 .frame_at_bar(self.transport.position_bars().floor() as u32 + 1),
354 ),
355 At::Marker(name) => {
356 let marker = self
357 .program
358 .meta
359 .markers
360 .iter()
361 .find(|m| &m.name == name)
362 .ok_or_else(|| PerformanceError::UnknownPosition(name.clone()))?;
363 self.deadline_for_transport_frame(self.transport.frame_at_beat(marker.at.to_f64()))
364 }
365 At::Section(name) => {
366 let section = self
367 .program
368 .meta
369 .sections
370 .iter()
371 .find(|s| &s.name == name)
372 .ok_or_else(|| PerformanceError::UnknownPosition(name.clone()))?;
373 self.deadline_for_transport_frame(self.transport.frame_at_bar(section.bar))
374 }
375 })
376 }
377
378 fn deadline_for_transport_frame(&self, target: u64) -> u64 {
382 let position = self.transport.position_frames();
383 let distance = if target >= position {
384 target - position
385 } else if let Some((start, end)) = self.transport.loop_range()
386 && (start..end).contains(&target)
387 && position < end
388 {
389 (end - position).saturating_add(target - start)
390 } else {
391 0
392 };
393 self.clock.saturating_add(distance)
394 }
395
396 fn require_queue_room(&mut self) -> Result<(), PerformanceError> {
397 if self.queue.len() < COMMAND_QUEUE_CAP {
398 return Ok(());
399 }
400 self.metrics.commands_dropped += 1;
401 Err(PerformanceError::QueueFull)
402 }
403
404 fn enqueue(&mut self, command: Command, frame: u64) -> u64 {
405 debug_assert!(self.queue.len() < COMMAND_QUEUE_CAP);
406 self.seq += 1;
407 let stamped = TimestampedCommand {
408 at_frame: frame,
409 seq: self.seq,
410 command,
411 };
412 if let Command::Swap(program) = &stamped.command {
415 self.swap_sources
416 .insert(stamped.seq, SongSource::build(program));
417 }
418 let pos = self
419 .queue
420 .iter()
421 .position(|c| (c.at_frame, c.seq) > (stamped.at_frame, stamped.seq))
422 .unwrap_or(self.queue.len());
423 if let Some(capture) = &mut self.capture {
424 capture.push(stamped.clone());
425 }
426 self.queue.insert(pos, stamped);
427 self.metrics.queue_depth_max = self.metrics.queue_depth_max.max(self.queue.len());
428 self.seq
429 }
430
431 pub fn schedule(&mut self, command: Command, at: At) -> Result<u64, PerformanceError> {
435 let frame = self.resolve(&at)?;
436 self.require_queue_room()?;
437 Ok(self.enqueue(command, frame))
438 }
439
440 pub fn stinger(&mut self, doc: &SoundDoc, gain: f32, at: At) -> Result<u64, PerformanceError> {
444 let frame = self.resolve(&at)?;
445 self.require_queue_room()?;
446 let mut doc = doc.clone();
449 doc.sample_rate = self.sample_rate;
450 let (left, right) = crate::player::render_stereo(&doc);
451 let mut buf = Vec::with_capacity(left.len() * 2);
452 for i in 0..left.len() {
453 buf.push(left[i]);
454 buf.push(right[i]);
455 }
456 let samples: Arc<[f32]> = buf.into();
457 let pending = self
460 .queue
461 .iter()
462 .filter(|c| matches!(c.command, Command::Stinger { .. }))
463 .count()
464 + 1;
465 self.stingers.reserve(pending);
466 Ok(self.enqueue(Command::Stinger { samples, gain }, frame))
467 }
468
469 pub fn swap_to(&mut self, program: Arc<Program>, at: At) -> Result<u64, PerformanceError> {
473 if program.program_version > crate::program::PROGRAM_VERSION {
474 return Err(PerformanceError::BadProgram(format!(
475 "program version {} is newer than supported ({})",
476 program.program_version,
477 crate::program::PROGRAM_VERSION
478 )));
479 }
480 if program.hash != program.computed_hash() {
481 return Err(PerformanceError::BadProgram(
482 "hash mismatch — the program was edited after compilation".into(),
483 ));
484 }
485 if program.meta.sample_rate != self.sample_rate
486 || program.doc.sample_rate != self.sample_rate
487 {
488 return Err(PerformanceError::BadProgram(format!(
489 "sample rate mismatch — performance is {} Hz, program metadata is {} Hz, and its document is {} Hz",
490 self.sample_rate, program.meta.sample_rate, program.doc.sample_rate
491 )));
492 }
493 self.schedule(Command::Swap(program), at)
494 }
495
496 pub fn transition_to_section(&mut self, name: &str, at: At) -> Result<u64, PerformanceError> {
502 if !self.program.meta.sections.iter().any(|s| s.name == name) {
503 return Err(PerformanceError::UnknownPosition(name.to_string()));
504 }
505 let frame = self.resolve(&at)?;
506 let replaced: Vec<u64> = self
508 .queue
509 .iter()
510 .filter(|c| matches!(c.command, Command::SeekSection(_)))
511 .map(|c| c.seq)
512 .collect();
513 self.queue
514 .retain(|c| !matches!(c.command, Command::SeekSection(_)));
515 if let Some(capture) = &mut self.capture {
516 capture.retain(|c| !replaced.contains(&c.seq));
517 }
518 self.require_queue_room()?;
519 Ok(self.enqueue(Command::SeekSection(name.to_string()), frame))
520 }
521
522 pub fn start_capture(&mut self) {
524 self.capture = Some(Vec::new());
525 }
526
527 pub fn stop_capture(&mut self) -> Vec<TimestampedCommand> {
529 self.capture.take().unwrap_or_default()
530 }
531
532 pub fn replay(&mut self, commands: &[TimestampedCommand]) {
534 let stingers = commands
537 .iter()
538 .filter(|c| matches!(c.command, Command::Stinger { .. }))
539 .count();
540 if stingers > 0 {
541 self.stingers.reserve(stingers);
542 }
543 for c in commands {
544 let frame = c.at_frame;
548 if self.queue.len() >= COMMAND_QUEUE_CAP {
549 self.metrics.commands_dropped += 1;
550 continue;
551 }
552 self.seq += 1;
553 if let Command::Swap(program) = &c.command {
554 self.swap_sources
555 .insert(self.seq, SongSource::build(program));
556 }
557 self.queue.push_back(TimestampedCommand {
558 at_frame: frame,
559 seq: self.seq,
560 command: c.command.clone(),
561 });
562 }
563 self.queue
564 .make_contiguous()
565 .sort_by_key(|c| (c.at_frame, c.seq));
566 }
567
568 pub fn snapshot(&self) -> PerformanceSnapshot {
571 PerformanceSnapshot {
572 position: self.transport.position_frames(),
573 state: self.transport.state(),
574 master_gain: self.master_gain,
575 loop_range: self.transport.loop_range(),
576 }
577 }
578
579 pub fn apply_snapshot(&mut self, snapshot: &PerformanceSnapshot) {
581 self.master_gain = snapshot.master_gain;
582 if let Some((start, end)) = snapshot.loop_range {
583 self.transport.set_loop_frames(start, end);
584 } else {
585 self.transport.clear_loop();
586 }
587 match snapshot.state {
588 TransportState::Stopped => self.transport.stop(),
589 TransportState::Playing => self.transport.play(),
590 TransportState::Paused => self.transport.pause(),
591 }
592 self.transport.seek_frame(snapshot.position);
593 self.song.seek(self.transport.position_frames() as usize);
594 }
595
596 fn execute(&mut self, stamped: TimestampedCommand) {
598 self.metrics.commands_executed += 1;
599 match stamped.command {
600 Command::Play => self.transport.play(),
601 Command::Pause => self.transport.pause(),
602 Command::Stop => {
603 self.transport.stop();
604 self.song.seek(0);
605 }
606 Command::SeekBeat(beat) => {
607 let frame = self.transport.frame_at_beat(beat);
608 self.transport.seek_frame(frame);
609 self.song.seek(frame as usize);
610 }
611 Command::SeekBar(bar) => {
612 let frame = self.transport.frame_at_bar(bar);
613 self.transport.seek_frame(frame);
614 self.song.seek(frame as usize);
615 }
616 Command::SeekSection(name) => {
617 if let Some(section) = self.program.meta.sections.iter().find(|s| s.name == name) {
618 let frame = self.transport.frame_at_bar(section.bar);
619 self.transport.seek_frame(frame);
620 self.song.seek(frame as usize);
621 }
622 }
623 Command::SetLoopBars(start, end) => {
624 self.transport.set_loop_bars(start, end);
625 }
626 Command::ClearLoop => self.transport.clear_loop(),
627 Command::SetGain(gain) => {
628 let from = self.current_gain();
629 self.master_gain = gain.clamp(0.0, 2.0);
630 self.gain_ramp = Some((from, self.master_gain, GAIN_RAMP_FRAMES));
631 }
632 Command::Swap(program) => {
633 if let Some(new_source) = self.swap_sources.remove(&stamped.seq) {
637 let outgoing = std::mem::replace(&mut self.song, new_source);
638 self.fade = Some((outgoing, SWAP_FADE_FRAMES));
639 self.transport = Transport::for_program(&program.meta);
640 self.transport.play();
641 self.program = program;
642 self.metrics.swaps += 1;
643 }
644 }
645 Command::Stinger { samples, gain } => {
646 let mut ramp = Ramp::new(1.0);
647 ramp.set(gain.max(0.0), Tween::ms(2.0, self.sample_rate));
648 self.stingers.push(ActiveStinger {
649 buf: samples,
650 pos: 0,
651 gain: ramp,
652 });
653 self.metrics.stingers_fired += 1;
654 }
655 }
656 }
657
658 fn current_gain(&self) -> f32 {
660 match &self.gain_ramp {
661 Some((from, to, left)) => {
662 from + (to - from) * (1.0 - *left as f32 / GAIN_RAMP_FRAMES as f32)
663 }
664 None => self.master_gain,
665 }
666 }
667
668 fn render_slice(&mut self, out: &mut [f32]) {
672 let frames = out.len() / 2;
673 if self.scratch_l.len() < frames {
674 self.scratch_l.resize(frames, 0.0);
675 self.scratch_r.resize(frames, 0.0);
676 self.scratch_fl.resize(frames, 0.0);
677 self.scratch_fr.resize(frames, 0.0);
678 }
679 if self.scratch_e.len() < out.len() {
680 self.scratch_e.resize(out.len(), 0.0);
681 }
682 if self.transport.is_playing() {
683 let mut done = 0usize;
687 while done < frames {
688 let chunk = match self.transport.loop_range() {
689 Some((_, end)) => {
690 let pos = self.transport.position_frames();
691 (end.saturating_sub(pos) as usize).min(frames - done)
692 }
693 None => frames - done,
694 };
695 if chunk == 0 {
696 let advance = self.transport.advance(0);
698 if advance.wrapped
699 && let Some((start, _)) = self.transport.loop_range()
700 {
701 self.song.seek(start as usize);
702 }
703 continue;
704 }
705 self.song.fill(
706 &mut self.scratch_l[done..done + chunk],
707 &mut self.scratch_r[done..done + chunk],
708 );
709 let advance = self.transport.advance(chunk as u64);
710 done += chunk;
711 if advance.wrapped
712 && let Some((start, _)) = self.transport.loop_range()
713 {
714 self.song.seek(start as usize);
715 }
716 if advance.finished {
717 self.scratch_l[done..frames].fill(0.0);
719 self.scratch_r[done..frames].fill(0.0);
720 break;
721 }
722 }
723 } else {
724 self.scratch_l[..frames].fill(0.0);
725 self.scratch_r[..frames].fill(0.0);
726 }
727 let fading = self.fade.is_some();
730 if fading {
731 let (outgoing, _) = self.fade.as_mut().expect("checked");
732 outgoing.fill(
733 &mut self.scratch_fl[..frames],
734 &mut self.scratch_fr[..frames],
735 );
736 }
737 {
740 let out_frames = frames;
741 let stereo = &mut self.scratch_e[..out.len()];
742 stereo.fill(0.0);
743 for s in &mut self.stingers {
744 let take = ((s.buf.len() - s.pos) / 2).min(out_frames);
745 for f in 0..take {
746 let g = s.gain.tick();
747 stereo[f * 2] += s.buf[s.pos + f * 2] * g;
748 stereo[f * 2 + 1] += s.buf[s.pos + f * 2 + 1] * g;
749 }
750 s.pos += take * 2;
751 }
752 self.stingers.retain(|s| s.pos < s.buf.len());
753 }
754 let mut fade_left = self.fade.as_ref().map(|(_, left)| *left);
759 for i in 0..frames {
760 let g = if let Some((from, to, left)) = self.gain_ramp {
761 let u = (GAIN_RAMP_FRAMES - left) as f32 / GAIN_RAMP_FRAMES as f32;
762 self.gain_ramp = (left > 1).then_some((from, to, left - 1));
763 from + (to - from) * u
764 } else {
765 self.master_gain
766 };
767 let mut left = self.scratch_l[i] * g;
768 let mut right = self.scratch_r[i] * g;
769 if let Some(fl) = fade_left.filter(|fl| *fl > 0) {
770 let fade_u = fl as f32 / SWAP_FADE_FRAMES as f32;
771 fade_left = Some(fl - 1);
772 left += self.scratch_fl[i] * fade_u;
773 right += self.scratch_fr[i] * fade_u;
774 }
775 out[i * 2] = left + self.scratch_e[i * 2];
776 out[i * 2 + 1] = right + self.scratch_e[i * 2 + 1];
777 }
778 if let Some((_, left)) = &mut self.fade {
780 match fade_left {
781 Some(fl) if fl > 0 => *left = fl,
782 _ => self.fade = None,
783 }
784 }
785 self.metrics.frames_rendered += frames as u64;
786 }
787}
788
789impl AudioSource for Performance {
790 fn fill(&mut self, out: &mut [f32]) -> usize {
794 let frames = out.len() / 2;
795 let mut done = 0usize;
796 while done < frames {
797 let due = self
798 .queue
799 .front()
800 .filter(|c| c.at_frame <= self.clock + (frames - done) as u64)
801 .map(|c| c.at_frame);
802 match due {
803 Some(at) => {
804 let at = (at.saturating_sub(self.clock) as usize).min(frames - done);
805 if at > 0 {
806 self.render_slice(&mut out[done * 2..(done + at) * 2]);
807 self.clock += at as u64;
808 done += at;
809 }
810 let stamped = self.queue.pop_front().expect("front was due");
811 self.execute(stamped);
812 }
813 None => {
814 self.render_slice(&mut out[done * 2..]);
815 self.clock += (frames - done) as u64;
816 done = frames;
817 }
818 }
819 }
820 frames
821 }
822}
823
824#[derive(Debug, Clone, PartialEq)]
826pub struct PerformanceSnapshot {
827 pub position: u64,
829 pub state: TransportState,
831 pub master_gain: f32,
833 pub loop_range: Option<(u64, u64)>,
835}
836
837#[cfg(test)]
838mod tests {
839 use super::*;
840 use crate::dsl::{Adsr, SeqWave};
841 use crate::song::{CompileOptions, Song, note};
842
843 fn amp() -> Adsr {
844 Adsr {
845 a: 0.005,
846 d: 0.1,
847 s: 0.8,
848 r: 0.2,
849 punch: 0.0,
850 }
851 }
852
853 fn demo_program() -> Arc<Program> {
854 let mut song = Song::new("perf-demo", 120.0);
855 song.add_track("bass", SeqWave::Bass, amp());
856 song.add_track("keys", SeqWave::Epiano, amp());
857 song.add_pattern("riff", 1, vec![note(0, 4, "C2"), note(8, 4, "G2")]);
858 song.add_pattern("stab", 1, vec![note(0, 2, "C4"), note(6, 2, "D#4")]);
859 song.arrange_repeat("bass", "riff", 0, 4);
860 song.arrange_repeat("keys", "stab", 0, 4);
861 song.sections.push(crate::song::Section {
862 name: "second".into(),
863 bar: 2,
864 bars: 2,
865 });
866 Arc::new(song.compile(&CompileOptions::default()).unwrap())
867 }
868
869 fn other_program() -> Arc<Program> {
870 let mut song = Song::new("perf-other", 100.0);
871 song.add_track("lead", SeqWave::Square, amp());
872 song.tracks[0].notes.push(note(0, 16, "A4"));
873 Arc::new(song.compile(&CompileOptions::default()).unwrap())
874 }
875
876 fn other_program_at(sample_rate: u32) -> Arc<Program> {
877 let mut song = Song::new("perf-other-rate", 100.0);
878 song.add_track("lead", SeqWave::Square, amp());
879 song.tracks[0].notes.push(note(0, 16, "A4"));
880 Arc::new(
881 song.compile(&CompileOptions {
882 sample_rate: Some(sample_rate),
883 ..CompileOptions::default()
884 })
885 .unwrap(),
886 )
887 }
888
889 fn stinger_doc() -> SoundDoc {
890 serde_json::from_str(
891 r#"{ "name": "blip", "duration": 0.2, "root": { "type": "mul", "inputs": [
892 { "type": "sawtooth", "freq": 880 },
893 { "type": "env", "a": 0.0, "d": 0.05, "s": 0.0, "r": 0.01 } ] } }"#,
894 )
895 .unwrap()
896 }
897
898 fn bounce_interleaved(program: &Program) -> Vec<f32> {
899 let (l, r) = program.render_stereo();
900 let mut out = Vec::with_capacity(l.len() * 2);
901 for i in 0..l.len() {
902 out.push(l[i]);
903 out.push(r[i]);
904 }
905 out
906 }
907
908 fn fill_all(p: &mut Performance, frames: usize, block: usize) -> Vec<f32> {
909 let mut got = Vec::with_capacity(frames * 2);
910 while got.len() < frames * 2 {
911 let take = block.min(frames - got.len() / 2);
912 let mut buf = vec![0.0f32; take * 2];
913 p.fill(&mut buf);
914 got.extend_from_slice(&buf);
915 }
916 got
917 }
918
919 fn bits(s: &[f32]) -> Vec<u32> {
920 s.iter().map(|x| x.to_bits()).collect()
921 }
922
923 #[test]
924 fn plays_byte_identical_to_the_bounce() {
925 let program = demo_program();
926 let expected = bounce_interleaved(&program);
927 let mut p = Performance::new(program.clone());
928 assert!(program.is_streamable(), "the demo streams natively");
929 p.schedule(Command::Play, At::Immediate).unwrap();
930 for block in [1usize, 7, 64, 333, 4096] {
931 let mut p = Performance::new(program.clone());
932 p.schedule(Command::Play, At::Immediate).unwrap();
933 let got = fill_all(&mut p, expected.len() / 2, block);
934 assert_eq!(bits(&got), bits(&expected), "block size {block} diverged");
935 }
936 }
937
938 #[test]
939 fn scheduled_gain_lands_on_the_exact_frame() {
940 let program = demo_program();
941 let expected = bounce_interleaved(&program);
942 let at = 10_000usize;
943 let mut p = Performance::new(program);
944 p.schedule(Command::Play, At::Immediate).unwrap();
945 p.schedule(Command::SetGain(0.0), At::Frame(at as u64))
946 .unwrap();
947 let got = fill_all(&mut p, expected.len() / 2, 512);
948 assert_eq!(bits(&got[..at * 2]), bits(&expected[..at * 2]));
950 let tail_rms: f32 = {
953 let start = (at + 1_024) * 2;
954 let sum: f32 = got[start..].iter().map(|x| x * x).sum();
955 (sum / (got.len() - start) as f32).sqrt()
956 };
957 assert_eq!(tail_rms, 0.0, "gain 0 after the ramp");
958 assert_eq!(p.metrics().commands_executed, 2);
959 }
960
961 #[test]
962 fn seek_and_loop_reproduce_the_bounce_region() {
963 let program = demo_program();
964 let expected = bounce_interleaved(&program);
965 let bar2 = {
968 let t = Transport::for_program(&program.meta);
969 t.frame_at_bar(2) as usize
970 };
971 let mut p = Performance::new(program.clone());
972 p.schedule(Command::SeekBar(2), At::Immediate).unwrap();
973 p.schedule(Command::Play, At::Immediate).unwrap();
974 let got = fill_all(&mut p, 8_000, 512);
975 assert_eq!(bits(&got), bits(&expected[bar2 * 2..(bar2 + 8_000) * 2]));
976 let mut p = Performance::new(program);
978 p.schedule(Command::SetLoopBars(1, 2), At::Immediate)
979 .unwrap();
980 p.schedule(Command::SeekBar(1), At::Immediate).unwrap();
981 p.schedule(Command::Play, At::Immediate).unwrap();
982 let bar1 = {
983 let t = Transport::for_program(&p.program().meta);
984 t.frame_at_bar(1) as usize
985 };
986 let span = bar2 - bar1;
987 let got = fill_all(&mut p, span + 2_000, 256);
988 assert_eq!(
989 bits(&got[..2_000 * 2]),
990 bits(&expected[bar1 * 2..(bar1 + 2_000) * 2]),
991 "first pass"
992 );
993 assert_eq!(
994 bits(&got[span * 2..(span + 2_000) * 2]),
995 bits(&expected[bar1 * 2..(bar1 + 2_000) * 2]),
996 "the wrapped pass repeats the loop region exactly"
997 );
998 }
999
1000 #[test]
1001 fn section_transition_lands_on_the_section_bar() {
1002 let program = demo_program();
1003 let expected = bounce_interleaved(&program);
1004 let bar2 = {
1005 let t = Transport::for_program(&program.meta);
1006 t.frame_at_bar(2) as usize
1007 };
1008 let mut p = Performance::new(program);
1009 p.schedule(Command::Play, At::Immediate).unwrap();
1010 p.transition_to_section("second", At::Frame(5_000)).unwrap();
1011 let got = fill_all(&mut p, 12_000, 512);
1012 assert_eq!(
1013 bits(&got[5_000 * 2..7_000 * 2]),
1014 bits(&expected[bar2 * 2..(bar2 + 2_000) * 2]),
1015 "post-transition audio is the section's first bar"
1016 );
1017 let mut p = Performance::new(demo_program());
1019 assert!(p.transition_to_section("nope", At::Immediate).is_err());
1020 }
1021
1022 #[test]
1023 fn stinger_fires_on_the_exact_beat() {
1024 let program = demo_program();
1025 let stinger = stinger_doc();
1026 let at = {
1028 let t = Transport::for_program(&program.meta);
1029 t.frame_at_beat(4.0) as usize
1030 };
1031 let mut p = Performance::new(program.clone());
1032 p.schedule(Command::Play, At::Immediate).unwrap();
1033 p.stinger(&stinger, 1.0, At::Beat(4.0)).unwrap();
1034 let got = fill_all(&mut p, at + 4_000, 512);
1035 let before = &got[(at - 8) * 2..at * 2];
1038 let after = &got[at * 2..(at + 64) * 2];
1039 let bmax = before.iter().fold(0.0f32, |m, x| m.max(x.abs()));
1040 let amax = after.iter().fold(0.0f32, |m, x| m.max(x.abs()));
1041 assert!(amax > bmax, "the stinger onset lands on the beat");
1042 let mix_window = &got[at * 2..(at + 64) * 2];
1045 let bounce = bounce_interleaved(&program);
1046 let diverges = mix_window
1047 .iter()
1048 .zip(&bounce[at * 2..(at + 64) * 2])
1049 .any(|(a, b)| (a - b).abs() > 1e-6);
1050 assert!(diverges, "the mix at the beat carries the stinger");
1051 assert_eq!(p.metrics().stingers_fired, 1);
1052 }
1053
1054 #[test]
1055 fn swap_crossfades_deterministically() {
1056 let run = || {
1057 let mut p = Performance::new(demo_program());
1058 p.schedule(Command::Play, At::Immediate).unwrap();
1059 p.swap_to(other_program(), At::Frame(20_000)).unwrap();
1060 fill_all(&mut p, 60_000, 512)
1061 };
1062 let a = run();
1063 let b = run();
1064 assert_eq!(bits(&a), bits(&b), "the swap is deterministic");
1065 let window = &a[19_000 * 2..21_500 * 2];
1068 let max_step = window
1069 .windows(2)
1070 .map(|w| (w[1] - w[0]).abs())
1071 .fold(0.0f32, f32::max);
1072 assert!(max_step < 1.5, "no click at the swap ({max_step})");
1073 assert_eq!(Performance::new(demo_program()).metrics().swaps, 0);
1074 }
1075
1076 #[test]
1077 fn capture_and_replay_reproduces_the_take() {
1078 let program = demo_program();
1079 let scripted = |p: &mut Performance| {
1080 p.schedule(Command::Play, At::Immediate).unwrap();
1081 p.schedule(Command::SetGain(0.5), At::Frame(8_000)).unwrap();
1082 p.schedule(Command::SeekBar(1), At::Frame(12_000)).unwrap();
1083 p.schedule(Command::SetGain(1.0), At::Frame(16_000))
1084 .unwrap();
1085 };
1086 let mut p = Performance::new(program.clone());
1087 p.start_capture();
1088 scripted(&mut p);
1089 let captured = p.stop_capture();
1090 assert_eq!(captured.len(), 4);
1091 let take_a = fill_all(&mut p, 40_000, 512);
1092
1093 let mut q = Performance::new(program);
1094 q.replay(&captured);
1095 let take_b = fill_all(&mut q, 40_000, 512);
1096 assert_eq!(bits(&take_a), bits(&take_b), "replay reproduces the take");
1097 }
1098
1099 #[test]
1100 fn captured_stinger_replays_on_a_fresh_performance() {
1101 let program = demo_program();
1102 let mut p = Performance::new(program.clone());
1103 p.start_capture();
1104 p.schedule(Command::Play, At::Immediate).unwrap();
1105 p.stinger(&stinger_doc(), 0.75, At::Frame(1_000)).unwrap();
1106 let captured = p.stop_capture();
1107 let take_a = fill_all(&mut p, 12_000, 333);
1108
1109 let mut q = Performance::new(program);
1110 q.replay(&captured);
1111 let take_b = fill_all(&mut q, 12_000, 512);
1112 assert_eq!(bits(&take_a), bits(&take_b));
1113 assert_eq!(q.metrics().stingers_fired, 1);
1114 }
1115
1116 #[test]
1117 fn musical_deadlines_are_relative_to_the_current_transport() {
1118 let mut p = Performance::new(demo_program());
1119 p.schedule(Command::Play, At::Immediate).unwrap();
1120 fill_all(&mut p, 1_000, 512);
1121 p.schedule(Command::Pause, At::Immediate).unwrap();
1122 fill_all(&mut p, 5_000, 512);
1123 assert_eq!(p.clock(), 6_000);
1124 assert_eq!(p.transport().position_frames(), 1_000);
1125
1126 let next_beat = p.transport().frame_at_beat(1.0);
1127 p.schedule(Command::SetGain(0.5), At::NextBeat).unwrap();
1128 let queued = p.queue.back().unwrap();
1129 assert_eq!(queued.at_frame, 6_000 + next_beat - 1_000);
1130 }
1131
1132 #[test]
1133 fn swap_rejects_a_different_sample_rate_in_core() {
1134 let mut p = Performance::new(demo_program());
1135 let err = p
1136 .swap_to(other_program_at(48_000), At::Immediate)
1137 .unwrap_err();
1138 assert!(matches!(err, PerformanceError::BadProgram(_)));
1139 assert_eq!(p.program().meta.sample_rate, 44_100);
1140 }
1141
1142 #[test]
1143 fn a_full_queue_rejects_and_counts() {
1144 let mut p = Performance::new(demo_program());
1145 for i in 0..COMMAND_QUEUE_CAP {
1146 p.schedule(Command::SetGain(0.5), At::Frame(1_000_000 + i as u64))
1147 .unwrap();
1148 }
1149 let err = p.schedule(Command::Play, At::Immediate).unwrap_err();
1150 assert_eq!(err, PerformanceError::QueueFull);
1151 assert_eq!(p.metrics().commands_dropped, 1);
1152 assert_eq!(p.queue_depth(), COMMAND_QUEUE_CAP);
1153 }
1154
1155 #[test]
1156 fn snapshot_restores_the_control_state() {
1157 let mut p = Performance::new(demo_program());
1158 p.schedule(Command::Play, At::Immediate).unwrap();
1159 fill_all(&mut p, 10_000, 512);
1160 let snap = p.snapshot();
1161 let take_a = fill_all(&mut p, 4_000, 512);
1162 p.apply_snapshot(&snap);
1163 let take_b = fill_all(&mut p, 4_000, 512);
1164 assert_eq!(
1165 bits(&take_a),
1166 bits(&take_b),
1167 "the snapshot replays the position"
1168 );
1169
1170 let mut stopped = Performance::new(demo_program());
1171 stopped.transport.seek_frame(12_345);
1172 let snap = stopped.snapshot();
1173 stopped.apply_snapshot(&snap);
1174 assert_eq!(stopped.transport.state(), TransportState::Stopped);
1175 assert_eq!(stopped.transport.position_frames(), 12_345);
1176 }
1177
1178 #[test]
1179 fn gain_ride_and_swap_fade_are_block_size_invariant() {
1180 let run_gain = |block: usize| {
1183 let mut p = Performance::new(demo_program());
1184 p.schedule(Command::Play, At::Immediate).unwrap();
1185 p.schedule(Command::SetGain(0.5), At::Frame(1_000)).unwrap();
1186 p.schedule(Command::SetGain(1.0), At::Frame(1_500)).unwrap();
1187 fill_all(&mut p, 8_000, block)
1188 };
1189 for block in [1usize, 7, 333, 512, 4096] {
1190 assert_eq!(
1191 bits(&run_gain(block)),
1192 bits(&run_gain(512)),
1193 "gain ride diverged at block size {block}"
1194 );
1195 }
1196 let run_swap = |block: usize| {
1197 let mut p = Performance::new(demo_program());
1198 p.schedule(Command::Play, At::Immediate).unwrap();
1199 p.swap_to(other_program(), At::Frame(1_000)).unwrap();
1200 fill_all(&mut p, 8_000, block)
1201 };
1202 for block in [1usize, 7, 333, 512, 4096] {
1203 assert_eq!(
1204 bits(&run_swap(block)),
1205 bits(&run_swap(512)),
1206 "swap crossfade diverged at block size {block}"
1207 );
1208 }
1209 }
1210}