Feat: trying to get rid of some sequencer bugs
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user