From 1513d80a8dc45825878066ae12eddc9b22491398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Mon, 16 Mar 2026 22:07:15 +0100 Subject: [PATCH] Fix: try to fix the non working sync --- Cargo.lock | 20 +--- plugins/cagire-plugins/Cargo.toml | 2 +- plugins/cagire-plugins/src/lib.rs | 1 + scripts/build-all.sh | 48 ++++++++-- src/engine/sequencer.rs | 147 +++++++++++++++++++++++++++++- 5 files changed, 191 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6400f5..2a07b06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -873,7 +873,7 @@ dependencies = [ "cpal 0.17.1", "crossbeam-channel", "crossterm", - "doux 0.0.14", + "doux", "eframe", "egui", "egui_ratatui", @@ -925,7 +925,7 @@ dependencies = [ "cagire-ratatui", "crossbeam-channel", "crossterm", - "doux 0.0.13", + "doux", "egui_ratatui", "nih_plug", "nih_plug_egui", @@ -1822,22 +1822,6 @@ dependencies = [ "litrs", ] -[[package]] -name = "doux" -version = "0.0.13" -source = "git+https://github.com/sova-org/doux?tag=v0.0.13#b8150d907e4cc2764e82fdaa424df41ceef9b0d2" -dependencies = [ - "arc-swap", - "clap", - "cpal 0.17.1", - "crossbeam-channel", - "ringbuf", - "rosc", - "rustyline", - "soundfont", - "symphonia", -] - [[package]] name = "doux" version = "0.0.14" diff --git a/plugins/cagire-plugins/Cargo.toml b/plugins/cagire-plugins/Cargo.toml index c7dba50..2fe055f 100644 --- a/plugins/cagire-plugins/Cargo.toml +++ b/plugins/cagire-plugins/Cargo.toml @@ -14,7 +14,7 @@ cagire = { path = "../..", default-features = false, features = ["block-renderer cagire-forth = { path = "../../crates/forth" } cagire-project = { path = "../../crates/project" } cagire-ratatui = { path = "../../crates/ratatui" } -doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.13", features = ["native", "soundfont"] } +doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.14", features = ["native", "soundfont"] } nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] } nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" } egui_ratatui = "2.1" diff --git a/plugins/cagire-plugins/src/lib.rs b/plugins/cagire-plugins/src/lib.rs index 0b206dc..58f9008 100644 --- a/plugins/cagire-plugins/src/lib.rs +++ b/plugins/cagire-plugins/src/lib.rs @@ -185,6 +185,7 @@ impl Plugin for CagirePlugin { self.sample_rate, self.output_channels, 64, + buffer_config.max_buffer_size as usize, ); self.bridge .sample_registry diff --git a/scripts/build-all.sh b/scripts/build-all.sh index 55e8d0e..f3d1094 100755 --- a/scripts/build-all.sh +++ b/scripts/build-all.sh @@ -51,7 +51,7 @@ while [[ $# -gt 0 ]]; do echo "" echo "Options:" echo " --platforms Comma-separated: macos-arm64,macos-x86_64,linux-x86_64,linux-aarch64,windows-x86_64" - echo " --targets Comma-separated: cli,desktop,plugins" + echo " --targets Comma-separated: cli,desktop,plugins,installer" echo " --all Build all platforms and targets" echo " --yes Skip confirmation prompt" echo "" @@ -105,22 +105,30 @@ prompt_platforms() { } prompt_targets() { + local show_installer=false + for p in "${selected_platforms[@]}"; do + [[ "$p" == *windows* ]] && show_installer=true + done + echo "" echo "Select targets (0=all, comma-separated):" echo " 0) All" echo " 1) cagire" echo " 2) cagire-desktop" echo " 3) cagire-plugins (CLAP/VST3)" + $show_installer && echo " 4) installer (NSIS, implies cli+desktop+plugins)" read -rp "> " choice build_cagire=false build_desktop=false build_plugins=false + build_installer=false if [[ "$choice" == "0" || -z "$choice" ]]; then build_cagire=true build_desktop=true build_plugins=true + $show_installer && build_installer=true else IFS=',' read -ra targets <<< "$choice" for t in "${targets[@]}"; do @@ -129,10 +137,24 @@ prompt_targets() { 1) build_cagire=true ;; 2) build_desktop=true ;; 3) build_plugins=true ;; + 4) + if $show_installer; then + build_installer=true + else + echo "Invalid target: $t"; exit 1 + fi + ;; *) echo "Invalid target: $t"; exit 1 ;; esac done fi + + # Installer requires cli+desktop+plugins + if $build_installer; then + build_cagire=true + build_desktop=true + build_plugins=true + fi } confirm_summary() { @@ -147,7 +169,8 @@ confirm_summary() { echo "Targets:" $build_cagire && echo " - cagire" $build_desktop && echo " - cagire-desktop" - $build_plugins && echo " - cagire-plugins (CLAP/VST3)" + $build_plugins && echo " - cagire-plugins (CLAP/VST3)" + $build_installer && echo " - installer (NSIS)" echo "" read -rp "Proceed? [Y/n] " yn case "${yn,,}" in @@ -328,7 +351,7 @@ copy_artifacts() { fi # NSIS installer for Windows targets - if [[ "$os" == "windows" ]] && command -v makensis &>/dev/null; then + if $build_installer && [[ "$os" == "windows" ]] && command -v makensis &>/dev/null; then echo " Building NSIS installer..." local version version=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/') @@ -384,6 +407,7 @@ if $cli_all; then build_cagire=true build_desktop=true build_plugins=true + build_installer=true elif [[ -n "$cli_platforms" || -n "$cli_targets" ]]; then # Resolve platforms from CLI if [[ -n "$cli_platforms" ]]; then @@ -405,21 +429,31 @@ elif [[ -n "$cli_platforms" || -n "$cli_targets" ]]; then build_cagire=false build_desktop=false build_plugins=false + build_installer=false if [[ -n "$cli_targets" ]]; then IFS=',' read -ra tgts <<< "$cli_targets" for t in "${tgts[@]}"; do t="${t// /}" case "$t" in - cli) build_cagire=true ;; - desktop) build_desktop=true ;; - plugins) build_plugins=true ;; - *) echo "Unknown target: $t (expected: cli, desktop, plugins)"; exit 1 ;; + cli) build_cagire=true ;; + desktop) build_desktop=true ;; + plugins) build_plugins=true ;; + installer) build_installer=true ;; + *) echo "Unknown target: $t (expected: cli, desktop, plugins, installer)"; exit 1 ;; esac done else build_cagire=true build_desktop=true build_plugins=true + build_installer=true + fi + + # Installer requires cli+desktop+plugins + if $build_installer; then + build_cagire=true + build_desktop=true + build_plugins=true fi else prompt_platforms diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index 42bed3c..87cb06e 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -299,6 +299,7 @@ struct ActivePattern { step_index: usize, iter: usize, last_step_beat: f64, + activation_beat: Option, } #[derive(Clone, Copy)] @@ -490,6 +491,30 @@ fn check_quantization_boundary( } } +fn quantization_boundary_beat( + quantization: LaunchQuantization, + prev_beat: f64, + quantum: f64, +) -> Option { + match quantization { + LaunchQuantization::Immediate => None, + LaunchQuantization::Beat => Some(prev_beat.floor() + 1.0), + LaunchQuantization::Bar => Some(((prev_beat / quantum).floor() + 1.0) * quantum), + LaunchQuantization::Bars2 => { + let q = quantum * 2.0; + Some(((prev_beat / q).floor() + 1.0) * q) + } + LaunchQuantization::Bars4 => { + let q = quantum * 4.0; + Some(((prev_beat / q).floor() + 1.0) * q) + } + LaunchQuantization::Bars8 => { + let q = quantum * 8.0; + Some(((prev_beat / q).floor() + 1.0) * q) + } + } +} + type StepKey = (usize, usize, usize); struct RunsCounter { @@ -892,6 +917,8 @@ impl SequencerState { } } }; + let boundary = + quantization_boundary_beat(pending.quantization, prev_beat, quantum); self.runs_counter .clear_pattern(pending.id.bank, pending.id.pattern); self.audio_state.active_patterns.insert( @@ -902,6 +929,7 @@ impl SequencerState { step_index: start_step, iter: 0, last_step_beat: beat, + activation_beat: boundary, }, ); self.buf_activated.push(pending.id); @@ -982,8 +1010,13 @@ impl SequencerState { .unwrap_or_else(|| pattern.speed.multiplier()); let step_beats = substeps_in_window(frontier, lookahead_end, speed_mult); + let activation = active.activation_beat.take(); + let skip = activation.map_or(0, |ab| { + step_beats.iter().take_while(|&&b| b < ab).count() + }); - for step_beat in step_beats { + for step_beat in &step_beats[skip..] { + let step_beat = *step_beat; result.any_step_fired = true; active.last_step_beat = step_beat; let step_idx = active.step_index % pattern.length; @@ -2391,6 +2424,118 @@ mod tests { ); } + #[test] + fn test_quantization_boundary_beat() { + let quantum = 4.0; + + // Immediate → None + assert_eq!( + quantization_boundary_beat(LaunchQuantization::Immediate, 1.5, quantum), + None + ); + + // Beat → next integer beat + assert_eq!( + quantization_boundary_beat(LaunchQuantization::Beat, 1.5, quantum), + Some(2.0) + ); + assert_eq!( + quantization_boundary_beat(LaunchQuantization::Beat, 3.0, quantum), + Some(4.0) + ); + + // Bar → next multiple of quantum + assert_eq!( + quantization_boundary_beat(LaunchQuantization::Bar, 3.9, quantum), + Some(4.0) + ); + assert_eq!( + quantization_boundary_beat(LaunchQuantization::Bar, 0.0, quantum), + Some(4.0) + ); + + // Bars2 → next multiple of quantum*2 + assert_eq!( + quantization_boundary_beat(LaunchQuantization::Bars2, 3.9, quantum), + Some(8.0) + ); + + // Bars4 → next multiple of quantum*4 + assert_eq!( + quantization_boundary_beat(LaunchQuantization::Bars4, 3.9, quantum), + Some(16.0) + ); + + // Bars8 → next multiple of quantum*8 + assert_eq!( + quantization_boundary_beat(LaunchQuantization::Bars8, 3.9, quantum), + Some(32.0) + ); + } + + #[test] + fn test_activation_beat_prevents_early_substeps() { + let mut state = make_state(); + + state.tick(tick_with( + vec![SeqCommand::PatternUpdate { + bank: 0, + pattern: 0, + data: simple_pattern(16), + }], + 0.0, + )); + + // Queue Bar-quantized start (boundary at beat 4.0, quantum=4) + 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))); + + // Simulate a wide lookahead window that crosses the bar boundary: + // frontier=3.875, lookahead_end=4.125 — spans both sides of beat 4.0 + // Without activation_beat filtering, substeps before 4.0 would fire. + state.tick(TickInput { + commands: Vec::new(), + playing: true, + beat: 4.125, + lookahead_end: 4.125, + tempo: 120.0, + quantum: 4.0, + fill: false, + nudge_secs: 0.0, + current_time_us: 0, + audio_sample_pos: 0, + sr: 48000.0, + mouse_x: 0.5, + mouse_y: 0.5, + mouse_down: 0.0, + }); + + // Pattern should be active + assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0))); + + // The activation_beat should have been consumed (set to None) + let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); + assert!(ap.activation_beat.is_none(), "activation_beat should be consumed after first execute"); + + // Step index should reflect only substeps at/after beat 4.0, not before + // At 1x speed: substeps at 0.25-beat intervals. From frontier 3.875 to 4.125: + // substeps_in_window yields beats at 4.0 (= 16/4). Pre-4.0 substeps should be skipped. + assert_eq!(ap.step_index, 1, "Only substep at beat 4.0 should fire, not pre-boundary ones"); + + // Second tick: activation_beat already consumed, all substeps should fire normally + let _output2 = state.tick(tick_at(4.375, true)); + let ap2 = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); + assert_eq!(ap2.step_index, 2, "Second tick should advance normally"); + } + #[test] fn test_no_false_boundary_after_pause_within_same_bar() { let mut state = make_state();