Feat: trying to get rid of some sequencer bugs

This commit is contained in:
2026-02-07 01:24:38 +01:00
parent 52cc890a67
commit 5758b18d58
5 changed files with 38 additions and 10 deletions

View File

@@ -24,6 +24,12 @@ All notable changes to this project will be documented in this file.
- Removed unnecessary `Arc` wrapper from `Stack` type - Removed unnecessary `Arc` wrapper from `Stack` type
- Variable key cache computes on-demand with reusable buffers instead of pre-allocating 2048 Strings - Variable key cache computes on-demand with reusable buffers instead of pre-allocating 2048 Strings
### Fixed
- Sequencer sync: auto-loaded patterns now use PhaseLock instead of Reset, so they align to the global beat grid and stay in sync with manually-started patterns.
- PhaseLock off-by-one: start step calculation now uses the frontier beat instead of the lookahead end, eliminating a systematic 1-step offset.
- Stale pattern cache on load: dirty patterns are now flushed before queued start/stop changes, ensuring pattern data arrives before activation.
- Loading while paused no longer drops auto-started patterns; pending starts are preserved and activate on resume.
### Changed ### Changed
- Header bar is now always 3 lines tall with vertically centered content and full-height background colors, replacing the previous 1-or-2-line width-dependent layout. - Header bar is now always 3 lines tall with vertically centered content and full-height background colors, replacing the previous 1-or-2-line width-dependent layout.
- Help view Welcome page: BigText title is now gated behind `cfg(not(feature = "desktop"))`, falling back to a plain text title in the desktop build (same strategy as the splash screen). - Help view Welcome page: BigText title is now gated behind `cfg(not(feature = "desktop"))`, falling back to a plain text title in the desktop build (same strategy as the splash screen).

View File

@@ -622,7 +622,7 @@ impl App {
self.playback.queued_changes.push(StagedChange { self.playback.queued_changes.push(StagedChange {
change: PatternChange::Start { bank, pattern }, change: PatternChange::Start { bank, pattern },
quantization: crate::model::LaunchQuantization::Immediate, quantization: crate::model::LaunchQuantization::Immediate,
sync_mode: crate::model::SyncMode::Reset, sync_mode: crate::model::SyncMode::PhaseLock,
}); });
} }

View File

@@ -693,6 +693,10 @@ impl SequencerState {
let frontier = self.audio_state.prev_beat; let frontier = self.audio_state.prev_beat;
let lookahead_end = input.lookahead_end; let lookahead_end = input.lookahead_end;
if frontier < 0.0 {
self.realign_phaselock_patterns(lookahead_end);
}
self.activate_pending(lookahead_end, frontier, input.quantum); self.activate_pending(lookahead_end, frontier, input.quantum);
self.deactivate_pending(lookahead_end, frontier, input.quantum); self.deactivate_pending(lookahead_end, frontier, input.quantum);
@@ -739,7 +743,6 @@ impl SequencerState {
self.pattern_cache.set(key.0, key.1, snapshot); self.pattern_cache.set(key.0, key.1, snapshot);
} }
} }
self.audio_state.pending_starts.clear();
self.audio_state.prev_beat = -1.0; self.audio_state.prev_beat = -1.0;
self.buf_audio_commands.clear(); self.buf_audio_commands.clear();
let flush = std::mem::take(&mut self.audio_state.flush_midi_notes); let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
@@ -751,6 +754,21 @@ impl SequencerState {
} }
} }
fn realign_phaselock_patterns(&mut self, beat: f64) {
for (id, active) in &mut self.audio_state.active_patterns {
let Some(pattern) = self.pattern_cache.get(id.bank, id.pattern) else {
continue;
};
if pattern.sync_mode != SyncMode::PhaseLock {
continue;
}
let speed_mult = pattern.speed.multiplier();
let subs_per_beat = 4.0 * speed_mult;
let step = (beat * subs_per_beat).floor() as usize + 1;
active.step_index = step % pattern.length;
}
}
fn activate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) { fn activate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) {
self.buf_activated.clear(); self.buf_activated.clear();
for pending in &self.audio_state.pending_starts { for pending in &self.audio_state.pending_starts {
@@ -762,7 +780,9 @@ impl SequencerState {
self.pattern_cache.get(pending.id.bank, pending.id.pattern) self.pattern_cache.get(pending.id.bank, pending.id.pattern)
{ {
let speed_mult = pat.speed.multiplier(); let speed_mult = pat.speed.multiplier();
((beat * 4.0 * speed_mult) as usize) % pat.length let subs_per_beat = 4.0 * speed_mult;
let first_sub = (prev_beat * subs_per_beat).floor() as usize + 1;
first_sub % pat.length
} else { } else {
0 0
} }
@@ -1889,7 +1909,7 @@ mod tests {
} }
#[test] #[test]
fn test_start_while_paused_is_discarded() { fn test_start_while_paused_is_preserved() {
let mut state = make_state(); let mut state = make_state();
state.tick(tick_with( state.tick(tick_with(
@@ -1901,7 +1921,7 @@ mod tests {
0.0, 0.0,
)); ));
// Start while paused: pending_starts gets cleared // Start while paused: pending_starts preserved for resume
state.tick(TickInput { state.tick(TickInput {
commands: vec![SeqCommand::PatternStart { commands: vec![SeqCommand::PatternStart {
bank: 0, bank: 0,
@@ -1912,11 +1932,13 @@ mod tests {
..tick_at(1.0, false) ..tick_at(1.0, false)
}); });
assert!(state.audio_state.pending_starts.is_empty()); assert!(!state.audio_state.pending_starts.is_empty());
// Resume playing — pattern should NOT be active // Resume playing — first tick resets prev_beat from -1 to 2.0
state.tick(tick_at(2.0, true)); state.tick(tick_at(2.0, true));
assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 0))); // Second tick: prev_beat is now >= 0, so Immediate fires
state.tick(tick_at(2.25, true));
assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));
} }
#[test] #[test]

View File

@@ -68,7 +68,7 @@ pub fn init(args: InitArgs) -> Init {
pattern: 0, pattern: 0,
}, },
quantization: model::LaunchQuantization::Immediate, quantization: model::LaunchQuantization::Immediate,
sync_mode: model::SyncMode::Reset, sync_mode: model::SyncMode::PhaseLock,
}); });
app.audio.config.output_device = args.output.or(settings.audio.output_device.clone()); app.audio.config.output_device = args.output.or(settings.audio.output_device.clone());

View File

@@ -229,8 +229,8 @@ fn main() -> io::Result<()> {
let seq_snapshot = sequencer.snapshot(); let seq_snapshot = sequencer.snapshot();
app.metrics.event_count = seq_snapshot.event_count; app.metrics.event_count = seq_snapshot.event_count;
app.flush_queued_changes(&sequencer.cmd_tx);
app.flush_dirty_patterns(&sequencer.cmd_tx); app.flush_dirty_patterns(&sequencer.cmd_tx);
app.flush_queued_changes(&sequencer.cmd_tx);
let had_event = event::poll(Duration::from_millis( let had_event = event::poll(Duration::from_millis(
app.audio.config.refresh_rate.millis(), app.audio.config.refresh_rate.millis(),