use std::sync::Arc;
use crate::core::preanalysis::PreAnalysisArtifact;
use crate::core::ring_buffer::RingBuffer;
use crate::engine::control::{clamp_tempo_rate, EngineShared, Param};
use crate::engine::source::{SourceRing, TimelineMap};
use crate::engine::stage::{BlockBuf, OnsetEvent, Stage, StageCtx, BLOCK_FRAMES};
use crate::engine::stages::transient::{TransientCursor, MAX_EVENTS};
use crate::engine::stages::varispeed::{VarispeedHead, FEED_CHUNK_FRAMES, MAX_OUT_PER_FEED};
const MODULATION_HOLD_BLOCKS: u32 = 64;
const MODULATION_HOLD_TRIGGER: f64 = 1e-3;
const PRIME_BUDGET_CALLBACK_MULTIPLE: u64 = 2;
const PRIME_BUDGET_MIN_FRAMES: u64 = 256;
const PRIME_BUDGET_MAX_FRAMES: u64 = 2_048;
const DECLICK_FRAMES: u32 = 64;
const MAX_PENDING_RETARGETS: usize = 8;
const RATE_HISTORY_MARGIN: usize = 256;
pub struct EngineProcessor {
shared: Arc<EngineShared>,
ring: Arc<SourceRing>,
varispeed: VarispeedHead,
stages: Vec<Box<dyn Stage>>,
block: BlockBuf,
stage_fifo: RingBuffer<f32>,
out_fifo: RingBuffer<f32>,
timeline: TimelineMap,
emitted_frames: u64,
stage_in_frames: u64,
delivered_frames: u64,
underrun_total: u64,
channels: usize,
sample_rate: u32,
rate: f64,
max_block_frames: usize,
feed_scratch: Vec<f32>,
interleave_scratch: Vec<f32>,
block_scratch: Vec<f32>,
pipeline_latency_frames: usize,
transient: Option<TransientCursor>,
anchor: (u64, u64),
ring_frames_at_reset: u64,
modulation_hold_blocks: u32,
prime_source_frames: f64,
discarded_frames: u64,
declick_remaining: u32,
pending_retargets: [(u64, f64); MAX_PENDING_RETARGETS],
pending_retarget_len: usize,
}
impl std::fmt::Debug for EngineProcessor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EngineProcessor")
.field("channels", &self.channels)
.field("sample_rate", &self.sample_rate)
.field("rate", &self.rate)
.field("stages", &self.stages.len())
.field("delivered_frames", &self.delivered_frames)
.finish_non_exhaustive()
}
}
impl EngineProcessor {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
shared: Arc<EngineShared>,
ring: Arc<SourceRing>,
stages: Vec<Box<dyn Stage>>,
channels: usize,
sample_rate: u32,
initial_rate: f64,
max_block_frames: usize,
artifact: Option<Arc<PreAnalysisArtifact>>,
) -> Self {
let out_fifo_frames = max_block_frames + 2 * MAX_OUT_PER_FEED + BLOCK_FRAMES;
let pipeline_latency_frames: usize = stages.iter().map(|s| s.latency_frames()).sum();
let timeline_capacity = (out_fifo_frames + pipeline_latency_frames + RATE_HISTORY_MARGIN)
/ (FEED_CHUNK_FRAMES / 4)
+ 16;
Self {
shared,
ring,
varispeed: VarispeedHead::new(channels),
block: BlockBuf::new(channels),
stage_fifo: RingBuffer::with_capacity((BLOCK_FRAMES + 2 * MAX_OUT_PER_FEED) * channels),
out_fifo: RingBuffer::with_capacity(out_fifo_frames * channels),
timeline: TimelineMap::with_capacity(timeline_capacity),
emitted_frames: 0,
stage_in_frames: 0,
delivered_frames: 0,
underrun_total: 0,
channels,
sample_rate,
rate: clamp_tempo_rate(initial_rate),
max_block_frames,
feed_scratch: vec![0.0; FEED_CHUNK_FRAMES * channels],
interleave_scratch: vec![0.0; MAX_OUT_PER_FEED * channels],
block_scratch: vec![0.0; BLOCK_FRAMES * channels],
stages,
pipeline_latency_frames,
transient: artifact.map(TransientCursor::new),
anchor: (0, 0),
ring_frames_at_reset: 0,
modulation_hold_blocks: 0,
prime_source_frames: 0.0,
discarded_frames: 0,
declick_remaining: 0,
pending_retargets: [(0, 1.0); MAX_PENDING_RETARGETS],
pending_retarget_len: 0,
}
}
pub fn process(&mut self, out: &mut [f32]) {
debug_assert_eq!(out.len() % self.channels, 0);
let whole = out.len() / self.channels * self.channels;
self.apply_pending_control();
if self.prime_source_frames > 0.0 {
let budget = ((whole / self.channels) as u64 * PRIME_BUDGET_CALLBACK_MULTIPLE)
.clamp(PRIME_BUDGET_MIN_FRAMES, PRIME_BUDGET_MAX_FRAMES);
self.run_priming(budget);
if self.prime_source_frames > 0.0 {
out.fill(0.0);
self.shared
.publish_position(self.priming_position(), self.delivered_frames);
return;
}
}
let max_chunk = self.max_block_frames * self.channels;
let mut offset = 0;
while offset < whole {
let end = (offset + max_chunk).min(whole);
self.render_chunk(&mut out[offset..end]);
offset = end;
}
out[whole..].fill(0.0);
self.timeline.evict_before(
self.media_delivered_frames()
.saturating_sub((self.pipeline_latency_frames + RATE_HISTORY_MARGIN) as u64),
);
let position = self
.timeline
.map_to_source(self.position_query_frame())
.unwrap_or(0.0);
self.shared
.publish_position(position, self.delivered_frames);
}
pub fn pipeline_latency_frames(&self) -> usize {
self.pipeline_latency_frames
}
pub fn control_to_audio_bound_frames(&self) -> usize {
let feed_out = (FEED_CHUNK_FRAMES as f64 / self.rate).ceil() as usize + 1;
let blocking = if self.stages.is_empty() {
0
} else {
BLOCK_FRAMES - 1
};
2 * feed_out + blocking
}
pub fn varispeed_lookahead_frames(&self) -> usize {
self.varispeed.lookahead_frames()
}
pub fn reset(&mut self) {
self.varispeed.reset();
for stage in &mut self.stages {
stage.reset();
}
self.stage_fifo.clear();
self.out_fifo.clear();
self.timeline.clear();
self.emitted_frames = 0;
self.stage_in_frames = 0;
self.delivered_frames = 0;
self.underrun_total = 0;
if let Some(cursor) = self.transient.as_mut() {
cursor.reset();
}
self.modulation_hold_blocks = 0;
self.prime_source_frames = 0.0;
self.discarded_frames = 0;
self.declick_remaining = 0;
self.pending_retarget_len = 0;
let max_drains = self.ring.capacity_samples() / self.feed_scratch.len() + 4;
for _ in 0..max_drains {
if self.ring.pop_slice(&mut self.feed_scratch) == 0 {
break;
}
}
self.ring_frames_at_reset = self.ring.head_frames();
self.shared.publish_position(0.0, 0);
}
pub fn channels(&self) -> usize {
self.channels
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub fn current_tempo_rate(&self) -> f64 {
self.rate
}
pub fn warm_start_preroll_frames(&self) -> usize {
if self.stages.is_empty() {
64
} else {
self.pipeline_latency_frames + 1_024
}
}
fn run_priming(&mut self, budget: u64) {
let mut budget = budget;
loop {
let done_at = self
.timeline
.map_to_output(self.prime_source_frames)
.map(|out_frame| out_frame.floor().max(0.0) as u64);
if let Some(done_at) = done_at {
if self.discarded_frames >= done_at {
self.prime_source_frames = 0.0;
self.declick_remaining = DECLICK_FRAMES;
return;
}
}
if budget == 0 {
return;
}
let want = match done_at {
Some(done_at) => (done_at - self.discarded_frames)
.min(BLOCK_FRAMES as u64)
.min(budget) as usize,
None => BLOCK_FRAMES.min(budget as usize),
};
let want_samples = want * self.channels;
self.fill_out_fifo(want_samples);
let popped = self
.out_fifo
.pop_slice(&mut self.block_scratch[..want_samples]);
if popped == 0 {
return; }
let frames = (popped / self.channels) as u64;
self.discarded_frames += frames;
budget = budget.saturating_sub(frames);
}
}
fn priming_position(&self) -> f64 {
self.timeline
.map_to_source(self.discarded_frames as f64)
.unwrap_or(0.0)
}
fn media_delivered_frames(&self) -> u64 {
self.delivered_frames + self.discarded_frames - self.underrun_total
}
fn position_query_frame(&self) -> f64 {
(self.media_delivered_frames() as f64 - self.pipeline_latency_frames as f64).max(0.0)
}
fn apply_pending_control(&mut self) {
let mut saw_asap_tempo = false;
while let Some(event) = self.shared.pop_event() {
match event.param {
Param::TempoRate => {
if event.at_frame == crate::engine::control::APPLY_ASAP {
saw_asap_tempo = true;
} else if self.pending_retarget_len < MAX_PENDING_RETARGETS {
let mut i = self.pending_retarget_len;
while i > 0 && self.pending_retargets[i - 1].0 > event.at_frame {
self.pending_retargets[i] = self.pending_retargets[i - 1];
i -= 1;
}
self.pending_retargets[i] = (event.at_frame, event.value);
self.pending_retarget_len += 1;
} else {
saw_asap_tempo = true;
self.shared
.store_tempo_latest(clamp_tempo_rate(event.value));
}
}
Param::WarmStart => {
self.prime_source_frames = event.value.max(0.0);
self.discarded_frames = 0;
self.declick_remaining = 0;
}
}
}
if saw_asap_tempo {
let new_rate = clamp_tempo_rate(self.shared.tempo_latest());
if (new_rate - self.rate).abs() > MODULATION_HOLD_TRIGGER {
self.modulation_hold_blocks = MODULATION_HOLD_BLOCKS;
}
self.rate = new_rate;
}
if let Some(anchor) = self.ring.anchor.load() {
self.anchor = anchor;
}
}
fn due_retargets_and_cap(&mut self) -> usize {
while self.pending_retarget_len > 0 && self.pending_retargets[0].0 <= self.emitted_frames {
let (_, rate) = self.pending_retargets[0];
let rate = clamp_tempo_rate(rate);
if (rate - self.rate).abs() > MODULATION_HOLD_TRIGGER {
self.modulation_hold_blocks = MODULATION_HOLD_BLOCKS;
}
self.rate = rate;
self.pending_retargets
.copy_within(1..self.pending_retarget_len, 0);
self.pending_retarget_len -= 1;
}
if self.pending_retarget_len > 0 {
(self.pending_retargets[0].0 - self.emitted_frames) as usize
} else {
usize::MAX
}
}
fn render_chunk(&mut self, out: &mut [f32]) {
let needed_samples = out.len();
self.fill_out_fifo(needed_samples);
let popped = self.out_fifo.pop_slice(out);
if popped < needed_samples {
out[popped..].fill(0.0);
let missing_frames = ((needed_samples - popped) / self.channels) as u64;
self.underrun_total += missing_frames;
self.shared.add_underrun_frames(missing_frames);
}
if self.declick_remaining > 0 {
for frame in 0..needed_samples / self.channels {
if self.declick_remaining == 0 {
break;
}
let gain = 1.0 - self.declick_remaining as f32 / DECLICK_FRAMES as f32;
for ch in 0..self.channels {
out[frame * self.channels + ch] *= gain;
}
self.declick_remaining -= 1;
}
}
self.delivered_frames += (needed_samples / self.channels) as u64;
}
fn fill_out_fifo(&mut self, needed_samples: usize) {
let guard = needed_samples / FEED_CHUNK_FRAMES + needed_samples + 64;
for _ in 0..guard {
if self.out_fifo.len() >= needed_samples {
return;
}
if self.stages.is_empty() {
if !self.feed_once_into_out() {
return;
}
} else if !self.advance_stage_pipeline() {
return;
}
}
debug_assert!(false, "fill_out_fifo guard exhausted");
}
fn feed_once_into_out(&mut self) -> bool {
let produced = self.feed_varispeed_once();
match produced {
None => false,
Some(produced) => {
self.push_varispeed_output(produced, true);
true
}
}
}
fn advance_stage_pipeline(&mut self) -> bool {
let block_samples = BLOCK_FRAMES * self.channels;
if self.stage_fifo.len() < block_samples {
let produced = self.feed_varispeed_once();
match produced {
None => return false,
Some(produced) => self.push_varispeed_output(produced, false),
}
}
if self.stage_fifo.len() >= block_samples {
let popped = self
.stage_fifo
.pop_slice(&mut self.block_scratch[..block_samples]);
debug_assert_eq!(popped, block_samples);
self.block
.fill_deinterleaved(&self.block_scratch[..block_samples], BLOCK_FRAMES);
self.stage_in_frames += BLOCK_FRAMES as u64;
let mut events: [OnsetEvent; MAX_EVENTS] = [OnsetEvent::default(); MAX_EVENTS];
let mut event_count = 0usize;
if let Some(cursor) = self.transient.as_mut() {
let (anchor_ring, anchor_track) = self.anchor;
let ring_base = self.ring_frames_at_reset;
let timeline = &self.timeline;
let mapped = cursor.advance(self.stage_in_frames as f64, |track_frame| {
if track_frame < anchor_track {
return Some(f64::NEG_INFINITY);
}
let ring_frame = anchor_ring + (track_frame - anchor_track);
if ring_frame < ring_base {
return Some(f64::NEG_INFINITY);
}
let fed_source = (ring_frame - ring_base) as f64;
timeline.map_to_output(fed_source)
});
event_count = mapped.len();
events[..event_count].copy_from_slice(mapped);
}
if self.modulation_hold_blocks > 0 {
self.modulation_hold_blocks -= 1;
}
let consumed_pos = self.stage_in_frames as f64 - self.pipeline_latency_frames as f64;
let embedded_rate = self.timeline.rate_at(consumed_pos).unwrap_or(self.rate);
const SLOPE_WINDOW: f64 = RATE_HISTORY_MARGIN as f64;
let embedded_rate_slope = self
.timeline
.rate_at(consumed_pos - SLOPE_WINDOW)
.map(|earlier| (embedded_rate - earlier) / SLOPE_WINDOW)
.unwrap_or(0.0);
let ctx = StageCtx {
embedded_rate,
embedded_rate_slope,
onsets: &events[..event_count],
modulation_hold: self.modulation_hold_blocks > 0,
has_artifact: self.transient.is_some(),
};
for stage in &mut self.stages {
stage.process(&mut self.block, &ctx);
}
self.block
.write_interleaved(&mut self.block_scratch[..block_samples], BLOCK_FRAMES);
let pushed = self
.out_fifo
.push_slice(&self.block_scratch[..block_samples]);
debug_assert_eq!(pushed, block_samples);
}
true
}
fn feed_varispeed_once(&mut self) -> Option<usize> {
let cap = self.due_retargets_and_cap();
let popped = self.ring.pop_slice(&mut self.feed_scratch);
if popped == 0 {
return None;
}
debug_assert_eq!(popped % self.channels, 0);
let produced = self
.varispeed
.feed_capped(&self.feed_scratch[..popped], self.rate, cap);
self.emitted_frames += produced as u64;
self.timeline
.push(self.emitted_frames, self.varispeed.source_pos(), self.rate);
Some(produced)
}
fn push_varispeed_output(&mut self, produced: usize, direct_to_out: bool) {
if produced == 0 {
return;
}
let samples = produced * self.channels;
for ch in 0..self.channels {
let src = self.varispeed.output(ch);
for (f, &sample) in src.iter().enumerate().take(produced) {
self.interleave_scratch[f * self.channels + ch] = sample;
}
}
let fifo = if direct_to_out {
&mut self.out_fifo
} else {
&mut self.stage_fifo
};
let pushed = fifo.push_slice(&self.interleave_scratch[..samples]);
debug_assert_eq!(pushed, samples, "engine FIFO sized too small");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::{Engine, EngineConfig, EngineProfile};
fn sine(freq: f32, sample_rate: f32, frames: usize) -> Vec<f32> {
(0..frames)
.map(|i| (2.0 * std::f32::consts::PI * freq * i as f32 / sample_rate).sin())
.collect()
}
#[test]
fn tape_unity_is_exact_passthrough_from_frame_zero() {
let handles = Engine::build(EngineConfig {
channels: 1,
..EngineConfig::default()
})
.unwrap();
let (mut processor, mut source) = (handles.processor, handles.source);
let input = sine(440.0, 44_100.0, 8192);
source.push(&input);
let mut out = vec![0.0f32; 512];
let mut collected = Vec::new();
for _ in 0..8 {
processor.process(&mut out);
collected.extend_from_slice(&out);
}
assert_eq!(processor.pipeline_latency_frames(), 0);
for (i, (&got, &want)) in collected.iter().zip(input.iter()).enumerate() {
assert!(
(got - want).abs() < 1e-4,
"sample {i}: got {got}, want {want}"
);
}
}
#[test]
fn stereo_channels_stay_aligned() {
let handles = Engine::build(EngineConfig::default()).unwrap();
let (mut processor, mut source) = (handles.processor, handles.source);
let frames = 4096;
let mut interleaved = Vec::with_capacity(frames * 2);
let mono = sine(300.0, 44_100.0, frames);
for &s in &mono {
interleaved.push(s);
interleaved.push(-s);
}
source.push(&interleaved);
let mut out = vec![0.0f32; 256 * 2];
for _ in 0..8 {
processor.process(&mut out);
for frame in out.chunks(2) {
assert!(
(frame[0] + frame[1]).abs() < 1e-5,
"stereo misalignment: {frame:?}"
);
}
}
}
#[test]
fn underrun_fills_silence_and_counts_then_recovers() {
let handles = Engine::build(EngineConfig {
channels: 1,
..EngineConfig::default()
})
.unwrap();
let (controller, mut processor, mut source) =
(handles.controller, handles.processor, handles.source);
source.push(&vec![0.5f32; 100]);
let mut out = vec![1.0f32; 256];
processor.process(&mut out);
assert!(controller.underrun_frames() > 0);
assert_eq!(out[255], 0.0, "shortfall must be silence");
source.push(&vec![0.5f32; 4096]);
processor.process(&mut out);
assert!(out.iter().any(|&s| s != 0.0), "must recover after refill");
}
#[test]
fn odd_callback_sizes_fill_exactly() {
let handles = Engine::build(EngineConfig {
channels: 1,
..EngineConfig::default()
})
.unwrap();
let (mut processor, mut source) = (handles.processor, handles.source);
source.push(&sine(440.0, 44_100.0, 44_100));
for size in [64usize, 96, 100, 333, 1024, 1500, 4096] {
let mut out = vec![9.9f32; size];
processor.process(&mut out);
assert!(
out.iter().all(|s| s.is_finite() && s.abs() <= 1.0),
"bad output at callback size {size}"
);
}
}
struct Invert;
impl Stage for Invert {
fn process(&mut self, block: &mut BlockBuf, ctx: &StageCtx) {
assert!(ctx.embedded_rate > 0.0, "ctx must carry a usable rate");
for ch in 0..block.channels() {
for s in block.channel_mut(ch) {
*s = -*s;
}
}
}
fn latency_frames(&self) -> usize {
0
}
fn reset(&mut self) {}
}
#[test]
fn stage_chain_processes_fixed_blocks() {
let config = EngineConfig {
channels: 1,
profile: EngineProfile::Tape,
..EngineConfig::default()
};
let handles = Engine::build_with_stages(config, vec![Box::new(Invert)]).unwrap();
let (mut processor, mut source) = (handles.processor, handles.source);
let input = sine(440.0, 44_100.0, 8192);
source.push(&input);
let mut out = vec![0.0f32; 512];
let mut collected = Vec::new();
for _ in 0..8 {
processor.process(&mut out);
collected.extend_from_slice(&out);
}
for (i, (&got, &want)) in collected.iter().zip(input.iter()).enumerate() {
assert!(
(got + want).abs() < 1e-4,
"sample {i}: got {got}, want inverted {want}"
);
}
}
fn measure_freq(window: &[f32], sample_rate: f64) -> f64 {
let (mut first, mut last, mut count) = (None, None, 0usize);
for i in 1..window.len() {
let (a, b) = (window[i - 1] as f64, window[i] as f64);
if a <= 0.0 && b > 0.0 {
let t = (i - 1) as f64 + a / (a - b);
if first.is_none() {
first = Some(t);
}
last = Some(t);
count += 1;
}
}
match (first, last) {
(Some(f), Some(l)) if count >= 2 => (count - 1) as f64 * sample_rate / (l - f),
_ => 0.0,
}
}
fn run_profile_at_rate(
profile: EngineProfile,
freq: f32,
rate: f64,
seconds: usize,
) -> Vec<f32> {
let handles = Engine::build(EngineConfig {
channels: 1,
profile,
initial_tempo_rate: rate,
..EngineConfig::default()
})
.unwrap();
let (mut processor, mut source) = (handles.processor, handles.source);
let input = sine(freq, 44_100.0, 44_100 * (seconds + 1));
let mut feed = 0usize;
let mut out = vec![0.0f32; 256];
let mut collected = Vec::new();
for _ in 0..(44_100 * seconds / 256) {
while feed < input.len() && source.occupied_frames() < 8_192 {
let end = (feed + 8_192).min(input.len());
feed += source.push(&input[feed..end]);
}
processor.process(&mut out);
collected.extend_from_slice(&out);
}
collected
}
#[test]
fn keylock_profile_holds_pitch_while_tape_follows_tempo() {
let rate = 1.06;
let tape = run_profile_at_rate(EngineProfile::Tape, 440.0, rate, 3);
let keylock = run_profile_at_rate(EngineProfile::Keylock, 440.0, rate, 3);
let scan = 44_100..88_200;
let tape_freq = measure_freq(&tape[scan.clone()], 44_100.0);
let keylock_freq = measure_freq(&keylock[scan], 44_100.0);
assert!(
(tape_freq - 440.0 * rate).abs() < 2.0,
"tape pitch {tape_freq:.1} Hz should follow tempo"
);
let cents = 1200.0 * (keylock_freq / 440.0).log2();
assert!(
cents.abs() < 10.0,
"keylock pitch off by {cents:.1} cents ({keylock_freq:.2} Hz)"
);
}
#[test]
fn keylock_pipeline_latency_within_budget_and_reported_exactly() {
let handles = Engine::build(EngineConfig {
channels: 1,
profile: EngineProfile::Keylock,
..EngineConfig::default()
})
.unwrap();
let (mut processor, mut source) = (handles.processor, handles.source);
let latency = processor.pipeline_latency_frames();
assert!(
latency as f64 / 44_100.0 <= 0.015,
"keylock pipeline latency {latency} frames exceeds 15 ms"
);
let mut input = vec![0.0f32; 32_768];
for (i, s) in input.iter_mut().enumerate().skip(4_096) {
*s = 0.6 * (2.0 * std::f32::consts::PI * 900.0 * (i - 4_096) as f32 / 44_100.0).sin();
}
source.push(&input);
let mut out = vec![0.0f32; 16_384];
processor.process(&mut out);
let onset = out
.iter()
.position(|s| s.abs() > 1e-3)
.expect("onset must appear");
let expected = 4_096 + latency;
assert!(
(onset as i64 - expected as i64).abs() <= BLOCK_FRAMES as i64,
"onset at {onset}, expected {expected} (reported latency {latency})"
);
}
#[test]
fn warm_start_resumes_at_steady_level_immediately() {
let handles = Engine::build(EngineConfig {
channels: 1,
profile: EngineProfile::Keylock,
initial_tempo_rate: 1.05,
..EngineConfig::default()
})
.unwrap();
let (controller, mut processor, mut source) =
(handles.controller, handles.processor, handles.source);
let track = sine(500.0, 44_100.0, 44_100 * 6);
let mut feed = 0usize;
let mut out = vec![0.0f32; 256];
for _ in 0..64 {
while feed < track.len() && source.occupied_frames() < 8_192 {
let end = (feed + 8_192).min(track.len());
feed += source.push(&track[feed..end]);
}
processor.process(&mut out);
}
processor.reset();
let target = 44_100 * 3;
let preroll = processor.warm_start_preroll_frames();
source.set_track_position((target - preroll) as u64);
controller.warm_start(preroll as u32);
let mut feed = target - preroll;
let mut post_seek: Vec<f32> = Vec::new();
for _ in 0..64 {
while feed < track.len() && source.occupied_frames() < 8_192 {
let end = (feed + 8_192).min(track.len());
feed += source.push(&track[feed..end]);
}
processor.process(&mut out);
post_seek.extend_from_slice(&out);
}
let first_sound = post_seek
.iter()
.position(|s| s.abs() > 1e-4)
.expect("audio must resume after priming");
assert!(
first_sound < 256 * 8,
"priming took too long: first sound at {first_sound}"
);
let early = &post_seek[first_sound + 64..first_sound + 64 + 1_024];
let late = &post_seek[first_sound + 8_192..first_sound + 8_192 + 4_096];
let rms = |s: &[f32]| {
(s.iter().map(|&x| (x as f64) * (x as f64)).sum::<f64>() / s.len() as f64).sqrt()
};
let ratio = rms(early) / rms(late);
assert!(
(0.85..1.15).contains(&ratio),
"post-seek level not steady immediately: early/late RMS ratio {ratio:.3}"
);
let scan = &post_seek[first_sound + 64..];
let bound = 2.0 * std::f32::consts::PI * 500.0 * 1.05 / 44_100.0 * 1.5;
for (i, w) in scan.windows(2).enumerate() {
assert!(
(w[1] - w[0]).abs() <= bound,
"post-seek click at {i}: {}",
(w[1] - w[0]).abs()
);
}
}
#[test]
fn keylock_fades_to_varispeed_at_extreme_rates() {
let rate = 1.3; let out = run_profile_at_rate(EngineProfile::Keylock, 440.0, rate, 3);
let freq = measure_freq(&out[44_100..88_200], 44_100.0);
assert!(
(freq - 440.0 * rate).abs() < 3.0,
"extreme rate must be uncorrected varispeed: measured {freq:.1} Hz, \
expected {:.1}",
440.0 * rate
);
}
#[test]
fn keylock_gates_hold_at_other_sample_rates() {
for sample_rate in [48_000u32, 96_000] {
let handles = Engine::build(EngineConfig {
channels: 1,
profile: EngineProfile::Keylock,
sample_rate,
initial_tempo_rate: 1.06,
..EngineConfig::default()
})
.unwrap();
let (mut processor, mut source) = (handles.processor, handles.source);
let sr = sample_rate as f64;
let track: Vec<f32> = (0..(sr as usize * 4))
.map(|i| 0.6 * (2.0 * std::f64::consts::PI * 440.0 * i as f64 / sr).sin() as f32)
.collect();
let mut feed = 0usize;
let mut out = vec![0.0f32; 256];
let mut collected = Vec::new();
for _ in 0..(sr as usize * 3 / 256) {
while feed < track.len() && source.occupied_frames() < 8_192 {
let end = (feed + 8_192).min(track.len());
feed += source.push(&track[feed..end]);
}
processor.process(&mut out);
collected.extend_from_slice(&out);
}
let scan = &collected[sr as usize..sr as usize * 2];
let freq = measure_freq(scan, sr);
let cents = 1_200.0 * (freq / 440.0).log2();
assert!(
cents.abs() < 10.0,
"{sample_rate} Hz: keylock pitch off by {cents:.1} cents"
);
}
}
#[test]
fn fine_fader_steps_are_artifact_and_drift_free() {
let handles = Engine::build(EngineConfig {
channels: 1,
profile: EngineProfile::Keylock,
..EngineConfig::default()
})
.unwrap();
let (controller, mut processor, mut source) =
(handles.controller, handles.processor, handles.source);
let input = sine(700.0, 44_100.0, 44_100 * 6);
let mut feed = 0usize;
let mut out = vec![0.0f32; 256];
let mut collected = Vec::new();
let mut rate_integral = 0.0f64;
let mut last_rate = 1.0f64;
for cb in 0..(44_100 * 4 / 256) {
let rate = 1.0 + 0.0002 * ((cb % 400) as f64 - 200.0).abs() / 200.0 * 100.0;
last_rate = rate;
controller.set_tempo_rate(rate);
while feed < input.len() && source.occupied_frames() < 8_192 {
let end = (feed + 8_192).min(input.len());
feed += source.push(&input[feed..end]);
}
processor.process(&mut out);
collected.extend_from_slice(&out);
rate_integral += 256.0 * rate;
}
assert_eq!(controller.underrun_frames(), 0);
let bound = 2.0 * std::f32::consts::PI * 700.0 * 1.03 / 44_100.0 * 1.5;
for (i, w) in collected[16_384..].windows(2).enumerate() {
assert!(
(w[1] - w[0]).abs() <= bound,
"fine-step click at {i}: {}",
(w[1] - w[0]).abs()
);
}
let expected = rate_integral - processor.pipeline_latency_frames() as f64 * last_rate;
let got = controller.source_position();
assert!(
(got - expected).abs() < 320.0,
"source position drifted: {got:.0} vs integral {expected:.0}"
);
}
#[test]
fn loop_wrap_is_gapless_across_ratios() {
for rate in [0.92f64, 1.0, 1.08] {
let handles = Engine::build(EngineConfig {
channels: 1,
profile: EngineProfile::Keylock,
initial_tempo_rate: rate,
..EngineConfig::default()
})
.unwrap();
let (mut processor, mut source) = (handles.processor, handles.source);
let loop_len = 17_640usize;
let track = sine(500.0, 44_100.0, loop_len);
let mut cursor = 0usize;
let mut out = vec![0.0f32; 256];
let mut collected = Vec::new();
source.set_track_position(0);
for _ in 0..512 {
while source.occupied_frames() < 8_192 {
let end = (cursor + 4_096).min(track.len());
cursor += source.push(&track[cursor..end]);
if cursor >= track.len() {
cursor = 0;
source.set_track_position(0);
}
}
processor.process(&mut out);
collected.extend_from_slice(&out);
}
let scan = &collected[16_384..];
let bound = 2.0 * std::f32::consts::PI * 500.0 * rate.max(1.0) as f32 / 44_100.0 * 1.5;
let mut worst = 0.0f32;
for w in scan.windows(2) {
worst = worst.max((w[1] - w[0]).abs());
}
assert!(
worst <= bound,
"rate {rate}: loop seam click {worst:.5} > {bound:.5}"
);
}
}
#[test]
fn source_position_tracks_delivery_at_constant_rate() {
let handles = Engine::build(EngineConfig {
channels: 1,
initial_tempo_rate: 1.25,
..EngineConfig::default()
})
.unwrap();
let (controller, mut processor, mut source) =
(handles.controller, handles.processor, handles.source);
source.push(&vec![0.25f32; 32_768]);
let mut out = vec![0.0f32; 256];
let mut delivered = 0u64;
for _ in 0..32 {
processor.process(&mut out);
delivered += 256;
}
let expected = delivered as f64 * 1.25;
let got = controller.source_position();
assert!(
(got - expected).abs() <= 32.0 * 1.25 + 1.0,
"source position {got} not within one feed chunk of {expected}"
);
assert_eq!(controller.delivered_frames(), delivered);
}
}