WIP: not sure

This commit is contained in:
2026-02-03 02:31:55 +01:00
parent 33ee1822a5
commit b305df3d79
7 changed files with 547 additions and 159 deletions

View File

@@ -1,5 +1,5 @@
use arc_swap::ArcSwap;
use crossbeam_channel::{bounded, Receiver, Sender, TrySendError};
use crossbeam_channel::{bounded, unbounded, Receiver, Sender};
use std::collections::HashMap;
#[cfg(feature = "desktop")]
use std::sync::atomic::AtomicU32;
@@ -7,11 +7,15 @@ use std::sync::atomic::{AtomicI64, AtomicU64};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::Duration;
use thread_priority::ThreadPriority;
#[cfg(not(unix))]
use thread_priority::set_current_thread_priority;
use thread_priority::ThreadPriority;
use super::LinkState;
use super::dispatcher::{dispatcher_loop, DispatchCommand, TimedCommand};
use super::{
micros_until_next_substep, substeps_crossed, LinkState, StepTiming, SyncTime,
ACTIVE_WAIT_THRESHOLD_US,
};
use crate::model::{
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables,
};
@@ -301,10 +305,11 @@ 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>();
let shared_state = Arc::new(ArcSwap::from_pointee(SharedSequencerState::default()));
let shared_state_clone = Arc::clone(&shared_state);
let audio_tx_for_thread = Arc::clone(&audio_tx);
let midi_tx_for_thread = Arc::clone(&midi_tx);
#[cfg(feature = "desktop")]
let mouse_x = config.mouse_x;
@@ -313,13 +318,28 @@ pub fn spawn_sequencer(
#[cfg(feature = "desktop")]
let mouse_down = config.mouse_down;
// Spawn dispatcher thread
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,
);
})
.expect("Failed to spawn dispatcher thread");
let thread = thread::Builder::new()
.name("sequencer".into())
.spawn(move || {
sequencer_loop(
cmd_rx,
audio_tx_for_thread,
midi_tx_for_thread,
dispatch_tx,
link,
playing,
variables,
@@ -407,32 +427,13 @@ fn check_quantization_boundary(
prev_beat: f64,
quantum: f64,
) -> bool {
if prev_beat < 0.0 {
return false;
}
match quantization {
LaunchQuantization::Immediate => true,
LaunchQuantization::Beat => beat.floor() as i64 != prev_beat.floor() as i64,
LaunchQuantization::Bar => {
let bar = (beat / quantum).floor() as i64;
let prev_bar = (prev_beat / quantum).floor() as i64;
bar != prev_bar
}
LaunchQuantization::Bars2 => {
let bars2 = (beat / (quantum * 2.0)).floor() as i64;
let prev_bars2 = (prev_beat / (quantum * 2.0)).floor() as i64;
bars2 != prev_bars2
}
LaunchQuantization::Bars4 => {
let bars4 = (beat / (quantum * 4.0)).floor() as i64;
let prev_bars4 = (prev_beat / (quantum * 4.0)).floor() as i64;
bars4 != prev_bars4
}
LaunchQuantization::Bars8 => {
let bars8 = (beat / (quantum * 8.0)).floor() as i64;
let prev_bars8 = (prev_beat / (quantum * 8.0)).floor() as i64;
bars8 != prev_bars8
}
LaunchQuantization::Immediate => prev_beat >= 0.0,
LaunchQuantization::Beat => StepTiming::NextBeat.crossed(prev_beat, beat, quantum),
LaunchQuantization::Bar => StepTiming::NextBar.crossed(prev_beat, beat, quantum),
LaunchQuantization::Bars2 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 2.0),
LaunchQuantization::Bars4 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 4.0),
LaunchQuantization::Bars8 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 8.0),
}
}
@@ -458,7 +459,8 @@ impl RunsCounter {
}
fn clear_pattern(&mut self, bank: usize, pattern: usize) {
self.counts.retain(|&(b, p, _), _| b != bank || p != pattern);
self.counts
.retain(|&(b, p, _), _| b != bank || p != pattern);
}
}
@@ -470,7 +472,7 @@ pub(crate) struct TickInput {
pub quantum: f64,
pub fill: bool,
pub nudge_secs: f64,
pub current_time_us: i64,
pub current_time_us: SyncTime,
pub engine_time: f64,
pub lookahead_secs: f64,
#[cfg(feature = "desktop")]
@@ -536,12 +538,6 @@ impl KeyCache {
}
}
#[derive(Clone, Copy)]
struct ActiveNote {
off_time_us: i64,
start_time_us: i64,
}
pub(crate) struct SequencerState {
audio_state: AudioState,
pattern_cache: PatternCache,
@@ -558,7 +554,6 @@ pub(crate) struct SequencerState {
buf_stopped: Vec<PatternId>,
buf_completed_iterations: Vec<PatternId>,
cc_access: Option<Arc<dyn CcAccess>>,
active_notes: HashMap<(u8, u8, u8), ActiveNote>,
muted: std::collections::HashSet<(usize, usize)>,
soloed: std::collections::HashSet<(usize, usize)>,
}
@@ -587,7 +582,6 @@ impl SequencerState {
buf_stopped: Vec::with_capacity(16),
buf_completed_iterations: Vec::with_capacity(16),
cc_access,
active_notes: HashMap::new(),
muted: std::collections::HashSet::new(),
soloed: std::collections::HashSet::new(),
}
@@ -761,7 +755,8 @@ impl SequencerState {
}
}
};
self.runs_counter.clear_pattern(pending.id.bank, pending.id.pattern);
self.runs_counter
.clear_pattern(pending.id.bank, pending.id.pattern);
self.audio_state.active_patterns.insert(
pending.id,
ActivePattern {
@@ -806,7 +801,7 @@ impl SequencerState {
quantum: f64,
fill: bool,
nudge_secs: f64,
_current_time_us: i64,
_current_time_us: SyncTime,
engine_time: f64,
lookahead_secs: f64,
#[cfg(feature = "desktop")] mouse_x: f64,
@@ -840,15 +835,7 @@ impl SequencerState {
.get(&(active.bank, active.pattern))
.copied()
.unwrap_or_else(|| pattern.speed.multiplier());
let beat_int = (beat * 4.0 * speed_mult).floor() as i64;
let prev_beat_int = (prev_beat * 4.0 * speed_mult).floor() as i64;
// Fire ALL skipped steps when scheduler jitter causes us to miss beats
let steps_to_fire = if prev_beat >= 0.0 {
(beat_int - prev_beat_int).clamp(0, 16) as usize
} else {
0
};
let steps_to_fire = substeps_crossed(prev_beat, beat, speed_mult);
for _ in 0..steps_to_fire {
result.any_step_fired = true;
@@ -863,8 +850,7 @@ impl SequencerState {
if step.active && has_script {
let pattern_key = (active.bank, active.pattern);
let is_muted = self.muted.contains(&pattern_key)
|| (!self.soloed.is_empty()
&& !self.soloed.contains(&pattern_key));
|| (!self.soloed.is_empty() && !self.soloed.contains(&pattern_key));
if !is_muted {
let source_idx = pattern.resolve_source(step_idx);
@@ -941,11 +927,7 @@ impl SequencerState {
result
}
fn read_variables(
&self,
completed: &[PatternId],
any_step_fired: bool,
) -> VariableReads {
fn read_variables(&self, completed: &[PatternId], any_step_fired: bool) -> VariableReads {
let stopped = &self.buf_stopped;
let needs_access = !completed.is_empty() || !stopped.is_empty() || any_step_fired;
if !needs_access {
@@ -1037,8 +1019,7 @@ impl SequencerState {
#[allow(clippy::too_many_arguments)]
fn sequencer_loop(
cmd_rx: Receiver<SeqCommand>,
audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
midi_tx: Arc<ArcSwap<Sender<MidiCommand>>>,
dispatch_tx: Sender<TimedCommand>,
link: Arc<LinkState>,
playing: Arc<std::sync::atomic::AtomicBool>,
variables: Variables,
@@ -1105,8 +1086,8 @@ fn sequencer_loop(
}
let state = link.capture_app_state();
let current_time_us = link.clock_micros();
let beat = state.beat_at_time(current_time_us, quantum);
let current_time_us = link.clock_micros() as SyncTime;
let beat = state.beat_at_time(current_time_us as i64, quantum);
let tempo = state.tempo();
let sr = sample_rate.load(Ordering::Relaxed) as f64;
@@ -1139,87 +1120,54 @@ fn sequencer_loop(
let output = seq_state.tick(input);
// Dispatch commands via the dispatcher thread
for tsc in output.audio_commands {
if let Some((midi_cmd, dur)) = parse_midi_command(&tsc.cmd) {
match midi_tx.load().try_send(midi_cmd.clone()) {
Ok(()) => {
if let (
MidiCommand::NoteOn {
device,
channel,
note,
..
},
Some(dur_secs),
) = (&midi_cmd, dur)
{
let dur_us = (dur_secs * 1_000_000.0) as i64;
seq_state.active_notes.insert(
(*device, *channel, *note),
ActiveNote {
off_time_us: current_time_us + dur_us,
start_time_us: current_time_us,
},
);
}
}
Err(TrySendError::Full(_) | TrySendError::Disconnected(_)) => {
seq_state.dropped_events += 1;
}
// Queue MIDI command for immediate dispatch
let _ = dispatch_tx.send(TimedCommand {
command: DispatchCommand::Midi(midi_cmd.clone()),
target_time_us: current_time_us,
});
// Schedule note-off if duration specified
if let (
MidiCommand::NoteOn {
device,
channel,
note,
..
},
Some(dur_secs),
) = (&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 {
device: *device,
channel: *channel,
note: *note,
}),
target_time_us: off_time_us,
});
}
} else {
let cmd = AudioCommand::Evaluate {
cmd: tsc.cmd,
time: tsc.time,
};
match audio_tx.load().try_send(cmd) {
Ok(()) => {}
Err(TrySendError::Full(_) | TrySendError::Disconnected(_)) => {
seq_state.dropped_events += 1;
}
}
// Queue audio command
let _ = dispatch_tx.send(TimedCommand {
command: DispatchCommand::Audio {
cmd: tsc.cmd,
time: tsc.time,
},
target_time_us: current_time_us,
});
}
}
const MAX_NOTE_DURATION_US: i64 = 30_000_000; // 30 second safety timeout
// Handle MIDI flush request
if output.flush_midi_notes {
for ((device, channel, note), _) in seq_state.active_notes.drain() {
let _ = midi_tx.load().try_send(MidiCommand::NoteOff {
device,
channel,
note,
});
}
// Send MIDI panic (CC 123 = All Notes Off) on all 16 channels for all devices
for dev in 0..4u8 {
for chan in 0..16u8 {
let _ = midi_tx.load().try_send(MidiCommand::CC {
device: dev,
channel: chan,
cc: 123,
value: 0,
});
}
}
} else {
seq_state
.active_notes
.retain(|&(device, channel, note), active| {
let should_release = current_time_us >= active.off_time_us;
let timed_out = (current_time_us - active.start_time_us) > MAX_NOTE_DURATION_US;
if should_release || timed_out {
let _ = midi_tx.load().try_send(MidiCommand::NoteOff {
device,
channel,
note,
});
false
} else {
true
}
});
let _ = dispatch_tx.send(TimedCommand {
command: DispatchCommand::FlushMidi,
target_time_us: current_time_us,
});
}
if let Some(t) = output.new_tempo {
@@ -1228,16 +1176,34 @@ fn sequencer_loop(
shared_state.store(Arc::new(output.shared_state));
// Adaptive sleep: calculate time until next substep boundary
// At max speed (8x), substeps occur every beat/32
// Sleep for most of that time, leaving 500μs margin for processing
let beats_per_sec = tempo / 60.0;
let max_speed = 8.0; // Maximum speed multiplier from speed.clamp()
let secs_per_substep = 1.0 / (beats_per_sec * 4.0 * max_speed);
let substep_us = (secs_per_substep * 1_000_000.0) as u64;
// Sleep for most of the substep duration, clamped to reasonable bounds
let sleep_us = substep_us.saturating_sub(500).clamp(50, 2000);
precise_sleep(sleep_us);
// 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);
}
}
@@ -1285,6 +1251,21 @@ fn precise_sleep(micros: u64) {
thread::sleep(Duration::from_micros(micros));
}
/// Two-phase wait: bulk sleep followed by active spin-wait for final precision.
fn wait_until(target_us: SyncTime, link: &LinkState) {
let current = link.clock_micros() as SyncTime;
let remaining = target_us.saturating_sub(current);
if remaining > ACTIVE_WAIT_THRESHOLD_US {
precise_sleep(remaining - ACTIVE_WAIT_THRESHOLD_US);
}
// Active wait for final precision
while (link.clock_micros() as SyncTime) < target_us {
std::hint::spin_loop();
}
}
fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
if !cmd.starts_with("/midi/") {
return None;
@@ -2131,7 +2112,10 @@ mod tests {
}
// Should fire steps continuously without gaps
assert!(step_count > 350, "Expected continuous steps, got {step_count}");
assert!(
step_count > 350,
"Expected continuous steps, got {step_count}"
);
}
#[test]