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
|
- 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).
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user