Fix: simpler scheduling
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user