Insane linux fixes
This commit is contained in:
@@ -69,6 +69,10 @@ ringbuf = "0.4"
|
|||||||
arc-swap = "1"
|
arc-swap = "1"
|
||||||
midir = "0.10"
|
midir = "0.10"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
nix = { version = "0.29", features = ["time"] }
|
||||||
|
|
||||||
# Desktop-only dependencies (behind feature flag)
|
# Desktop-only dependencies (behind feature flag)
|
||||||
egui = { version = "0.33", optional = true }
|
egui = { version = "0.33", optional = true }
|
||||||
|
|||||||
@@ -843,7 +843,14 @@ impl SequencerState {
|
|||||||
let beat_int = (beat * 4.0 * speed_mult).floor() as i64;
|
let beat_int = (beat * 4.0 * speed_mult).floor() as i64;
|
||||||
let prev_beat_int = (prev_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;
|
result.any_step_fired = true;
|
||||||
let step_idx = active.step_index % pattern.length;
|
let step_idx = active.step_index % pattern.length;
|
||||||
|
|
||||||
@@ -1054,15 +1061,30 @@ fn sequencer_loop(
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use thread_priority::unix::{
|
use thread_priority::unix::{
|
||||||
set_thread_priority_and_policy, thread_native_id, RealtimeThreadSchedulePolicy,
|
set_thread_priority_and_policy, thread_native_id, NormalThreadSchedulePolicy,
|
||||||
ThreadSchedulePolicy,
|
RealtimeThreadSchedulePolicy, ThreadSchedulePolicy,
|
||||||
};
|
};
|
||||||
|
|
||||||
let policy = ThreadSchedulePolicy::Realtime(RealtimeThreadSchedulePolicy::Fifo);
|
let tid = thread_native_id();
|
||||||
if let Err(e) =
|
|
||||||
set_thread_priority_and_policy(thread_native_id(), ThreadPriority::Max, policy)
|
// Try SCHED_FIFO first (requires CAP_SYS_NICE on Linux)
|
||||||
{
|
let fifo = ThreadSchedulePolicy::Realtime(RealtimeThreadSchedulePolicy::Fifo);
|
||||||
eprintln!("Warning: Could not set SCHED_FIFO: {e:?}");
|
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;
|
let substep_us = (secs_per_substep * 1_000_000.0) as u64;
|
||||||
// Sleep for most of the substep duration, clamped to reasonable bounds
|
// Sleep for most of the substep duration, clamped to reasonable bounds
|
||||||
let sleep_us = substep_us.saturating_sub(500).clamp(50, 2000);
|
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>)> {
|
fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
|
||||||
if !cmd.starts_with("/midi/") {
|
if !cmd.starts_with("/midi/") {
|
||||||
return None;
|
return None;
|
||||||
@@ -1632,19 +1698,23 @@ mod tests {
|
|||||||
0.5,
|
0.5,
|
||||||
));
|
));
|
||||||
|
|
||||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
// beat_int at 0.5 is 2, prev_beat_int at 0.0 is 0
|
||||||
assert_eq!(ap.step_index, 1);
|
// steps_to_fire = 2-0 = 2, firing steps 0 and 1, wrapping to 0
|
||||||
assert_eq!(ap.iter, 0);
|
|
||||||
|
|
||||||
state.tick(tick_at(0.75, true));
|
|
||||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||||
assert_eq!(ap.step_index, 0);
|
assert_eq!(ap.step_index, 0);
|
||||||
assert_eq!(ap.iter, 1);
|
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();
|
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||||
assert_eq!(ap.step_index, 1);
|
assert_eq!(ap.step_index, 1);
|
||||||
assert_eq!(ap.iter, 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]
|
#[test]
|
||||||
@@ -1673,9 +1743,15 @@ mod tests {
|
|||||||
0.5,
|
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));
|
state.tick(tick_at(0.625, true));
|
||||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
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]
|
#[test]
|
||||||
@@ -1840,14 +1916,22 @@ mod tests {
|
|||||||
0.5,
|
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(0.75, true));
|
||||||
state.tick(tick_at(1.0, true));
|
|
||||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||||
assert_eq!(ap.step_index, 3);
|
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
|
// 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(
|
state.tick(tick_with(
|
||||||
vec![SeqCommand::PatternUpdate {
|
vec![SeqCommand::PatternUpdate {
|
||||||
bank: 0,
|
bank: 0,
|
||||||
@@ -1857,12 +1941,12 @@ mod tests {
|
|||||||
1.25,
|
1.25,
|
||||||
));
|
));
|
||||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
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));
|
state.tick(tick_at(1.5, true));
|
||||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
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]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user