Fix: fix two show-stopper bugs
This commit is contained in:
@@ -1778,6 +1778,9 @@ fn emit_output(
|
||||
}
|
||||
|
||||
for (i, (k, v)) in params.iter().enumerate() {
|
||||
if v.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !out.ends_with('/') {
|
||||
out.push('/');
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@ pub fn create_editor(
|
||||
// Read live snapshot from the audio thread
|
||||
let shared = editor.bridge.shared_state.load();
|
||||
editor.snapshot = SequencerSnapshot::from(shared.as_ref());
|
||||
editor.app.playback.playing = editor.snapshot.playing;
|
||||
|
||||
// Sync host tempo into LinkState so title bar shows real tempo
|
||||
if shared.tempo > 0.0 {
|
||||
@@ -298,6 +299,11 @@ pub fn create_editor(
|
||||
let elapsed = editor.last_frame.elapsed();
|
||||
editor.last_frame = Instant::now();
|
||||
|
||||
if editor.app.playback.has_armed() {
|
||||
let rate = std::f32::consts::TAU;
|
||||
editor.app.ui.pulse_phase = (editor.app.ui.pulse_phase + elapsed.as_secs_f32() * rate) % std::f32::consts::TAU;
|
||||
}
|
||||
|
||||
let link = &editor.link;
|
||||
let app = &editor.app;
|
||||
let snapshot = &editor.snapshot;
|
||||
|
||||
@@ -496,6 +496,11 @@ impl eframe::App for CagireDesktop {
|
||||
let elapsed = self.last_frame.elapsed();
|
||||
self.last_frame = std::time::Instant::now();
|
||||
|
||||
if self.app.playback.has_armed() {
|
||||
let rate = std::f32::consts::TAU;
|
||||
self.app.ui.pulse_phase = (self.app.ui.pulse_phase + elapsed.as_secs_f32() * rate) % std::f32::consts::TAU;
|
||||
}
|
||||
|
||||
let link = &self.link;
|
||||
let app = &self.app;
|
||||
self.terminal
|
||||
|
||||
@@ -170,6 +170,7 @@ pub struct SharedSequencerState {
|
||||
pub event_count: usize,
|
||||
pub tempo: f64,
|
||||
pub beat: f64,
|
||||
pub playing: bool,
|
||||
pub script_trace: Option<ExecutionTrace>,
|
||||
pub print_output: Option<String>,
|
||||
}
|
||||
@@ -180,6 +181,7 @@ pub struct SequencerSnapshot {
|
||||
pub event_count: usize,
|
||||
pub tempo: f64,
|
||||
pub beat: f64,
|
||||
pub playing: bool,
|
||||
script_trace: Option<ExecutionTrace>,
|
||||
pub print_output: Option<String>,
|
||||
}
|
||||
@@ -192,6 +194,7 @@ impl From<&SharedSequencerState> for SequencerSnapshot {
|
||||
event_count: s.event_count,
|
||||
tempo: s.tempo,
|
||||
beat: s.beat,
|
||||
playing: s.playing,
|
||||
script_trace: s.script_trace.clone(),
|
||||
print_output: s.print_output.clone(),
|
||||
}
|
||||
@@ -207,6 +210,7 @@ impl SequencerSnapshot {
|
||||
event_count: 0,
|
||||
tempo: 0.0,
|
||||
beat: 0.0,
|
||||
playing: false,
|
||||
script_trace: None,
|
||||
print_output: None,
|
||||
}
|
||||
@@ -306,6 +310,7 @@ struct PendingPattern {
|
||||
|
||||
struct AudioState {
|
||||
prev_beat: f64,
|
||||
pause_beat: Option<f64>,
|
||||
active_patterns: HashMap<PatternId, ActivePattern>,
|
||||
pending_starts: Vec<PendingPattern>,
|
||||
pending_stops: Vec<PendingPattern>,
|
||||
@@ -316,6 +321,7 @@ impl AudioState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
prev_beat: -1.0,
|
||||
pause_beat: None,
|
||||
active_patterns: HashMap::new(),
|
||||
pending_starts: Vec::new(),
|
||||
pending_stops: Vec::new(),
|
||||
@@ -572,6 +578,7 @@ pub struct SequencerState {
|
||||
soloed: std::collections::HashSet<(usize, usize)>,
|
||||
last_tempo: f64,
|
||||
last_beat: f64,
|
||||
last_playing: bool,
|
||||
script_text: String,
|
||||
script_speed: crate::model::PatternSpeed,
|
||||
script_length: usize,
|
||||
@@ -610,6 +617,7 @@ impl SequencerState {
|
||||
soloed: std::collections::HashSet::new(),
|
||||
last_tempo: 0.0,
|
||||
last_beat: 0.0,
|
||||
last_playing: false,
|
||||
script_text: String::new(),
|
||||
script_speed: crate::model::PatternSpeed::default(),
|
||||
script_length: 16,
|
||||
@@ -714,6 +722,7 @@ impl SequencerState {
|
||||
self.audio_state.active_patterns.clear();
|
||||
self.audio_state.pending_starts.clear();
|
||||
self.audio_state.pending_stops.clear();
|
||||
self.audio_state.pause_beat = None;
|
||||
self.step_traces = Arc::new(HashMap::new());
|
||||
self.runs_counter.counts.clear();
|
||||
self.audio_state.flush_midi_notes = true;
|
||||
@@ -724,6 +733,7 @@ impl SequencerState {
|
||||
active.iter = 0;
|
||||
}
|
||||
self.audio_state.prev_beat = -1.0;
|
||||
self.audio_state.pause_beat = None;
|
||||
self.script_frontier = -1.0;
|
||||
self.script_step = 0;
|
||||
self.script_trace = None;
|
||||
@@ -751,6 +761,7 @@ impl SequencerState {
|
||||
self.process_commands(input.commands);
|
||||
self.last_tempo = input.tempo;
|
||||
self.last_beat = input.beat;
|
||||
self.last_playing = input.playing;
|
||||
|
||||
if !input.playing {
|
||||
return self.tick_paused();
|
||||
@@ -758,14 +769,21 @@ impl SequencerState {
|
||||
|
||||
let frontier = self.audio_state.prev_beat;
|
||||
let lookahead_end = input.lookahead_end;
|
||||
let resuming = frontier < 0.0;
|
||||
|
||||
if frontier < 0.0 {
|
||||
let boundary_frontier = if resuming {
|
||||
self.audio_state.pause_beat.take().unwrap_or(input.beat)
|
||||
} else {
|
||||
frontier
|
||||
};
|
||||
|
||||
self.activate_pending(lookahead_end, boundary_frontier, input.quantum);
|
||||
self.deactivate_pending(lookahead_end, boundary_frontier, input.quantum);
|
||||
|
||||
if resuming {
|
||||
self.realign_phaselock_patterns(lookahead_end);
|
||||
}
|
||||
|
||||
self.activate_pending(lookahead_end, frontier, input.quantum);
|
||||
self.deactivate_pending(lookahead_end, frontier, input.quantum);
|
||||
|
||||
let steps = self.execute_steps(
|
||||
input.beat,
|
||||
frontier,
|
||||
@@ -822,6 +840,9 @@ impl SequencerState {
|
||||
self.pattern_cache.set(key.0, key.1, snapshot);
|
||||
}
|
||||
}
|
||||
if self.audio_state.prev_beat >= 0.0 {
|
||||
self.audio_state.pause_beat = Some(self.audio_state.prev_beat);
|
||||
}
|
||||
self.audio_state.prev_beat = -1.0;
|
||||
self.script_frontier = -1.0;
|
||||
self.script_step = 0;
|
||||
@@ -1240,6 +1261,7 @@ impl SequencerState {
|
||||
event_count: self.event_count,
|
||||
tempo: self.last_tempo,
|
||||
beat: self.last_beat,
|
||||
playing: self.last_playing,
|
||||
script_trace: self.script_trace.clone(),
|
||||
print_output: self.print_output.clone(),
|
||||
}
|
||||
@@ -1975,10 +1997,8 @@ mod tests {
|
||||
|
||||
assert!(!state.audio_state.pending_starts.is_empty());
|
||||
|
||||
// Resume playing — first tick resets prev_beat from -1 to 2.0
|
||||
// Resume playing — Immediate fires on first tick
|
||||
state.tick(tick_at(2.0, true));
|
||||
// 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)));
|
||||
}
|
||||
|
||||
@@ -2187,4 +2207,222 @@ mod tests {
|
||||
// Should have commands from both patterns (2 patterns * 1 command each)
|
||||
assert!(output.audio_commands.len() >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bar_boundary_crossed_during_pause() {
|
||||
let mut state = make_state();
|
||||
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
data: simple_pattern(4),
|
||||
}],
|
||||
0.0,
|
||||
));
|
||||
|
||||
// Queue Bar-quantized start at beat 3.5 (before bar at beat 4.0)
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
3.5,
|
||||
));
|
||||
assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 0)));
|
||||
|
||||
// Pause — saves prev_beat=3.5 as pause_beat
|
||||
state.tick(tick_at(3.75, false));
|
||||
|
||||
// Resume after bar boundary (beat 4.0 crossed)
|
||||
state.tick(tick_at(4.5, true));
|
||||
assert!(
|
||||
state.audio_state.active_patterns.contains_key(&pid(0, 0)),
|
||||
"Bar-quantized pattern should activate on resume after bar boundary"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_immediate_activates_on_first_resume_tick() {
|
||||
let mut state = make_state();
|
||||
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
data: simple_pattern(4),
|
||||
}],
|
||||
0.0,
|
||||
));
|
||||
|
||||
// Pause
|
||||
state.tick(tick_at(1.0, false));
|
||||
|
||||
// Queue Immediate start while paused
|
||||
state.tick(TickInput {
|
||||
commands: vec![SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
..tick_at(2.0, false)
|
||||
});
|
||||
|
||||
// Resume — Immediate should fire on this single tick
|
||||
state.tick(tick_at(3.0, true));
|
||||
assert!(
|
||||
state.audio_state.active_patterns.contains_key(&pid(0, 0)),
|
||||
"Immediate should activate on first resume tick"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_patterns_sync_after_pause() {
|
||||
let mut state = make_state();
|
||||
|
||||
state.tick(tick_with(
|
||||
vec![
|
||||
SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
data: simple_pattern(4),
|
||||
},
|
||||
SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 1,
|
||||
data: simple_pattern(8),
|
||||
},
|
||||
],
|
||||
0.0,
|
||||
));
|
||||
|
||||
// Queue two Bar-quantized starts
|
||||
state.tick(tick_with(
|
||||
vec![
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 1,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
3.5,
|
||||
));
|
||||
|
||||
// Pause before bar
|
||||
state.tick(tick_at(3.75, false));
|
||||
|
||||
// Resume after bar boundary
|
||||
state.tick(tick_at(4.5, true));
|
||||
assert!(
|
||||
state.audio_state.active_patterns.contains_key(&pid(0, 0)),
|
||||
"First pattern should activate"
|
||||
);
|
||||
assert!(
|
||||
state.audio_state.active_patterns.contains_key(&pid(0, 1)),
|
||||
"Second pattern should activate together"
|
||||
);
|
||||
}
|
||||
|
||||
fn phaselock_pattern(length: usize) -> PatternSnapshot {
|
||||
PatternSnapshot {
|
||||
speed: Default::default(),
|
||||
length,
|
||||
steps: (0..length)
|
||||
.map(|_| StepSnapshot {
|
||||
active: true,
|
||||
script: "test".into(),
|
||||
source: None,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: SyncMode::PhaseLock,
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_phaselock_position_correct_after_resume() {
|
||||
let mut state = make_state();
|
||||
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
data: phaselock_pattern(16),
|
||||
}],
|
||||
0.0,
|
||||
));
|
||||
|
||||
// Queue PhaseLock pattern
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::PhaseLock,
|
||||
}],
|
||||
3.5,
|
||||
));
|
||||
|
||||
// Pause before bar
|
||||
state.tick(tick_at(3.75, false));
|
||||
|
||||
// Resume at beat 5.0 (after bar boundary at 4.0)
|
||||
let resume_beat = 5.0;
|
||||
state.tick(tick_at(resume_beat, true));
|
||||
assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));
|
||||
|
||||
// realign_phaselock_patterns uses: (beat * 4.0).floor() + 1 % length
|
||||
// At beat 5.0: (5.0 * 4.0).floor() = 20, +1 = 21, 21 % 16 = 5
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
let expected = ((resume_beat * 4.0).floor() as usize + 1) % 16;
|
||||
assert_eq!(
|
||||
ap.step_index, expected,
|
||||
"PhaseLock step should be based on resume beat, not pause beat"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_false_boundary_after_pause_within_same_bar() {
|
||||
let mut state = make_state();
|
||||
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
data: simple_pattern(4),
|
||||
}],
|
||||
0.0,
|
||||
));
|
||||
|
||||
// Queue Bar-quantized start at beat 1.0
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
1.0,
|
||||
));
|
||||
|
||||
// Pause at beat 1.5 (within same bar)
|
||||
state.tick(tick_at(1.5, false));
|
||||
|
||||
// Resume at beat 2.5 (still within same bar, quantum=4)
|
||||
state.tick(tick_at(2.5, true));
|
||||
assert!(
|
||||
!state.audio_state.active_patterns.contains_key(&pid(0, 0)),
|
||||
"Bar-quantized pattern should NOT activate when no bar boundary was crossed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user