Fix: simpler scheduling

This commit is contained in:
2026-02-03 15:55:43 +01:00
parent 266a625cf3
commit 3284354f40
4 changed files with 125 additions and 103 deletions

View File

@@ -7,7 +7,7 @@ use std::sync::atomic::{AtomicI64, AtomicU64};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use super::dispatcher::{dispatcher_loop, DispatchCommand, TimedCommand};
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 crate::model::{
@@ -299,8 +299,8 @@ pub fn spawn_sequencer(
let audio_tx = Arc::new(ArcSwap::from_pointee(audio_tx));
let midi_tx = Arc::new(ArcSwap::from_pointee(midi_tx));
// Dispatcher channel (unbounded to avoid blocking the scheduler)
let (dispatch_tx, dispatch_rx) = unbounded::<TimedCommand>();
// Dispatcher channel — MIDI only (unbounded to avoid blocking the scheduler)
let (dispatch_tx, dispatch_rx) = unbounded::<TimedMidiCommand>();
let shared_state = Arc::new(ArcSwap::from_pointee(SharedSequencerState::default()));
let shared_state_clone = Arc::clone(&shared_state);
@@ -312,28 +312,24 @@ pub fn spawn_sequencer(
#[cfg(feature = "desktop")]
let mouse_down = config.mouse_down;
// Spawn dispatcher thread
// Spawn dispatcher thread (MIDI only — audio goes direct to doux)
let dispatcher_link = Arc::clone(&link);
let dispatcher_audio_tx = Arc::clone(&audio_tx);
let dispatcher_midi_tx = Arc::clone(&midi_tx);
thread::Builder::new()
.name("cagire-dispatcher".into())
.spawn(move || {
dispatcher_loop(
dispatch_rx,
dispatcher_audio_tx,
dispatcher_midi_tx,
dispatcher_link,
);
dispatcher_loop(dispatch_rx, dispatcher_midi_tx, dispatcher_link);
})
.expect("Failed to spawn dispatcher thread");
let sequencer_audio_tx = Arc::clone(&audio_tx);
let thread = thread::Builder::new()
.name("sequencer".into())
.spawn(move || {
sequencer_loop(
cmd_rx,
dispatch_tx,
sequencer_audio_tx,
link,
playing,
variables,
@@ -830,11 +826,24 @@ impl SequencerState {
.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 _ in 0..steps_to_fire {
for step_offset in 0..steps_to_fire {
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
} else {
0.0
};
let event_time = Some(engine_time + lookahead_secs + time_delta);
if let Some(step) = pattern.steps.get(step_idx) {
let resolved_script = pattern.resolve_script(step_idx);
let has_script = resolved_script
@@ -887,12 +896,6 @@ impl SequencerState {
std::mem::take(&mut trace),
);
let event_time = if lookahead_secs > 0.0 {
Some(engine_time + lookahead_secs)
} else {
None
};
for cmd in cmds {
self.event_count += 1;
self.buf_audio_commands.push(TimestampedCommand {
@@ -932,7 +935,9 @@ impl SequencerState {
}
let vars = self.variables.load();
let new_tempo = vars.get("__tempo__").and_then(|v: &Value| v.as_float().ok());
let new_tempo = vars
.get("__tempo__")
.and_then(|v: &Value| v.as_float().ok());
let mut chain_transitions = Vec::new();
for id in completed {
@@ -1030,7 +1035,8 @@ impl SequencerState {
#[allow(clippy::too_many_arguments)]
fn sequencer_loop(
cmd_rx: Receiver<SeqCommand>,
dispatch_tx: Sender<TimedCommand>,
dispatch_tx: Sender<TimedMidiCommand>,
audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
link: Arc<LinkState>,
playing: Arc<std::sync::atomic::AtomicBool>,
variables: Variables,
@@ -1109,16 +1115,14 @@ fn sequencer_loop(
let output = seq_state.tick(input);
// Dispatch commands via the dispatcher thread
// Route commands: audio direct to doux, MIDI through dispatcher
for tsc in output.audio_commands {
if let Some((midi_cmd, dur)) = parse_midi_command(&tsc.cmd) {
// Queue MIDI command for immediate dispatch
let _ = dispatch_tx.send(TimedCommand {
command: DispatchCommand::Midi(midi_cmd.clone()),
let _ = dispatch_tx.send(TimedMidiCommand {
command: MidiDispatch::Send(midi_cmd.clone()),
target_time_us: current_time_us,
});
// Schedule note-off if duration specified
if let (
MidiCommand::NoteOn {
device,
@@ -1130,8 +1134,8 @@ fn sequencer_loop(
) = (&midi_cmd, dur)
{
let off_time_us = current_time_us + (dur_secs * 1_000_000.0) as SyncTime;
let _ = dispatch_tx.send(TimedCommand {
command: DispatchCommand::Midi(MidiCommand::NoteOff {
let _ = dispatch_tx.send(TimedMidiCommand {
command: MidiDispatch::Send(MidiCommand::NoteOff {
device: *device,
channel: *channel,
note: *note,
@@ -1140,21 +1144,17 @@ fn sequencer_loop(
});
}
} else {
// Queue audio command
let _ = dispatch_tx.send(TimedCommand {
command: DispatchCommand::Audio {
cmd: tsc.cmd,
time: tsc.time,
},
target_time_us: current_time_us,
// Audio direct to doux — sample-accurate scheduling via /time/ parameter
let _ = audio_tx.load().try_send(AudioCommand::Evaluate {
cmd: tsc.cmd,
time: tsc.time,
});
}
}
// Handle MIDI flush request
if output.flush_midi_notes {
let _ = dispatch_tx.send(TimedCommand {
command: DispatchCommand::FlushMidi,
let _ = dispatch_tx.send(TimedMidiCommand {
command: MidiDispatch::FlushAll,
target_time_us: current_time_us,
});
}
@@ -1201,7 +1201,6 @@ fn sequencer_loop(
/// 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)