Feat: really good lookahead mechanism for scheduling
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s

This commit is contained in:
2026-02-04 20:28:42 +01:00
parent 767575b25d
commit d40d713649
5 changed files with 128 additions and 122 deletions

View File

@@ -11,8 +11,8 @@ use std::sync::Arc;
use std::thread::{self, JoinHandle};
use super::dispatcher::{dispatcher_loop, MidiDispatch, TimedMidiCommand};
use super::realtime::{precise_sleep_us, set_realtime_priority};
use super::{micros_until_next_substep, substeps_crossed, LinkState, StepTiming, SyncTime};
use super::realtime::set_realtime_priority;
use super::{substeps_in_window, LinkState, StepTiming, SyncTime};
use crate::model::{
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables,
};
@@ -450,6 +450,7 @@ pub(crate) struct TickInput {
pub commands: Vec<SeqCommand>,
pub playing: bool,
pub beat: f64,
pub lookahead_end: f64,
pub tempo: f64,
pub quantum: f64,
pub fill: bool,
@@ -682,15 +683,16 @@ impl SequencerState {
return self.tick_paused();
}
let beat = input.beat;
let prev_beat = self.audio_state.prev_beat;
let frontier = self.audio_state.prev_beat;
let lookahead_end = input.lookahead_end;
self.activate_pending(beat, prev_beat, input.quantum);
self.deactivate_pending(beat, prev_beat, input.quantum);
self.activate_pending(lookahead_end, frontier, input.quantum);
self.deactivate_pending(lookahead_end, frontier, input.quantum);
let steps = self.execute_steps(
beat,
prev_beat,
input.beat,
frontier,
lookahead_end,
input.tempo,
input.quantum,
input.fill,
@@ -708,7 +710,7 @@ impl SequencerState {
let vars = self.read_variables(&self.buf_completed_iterations, steps.any_step_fired);
self.apply_chain_transitions(vars.chain_transitions);
self.audio_state.prev_beat = beat;
self.audio_state.prev_beat = lookahead_end;
let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
TickOutput {
@@ -805,7 +807,8 @@ impl SequencerState {
fn execute_steps(
&mut self,
beat: f64,
prev_beat: f64,
frontier: f64,
lookahead_end: f64,
tempo: f64,
quantum: f64,
fill: bool,
@@ -843,17 +846,13 @@ impl SequencerState {
.get(&(active.bank, active.pattern))
.copied()
.unwrap_or_else(|| pattern.speed.multiplier());
let steps_to_fire = substeps_crossed(prev_beat, beat, speed_mult);
let substeps_per_beat = 4.0 * speed_mult;
let base_substep = (prev_beat * substeps_per_beat).floor() as i64;
for step_offset in 0..steps_to_fire {
let step_beats = substeps_in_window(frontier, lookahead_end, speed_mult);
for step_beat in step_beats {
result.any_step_fired = true;
let step_idx = active.step_index % pattern.length;
// Per-step timing: each step gets its exact beat position
let step_substep = base_substep + step_offset as i64 + 1;
let step_beat = step_substep as f64 / substeps_per_beat;
let beat_delta = step_beat - beat;
let time_delta = if tempo > 0.0 {
(beat_delta / tempo) * 60.0
@@ -882,11 +881,11 @@ impl SequencerState {
);
let ctx = StepContext {
step: step_idx,
beat,
beat: step_beat,
bank: active.bank,
pattern: active.pattern,
tempo,
phase: beat % quantum,
phase: step_beat % quantum,
slot: 0,
runs,
iter: active.iter,
@@ -1077,26 +1076,31 @@ fn sequencer_loop(
) {
use std::sync::atomic::Ordering;
let has_rt = set_realtime_priority();
#[cfg(target_os = "linux")]
if !has_rt {
eprintln!("[cagire] Warning: Could not set realtime priority for sequencer thread.");
eprintln!("[cagire] For best performance on Linux, configure rtprio limits:");
eprintln!("[cagire] Add your user to 'audio' group: sudo usermod -aG audio $USER");
eprintln!("[cagire] Edit /etc/security/limits.conf and add:");
eprintln!("[cagire] @audio - rtprio 95");
eprintln!("[cagire] @audio - memlock unlimited");
eprintln!("[cagire] Then log out and back in.");
}
set_realtime_priority();
let variables: Variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
let dict: Dictionary = Arc::new(Mutex::new(HashMap::new()));
let rng: Rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
let mut seq_state = SequencerState::new(variables, dict, rng, cc_access);
// Lookahead window: ~20ms expressed in beats, recomputed each tick
const LOOKAHEAD_SECS: f64 = 0.02;
// Wake cadence: how long to sleep between scheduling passes
const WAKE_INTERVAL: std::time::Duration = std::time::Duration::from_millis(3);
loop {
// Drain all pending commands, also serves as the sleep mechanism
let mut commands = Vec::with_capacity(8);
match cmd_rx.recv_timeout(WAKE_INTERVAL) {
Ok(cmd) => {
if matches!(cmd, SeqCommand::Shutdown) {
return;
}
commands.push(cmd);
}
Err(crossbeam_channel::RecvTimeoutError::Disconnected) => return,
Err(crossbeam_channel::RecvTimeoutError::Timeout) => {}
}
while let Ok(cmd) = cmd_rx.try_recv() {
if matches!(cmd, SeqCommand::Shutdown) {
return;
@@ -1109,6 +1113,13 @@ fn sequencer_loop(
let beat = state.beat_at_time(current_time_us as i64, quantum);
let tempo = state.tempo();
let lookahead_beats = if tempo > 0.0 {
LOOKAHEAD_SECS * tempo / 60.0
} else {
0.0
};
let lookahead_end = beat + lookahead_beats;
let sr = sample_rate.load(Ordering::Relaxed) as f64;
let audio_samples = audio_sample_pos.load(Ordering::Acquire);
let engine_time = if sr > 0.0 {
@@ -1120,6 +1131,7 @@ fn sequencer_loop(
commands,
playing: playing.load(Ordering::Relaxed),
beat,
lookahead_end,
tempo,
quantum,
fill: live_keys.fill(),
@@ -1165,7 +1177,6 @@ fn sequencer_loop(
});
}
} else {
// Audio direct to doux — sample-accurate scheduling via /time/ parameter
let _ = audio_tx.load().send(AudioCommand::Evaluate {
cmd: tsc.cmd,
time: tsc.time,
@@ -1185,63 +1196,6 @@ fn sequencer_loop(
}
shared_state.store(Arc::new(output.shared_state));
// Calculate time until next substep based on active patterns
let next_event_us = {
let mut min_micros = SyncTime::MAX;
for id in seq_state.audio_state.active_patterns.keys() {
let speed = seq_state
.speed_overrides
.get(&(id.bank, id.pattern))
.copied()
.or_else(|| {
seq_state
.pattern_cache
.get(id.bank, id.pattern)
.map(|p| p.speed.multiplier())
})
.unwrap_or(1.0);
let micros = micros_until_next_substep(beat, speed, tempo);
min_micros = min_micros.min(micros);
}
// If no active patterns, default to 1ms for command responsiveness
if min_micros == SyncTime::MAX {
1000
} else {
min_micros.max(50) // Minimum 50μs to prevent excessive CPU usage
}
};
let target_time_us = current_time_us + next_event_us;
wait_until(target_time_us, &link, has_rt);
}
}
/// Spin-wait threshold in microseconds. With RT priority, we sleep most of the
/// wait time and spin-wait for the final portion for precision. Without RT priority,
/// spinning is counterproductive and we sleep the entire duration.
const SPIN_THRESHOLD_US: SyncTime = 100;
/// Two-phase wait: sleep most of the time, optionally spin-wait for final precision.
/// With RT priority: sleep + spin for precision
/// Without RT priority: sleep only (spinning wastes CPU without benefit)
fn wait_until(target_us: SyncTime, link: &LinkState, has_rt_priority: bool) {
let current = link.clock_micros() as SyncTime;
let remaining = target_us.saturating_sub(current);
if has_rt_priority {
// With RT priority: sleep most, spin for final precision
if remaining > SPIN_THRESHOLD_US {
precise_sleep_us(remaining - SPIN_THRESHOLD_US);
}
while (link.clock_micros() as SyncTime) < target_us {
std::hint::spin_loop();
}
} else {
// Without RT priority: sleep the entire time (spin-waiting is counterproductive)
if remaining > 0 {
precise_sleep_us(remaining);
}
}
}
@@ -1390,6 +1344,7 @@ mod tests {
commands: Vec::new(),
playing,
beat,
lookahead_end: beat,
tempo: 120.0,
quantum: 4.0,
fill: false,
@@ -1410,6 +1365,7 @@ mod tests {
commands,
playing: true,
beat,
lookahead_end: beat,
tempo: 120.0,
quantum: 4.0,
fill: false,