This commit is contained in:
@@ -843,7 +843,14 @@ impl SequencerState {
|
||||
let beat_int = (beat * 4.0 * speed_mult).floor() as i64;
|
||||
let prev_beat_int = (prev_beat * 4.0 * speed_mult).floor() as i64;
|
||||
|
||||
if beat_int != prev_beat_int && prev_beat >= 0.0 {
|
||||
// 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
|
||||
};
|
||||
|
||||
for _ in 0..steps_to_fire {
|
||||
result.any_step_fired = true;
|
||||
let step_idx = active.step_index % pattern.length;
|
||||
|
||||
@@ -1054,15 +1061,30 @@ fn sequencer_loop(
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use thread_priority::unix::{
|
||||
set_thread_priority_and_policy, thread_native_id, RealtimeThreadSchedulePolicy,
|
||||
ThreadSchedulePolicy,
|
||||
set_thread_priority_and_policy, thread_native_id, NormalThreadSchedulePolicy,
|
||||
RealtimeThreadSchedulePolicy, ThreadSchedulePolicy,
|
||||
};
|
||||
|
||||
let policy = ThreadSchedulePolicy::Realtime(RealtimeThreadSchedulePolicy::Fifo);
|
||||
if let Err(e) =
|
||||
set_thread_priority_and_policy(thread_native_id(), ThreadPriority::Max, policy)
|
||||
{
|
||||
eprintln!("Warning: Could not set SCHED_FIFO: {e:?}");
|
||||
let tid = thread_native_id();
|
||||
|
||||
// Try SCHED_FIFO first (requires CAP_SYS_NICE on Linux)
|
||||
let fifo = ThreadSchedulePolicy::Realtime(RealtimeThreadSchedulePolicy::Fifo);
|
||||
if set_thread_priority_and_policy(tid, ThreadPriority::Max, fifo).is_err() {
|
||||
// Try SCHED_RR (round-robin realtime, sometimes works without caps)
|
||||
let rr = ThreadSchedulePolicy::Realtime(RealtimeThreadSchedulePolicy::RoundRobin);
|
||||
if set_thread_priority_and_policy(tid, ThreadPriority::Max, rr).is_err() {
|
||||
// Fall back to highest normal priority (SCHED_OTHER)
|
||||
let _ = set_thread_priority_and_policy(
|
||||
tid,
|
||||
ThreadPriority::Max,
|
||||
ThreadSchedulePolicy::Normal(NormalThreadSchedulePolicy::Other),
|
||||
);
|
||||
// Also try nice -20 via libc on Linux
|
||||
#[cfg(target_os = "linux")]
|
||||
unsafe {
|
||||
libc::setpriority(libc::PRIO_PROCESS, 0, -20);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1215,10 +1237,54 @@ fn sequencer_loop(
|
||||
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);
|
||||
thread::sleep(Duration::from_micros(sleep_us));
|
||||
precise_sleep(sleep_us);
|
||||
}
|
||||
}
|
||||
|
||||
/// High-precision sleep using timerfd on Linux, falling back to thread::sleep elsewhere
|
||||
#[cfg(target_os = "linux")]
|
||||
fn precise_sleep(micros: u64) {
|
||||
use nix::sys::timerfd::{ClockId, Expiration, TimerFd, TimerFlags, TimerSetTimeFlags};
|
||||
use std::os::fd::AsRawFd;
|
||||
|
||||
thread_local! {
|
||||
static TIMER: std::cell::RefCell<Option<TimerFd>> = const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
TIMER.with(|timer_cell| {
|
||||
let mut timer_ref = timer_cell.borrow_mut();
|
||||
let timer = timer_ref.get_or_insert_with(|| {
|
||||
TimerFd::new(ClockId::CLOCK_MONOTONIC, TimerFlags::empty())
|
||||
.expect("Failed to create timerfd")
|
||||
});
|
||||
|
||||
let duration = Duration::from_micros(micros);
|
||||
if timer
|
||||
.set(Expiration::OneShot(duration), TimerSetTimeFlags::empty())
|
||||
.is_ok()
|
||||
{
|
||||
// Use poll to wait for the timer instead of read to avoid allocating
|
||||
let mut pollfd = libc::pollfd {
|
||||
fd: timer.as_raw_fd(),
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
};
|
||||
unsafe {
|
||||
libc::poll(&mut pollfd, 1, -1);
|
||||
}
|
||||
// Clear the timer by reading (discard the count)
|
||||
let _ = timer.wait();
|
||||
} else {
|
||||
thread::sleep(duration);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn precise_sleep(micros: u64) {
|
||||
thread::sleep(Duration::from_micros(micros));
|
||||
}
|
||||
|
||||
fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
|
||||
if !cmd.starts_with("/midi/") {
|
||||
return None;
|
||||
@@ -1632,19 +1698,23 @@ mod tests {
|
||||
0.5,
|
||||
));
|
||||
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 1);
|
||||
assert_eq!(ap.iter, 0);
|
||||
|
||||
state.tick(tick_at(0.75, true));
|
||||
// beat_int at 0.5 is 2, prev_beat_int at 0.0 is 0
|
||||
// steps_to_fire = 2-0 = 2, firing steps 0 and 1, wrapping to 0
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 0);
|
||||
assert_eq!(ap.iter, 1);
|
||||
|
||||
state.tick(tick_at(1.0, true));
|
||||
// beat_int at 0.75 is 3, prev is 2, fires 1 step (step 0), advances to 1
|
||||
state.tick(tick_at(0.75, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 1);
|
||||
assert_eq!(ap.iter, 1);
|
||||
|
||||
// beat_int at 1.0 is 4, prev is 3, fires 1 step (step 1), wraps to 0
|
||||
state.tick(tick_at(1.0, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 0);
|
||||
assert_eq!(ap.iter, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1673,9 +1743,15 @@ mod tests {
|
||||
0.5,
|
||||
));
|
||||
|
||||
// At 2x speed: beat_int at 0.5 is (0.5*4*2)=4, prev at 0.0 is 0
|
||||
// Fires 4 steps (0,1,2,3), advancing to step 4
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 4);
|
||||
|
||||
// beat_int at 0.625 is (0.625*4*2)=5, prev is 4, fires 1 step
|
||||
state.tick(tick_at(0.625, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 2);
|
||||
assert_eq!(ap.step_index, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1840,14 +1916,22 @@ mod tests {
|
||||
0.5,
|
||||
));
|
||||
|
||||
// Advance to step_index=3
|
||||
// beat_int at 0.5 is 2, prev at 0.0 is 0, fires 2 steps (0,1), step_index=2
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 2);
|
||||
|
||||
// beat_int at 0.75 is 3, prev is 2, fires 1 step (2), step_index=3
|
||||
state.tick(tick_at(0.75, true));
|
||||
state.tick(tick_at(1.0, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 3);
|
||||
|
||||
// beat_int at 1.0 is 4, prev is 3, fires 1 step (3), wraps to step_index=0
|
||||
state.tick(tick_at(1.0, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 0);
|
||||
|
||||
// Update pattern to length 2 while running — step_index wraps via modulo
|
||||
// beat=1.25: beat_int=5, prev=4, step fires. step_index=3%2=1 fires, advances to (3+1)%2=0
|
||||
// beat=1.25: beat_int=5, prev=4, fires 1 step. step_index=0%2=0 fires, advances to 1
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
@@ -1857,12 +1941,12 @@ mod tests {
|
||||
1.25,
|
||||
));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 0);
|
||||
assert_eq!(ap.step_index, 1);
|
||||
|
||||
// beat=1.5: beat_int=6, prev=5, step fires. step_index=0 fires, advances to 1
|
||||
// beat=1.5: beat_int=6, prev=5, step fires. step_index=1 fires, wraps to 0
|
||||
state.tick(tick_at(1.5, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 1);
|
||||
assert_eq!(ap.step_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user