Fix: simpler scheduling
This commit is contained in:
@@ -7,52 +7,50 @@ use std::time::Duration;
|
||||
|
||||
use super::link::LinkState;
|
||||
use super::realtime::{precise_sleep_us, set_realtime_priority};
|
||||
use super::sequencer::{AudioCommand, MidiCommand};
|
||||
use super::sequencer::MidiCommand;
|
||||
use super::timing::SyncTime;
|
||||
|
||||
/// A command scheduled for dispatch at a specific time.
|
||||
/// A MIDI command scheduled for dispatch at a specific time.
|
||||
#[derive(Clone)]
|
||||
pub struct TimedCommand {
|
||||
pub command: DispatchCommand,
|
||||
pub struct TimedMidiCommand {
|
||||
pub command: MidiDispatch,
|
||||
pub target_time_us: SyncTime,
|
||||
}
|
||||
|
||||
/// Commands the dispatcher can send to audio/MIDI threads.
|
||||
/// MIDI commands the dispatcher can send.
|
||||
#[derive(Clone)]
|
||||
pub enum DispatchCommand {
|
||||
Audio { cmd: String, time: Option<f64> },
|
||||
Midi(MidiCommand),
|
||||
FlushMidi,
|
||||
pub enum MidiDispatch {
|
||||
Send(MidiCommand),
|
||||
FlushAll,
|
||||
}
|
||||
|
||||
impl Ord for TimedCommand {
|
||||
impl Ord for TimedMidiCommand {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
// Reverse ordering for min-heap (earliest time first)
|
||||
other.target_time_us.cmp(&self.target_time_us)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for TimedCommand {
|
||||
impl PartialOrd for TimedMidiCommand {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for TimedCommand {
|
||||
impl PartialEq for TimedMidiCommand {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.target_time_us == other.target_time_us
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for TimedCommand {}
|
||||
impl Eq for TimedMidiCommand {}
|
||||
|
||||
/// Spin-wait threshold in microseconds for dispatcher.
|
||||
const SPIN_THRESHOLD_US: SyncTime = 100;
|
||||
|
||||
/// Main dispatcher loop - receives timed commands and dispatches them at the right moment.
|
||||
/// Dispatcher loop — handles MIDI timing only.
|
||||
/// Audio commands bypass the dispatcher entirely and go straight to doux's
|
||||
/// sample-accurate scheduler via the audio thread channel.
|
||||
pub fn dispatcher_loop(
|
||||
cmd_rx: Receiver<TimedCommand>,
|
||||
audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
|
||||
cmd_rx: Receiver<TimedMidiCommand>,
|
||||
midi_tx: Arc<ArcSwap<Sender<MidiCommand>>>,
|
||||
link: Arc<LinkState>,
|
||||
) {
|
||||
@@ -63,37 +61,33 @@ pub fn dispatcher_loop(
|
||||
eprintln!("[cagire] Warning: Could not set realtime priority for dispatcher thread.");
|
||||
}
|
||||
|
||||
let mut queue: BinaryHeap<TimedCommand> = BinaryHeap::with_capacity(256);
|
||||
let mut queue: BinaryHeap<TimedMidiCommand> = BinaryHeap::with_capacity(256);
|
||||
|
||||
loop {
|
||||
let current_us = link.clock_micros() as SyncTime;
|
||||
|
||||
// Calculate timeout based on next queued event
|
||||
let timeout_us = queue
|
||||
.peek()
|
||||
.map(|cmd| cmd.target_time_us.saturating_sub(current_us))
|
||||
.unwrap_or(100_000) // 100ms default when idle
|
||||
.max(100); // Minimum 100μs to prevent busy-looping
|
||||
.unwrap_or(100_000)
|
||||
.max(100);
|
||||
|
||||
// Receive new commands (with timeout)
|
||||
match cmd_rx.recv_timeout(Duration::from_micros(timeout_us)) {
|
||||
Ok(cmd) => queue.push(cmd),
|
||||
Err(RecvTimeoutError::Timeout) => {}
|
||||
Err(RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
|
||||
// Drain any additional pending commands
|
||||
while let Ok(cmd) = cmd_rx.try_recv() {
|
||||
queue.push(cmd);
|
||||
}
|
||||
|
||||
// Dispatch ready commands
|
||||
let current_us = link.clock_micros() as SyncTime;
|
||||
while let Some(cmd) = queue.peek() {
|
||||
if cmd.target_time_us <= current_us + SPIN_THRESHOLD_US {
|
||||
let cmd = queue.pop().unwrap();
|
||||
wait_until_dispatch(cmd.target_time_us, &link, has_rt);
|
||||
dispatch_command(cmd.command, &audio_tx, &midi_tx);
|
||||
dispatch_midi(cmd.command, &midi_tx);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -101,44 +95,25 @@ pub fn dispatcher_loop(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Wait until the target time for dispatch.
|
||||
/// With RT priority: spin-wait for precision
|
||||
/// Without RT priority: sleep (spinning wastes CPU without benefit)
|
||||
fn wait_until_dispatch(target_us: SyncTime, link: &LinkState, has_rt: bool) {
|
||||
let current = link.clock_micros() as SyncTime;
|
||||
let remaining = target_us.saturating_sub(current);
|
||||
|
||||
if has_rt {
|
||||
// With RT priority: spin-wait for precise timing
|
||||
while (link.clock_micros() as SyncTime) < target_us {
|
||||
std::hint::spin_loop();
|
||||
}
|
||||
} else {
|
||||
// Without RT priority: sleep (spin-waiting is counterproductive)
|
||||
if remaining > 0 {
|
||||
precise_sleep_us(remaining);
|
||||
}
|
||||
} else if remaining > 0 {
|
||||
precise_sleep_us(remaining);
|
||||
}
|
||||
}
|
||||
|
||||
/// Route a command to the appropriate output channel.
|
||||
fn dispatch_command(
|
||||
cmd: DispatchCommand,
|
||||
audio_tx: &Arc<ArcSwap<Sender<AudioCommand>>>,
|
||||
midi_tx: &Arc<ArcSwap<Sender<MidiCommand>>>,
|
||||
) {
|
||||
fn dispatch_midi(cmd: MidiDispatch, midi_tx: &Arc<ArcSwap<Sender<MidiCommand>>>) {
|
||||
match cmd {
|
||||
DispatchCommand::Audio { cmd, time } => {
|
||||
let _ = audio_tx
|
||||
.load()
|
||||
.try_send(AudioCommand::Evaluate { cmd, time });
|
||||
}
|
||||
DispatchCommand::Midi(midi_cmd) => {
|
||||
MidiDispatch::Send(midi_cmd) => {
|
||||
let _ = midi_tx.load().try_send(midi_cmd);
|
||||
}
|
||||
DispatchCommand::FlushMidi => {
|
||||
// Send All Notes Off (CC 123) on all 16 channels for all 4 devices
|
||||
MidiDispatch::FlushAll => {
|
||||
for dev in 0..4u8 {
|
||||
for chan in 0..16u8 {
|
||||
let _ = midi_tx.load().try_send(MidiCommand::CC {
|
||||
@@ -159,22 +134,21 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_timed_command_ordering() {
|
||||
let mut heap: BinaryHeap<TimedCommand> = BinaryHeap::new();
|
||||
let mut heap: BinaryHeap<TimedMidiCommand> = BinaryHeap::new();
|
||||
|
||||
heap.push(TimedCommand {
|
||||
command: DispatchCommand::FlushMidi,
|
||||
heap.push(TimedMidiCommand {
|
||||
command: MidiDispatch::FlushAll,
|
||||
target_time_us: 300,
|
||||
});
|
||||
heap.push(TimedCommand {
|
||||
command: DispatchCommand::FlushMidi,
|
||||
heap.push(TimedMidiCommand {
|
||||
command: MidiDispatch::FlushAll,
|
||||
target_time_us: 100,
|
||||
});
|
||||
heap.push(TimedCommand {
|
||||
command: DispatchCommand::FlushMidi,
|
||||
heap.push(TimedMidiCommand {
|
||||
command: MidiDispatch::FlushAll,
|
||||
target_time_us: 200,
|
||||
});
|
||||
|
||||
// Min-heap: earliest time should come out first
|
||||
assert_eq!(heap.pop().unwrap().target_time_us, 100);
|
||||
assert_eq!(heap.pop().unwrap().target_time_us, 200);
|
||||
assert_eq!(heap.pop().unwrap().target_time_us, 300);
|
||||
|
||||
Reference in New Issue
Block a user