Feat: trying to get rid of some sequencer bugs

This commit is contained in:
2026-02-07 01:24:38 +01:00
parent e0d338a030
commit 83c756618f
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
- 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
- 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).

View File

@@ -622,7 +622,7 @@ impl App {
self.playback.queued_changes.push(StagedChange {
change: PatternChange::Start { bank, pattern },
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 lookahead_end = input.lookahead_end;
if frontier < 0.0 {
self.realign_phaselock_patterns(lookahead_end);
}
self.activate_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.audio_state.pending_starts.clear();
self.audio_state.prev_beat = -1.0;
self.buf_audio_commands.clear();
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) {
self.buf_activated.clear();
for pending in &self.audio_state.pending_starts {
@@ -762,7 +780,9 @@ impl SequencerState {
self.pattern_cache.get(pending.id.bank, pending.id.pattern)
{
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 {
0
}
@@ -1889,7 +1909,7 @@ mod tests {
}
#[test]
fn test_start_while_paused_is_discarded() {
fn test_start_while_paused_is_preserved() {
let mut state = make_state();
state.tick(tick_with(
@@ -1901,7 +1921,7 @@ mod tests {
0.0,
));
// Start while paused: pending_starts gets cleared
// Start while paused: pending_starts preserved for resume
state.tick(TickInput {
commands: vec![SeqCommand::PatternStart {
bank: 0,
@@ -1912,11 +1932,13 @@ mod tests {
..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));
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]

View File

@@ -68,7 +68,7 @@ pub fn init(args: InitArgs) -> Init {
pattern: 0,
},
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());

View File

@@ -229,8 +229,8 @@ fn main() -> io::Result<()> {
let seq_snapshot = sequencer.snapshot();
app.metrics.event_count = seq_snapshot.event_count;
app.flush_queued_changes(&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(
app.audio.config.refresh_rate.millis(),