Compare commits
5 Commits
859629ae34
...
v0.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d71c64a34 | |||
| 097104a074 | |||
| c13ddaaf37 | |||
| 001a42abfc | |||
| 0d0c2738f5 |
@@ -54,22 +54,22 @@ jobs:
|
||||
echo "C:\Program Files\CMake\bin" >> $env:GITHUB_PATH
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --target x86_64-pc-windows-msvc
|
||||
run: cargo build --release --features asio --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Build desktop
|
||||
run: cargo build --release --features desktop --bin cagire-desktop --target x86_64-pc-windows-msvc
|
||||
run: cargo build --release --features desktop,asio --bin cagire-desktop --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Test
|
||||
if: inputs.run-tests
|
||||
run: cargo test --target x86_64-pc-windows-msvc
|
||||
run: cargo test --features asio --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Clippy
|
||||
if: inputs.run-clippy
|
||||
run: cargo clippy --target x86_64-pc-windows-msvc -- -D warnings
|
||||
run: cargo clippy --features asio --target x86_64-pc-windows-msvc -- -D warnings
|
||||
|
||||
- name: Bundle CLAP plugin
|
||||
if: inputs.build-packages
|
||||
run: cargo xtask bundle cagire-plugins --release --target x86_64-pc-windows-msvc
|
||||
run: cargo xtask bundle cagire-plugins --release --features asio --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Install NSIS
|
||||
if: inputs.build-packages
|
||||
|
||||
50
CHANGELOG.md
50
CHANGELOG.md
@@ -2,6 +2,56 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.1.4]
|
||||
|
||||
### Breaking
|
||||
- **Doux v0.0.12**: removed Mutable Instruments Plaits modes (`modal`, `va`, `analog`, `waveshape`, `grain`, `chord`, `swarm`, `pnoise`, etc.). Native percussion models retained; new models added: `tom`, `cowbell`, `cymbal`.
|
||||
- Simplified effects/filter API: removed per-filter envelope parameters in favor of the universal `env` word.
|
||||
- Recording commands simplified: removed `/sound/` path segment from `rec`, `overdub`, `orec`, `odub`.
|
||||
|
||||
### Forth Language
|
||||
- New modulation transition words: `islide` (swell), `oslide` (pluck), `pslide` (stair/stepped).
|
||||
- New `lpg` word (Low Pass Gate): pairs amplitude envelope with lowpass filter modulation.
|
||||
- New `inchan` word: select audio input channel by index.
|
||||
- New EQ frequency words: `eqlofreq`, `eqmidfreq`, `eqhifreq`.
|
||||
|
||||
### UI / UX
|
||||
- Redesigned top bar: consolidated transport, tempo, bar:beat display with visual beat segments.
|
||||
- CPU meter with color-coded fill bar (green/yellow/red).
|
||||
|
||||
### Engine
|
||||
- Audio input channel selection support.
|
||||
- Audio buffer sizing improved for multi-channel input.
|
||||
|
||||
### Packaging
|
||||
- CI migrated from GitHub Actions to Gitea Actions.
|
||||
- Removed WIX installer; Windows now distributed via zip and NSIS only.
|
||||
- Gitea Actions workflow for automatic website deployment.
|
||||
- Added LICENSE file.
|
||||
|
||||
### Documentation
|
||||
- Extensive documentation updates reflecting doux v0.0.12 API changes across sources, filters, modulation, wavetable, and audio modulation docs.
|
||||
|
||||
## [0.1.3]
|
||||
|
||||
### Forth Language
|
||||
- New `stretch` word: pitch-independent time stretching via phase vocoder (e.g., `kick sound 2 stretch .` plays at half speed, same pitch).
|
||||
- Automatic default release time on sounds when none is explicitly set.
|
||||
|
||||
### Engine
|
||||
- Sample-accurate timing: delta computation switched from float seconds to integer sample ticks, fixing precision issues.
|
||||
- Lock-free audio input buffer: replaced `Arc<Mutex<VecDeque>>` with `HeapRb` ring buffer.
|
||||
- Theme access optimized: `Rc<ThemeColors>` replaces deep cloning on every `get()`.
|
||||
- Dictionary keys cached in `App` to avoid repeated lock acquisitions during rendering.
|
||||
|
||||
### Fixed
|
||||
- Realtime priority diagnostics: dedicated `warn_no_rt()` on Linux, lookahead widened from 20ms to 40ms when RT priority unavailable.
|
||||
- Float epsilon precision in delta/nudge zero-comparisons.
|
||||
- Windows build fixes for standalone and plugin targets.
|
||||
|
||||
### Documentation
|
||||
- Time stretching usage guide added to `docs/engine/samples.md`.
|
||||
|
||||
## [0.1.2]
|
||||
|
||||
### Forth Language
|
||||
|
||||
74
Cargo.lock
generated
74
Cargo.lock
generated
@@ -372,6 +372,20 @@ dependencies = [
|
||||
"libloading 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asio-sys"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "826194e1612938c9be09b78b58323fbb2e326de3d491b4230186cf6e832d8ded"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"parse_cfg",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
@@ -846,7 +860,7 @@ checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21"
|
||||
|
||||
[[package]]
|
||||
name = "cagire"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"arc-swap",
|
||||
@@ -859,7 +873,7 @@ dependencies = [
|
||||
"cpal 0.17.1",
|
||||
"crossbeam-channel",
|
||||
"crossterm",
|
||||
"doux 0.0.12",
|
||||
"doux 0.0.14",
|
||||
"eframe",
|
||||
"egui",
|
||||
"egui_ratatui",
|
||||
@@ -885,7 +899,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cagire-forth"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"parking_lot",
|
||||
@@ -894,7 +908,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cagire-markdown"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"minimad",
|
||||
"ratatui",
|
||||
@@ -902,7 +916,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cagire-plugins"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"cagire",
|
||||
@@ -911,7 +925,7 @@ dependencies = [
|
||||
"cagire-ratatui",
|
||||
"crossbeam-channel",
|
||||
"crossterm",
|
||||
"doux 0.0.10",
|
||||
"doux 0.0.13",
|
||||
"egui_ratatui",
|
||||
"nih_plug",
|
||||
"nih_plug_egui",
|
||||
@@ -926,7 +940,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cagire-project"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"brotli",
|
||||
@@ -938,7 +952,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cagire-ratatui"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"rand",
|
||||
"ratatui",
|
||||
@@ -1452,6 +1466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb"
|
||||
dependencies = [
|
||||
"alsa 0.10.0",
|
||||
"asio-sys",
|
||||
"coreaudio-rs 0.13.0",
|
||||
"dasp_sample",
|
||||
"jack 0.13.5",
|
||||
@@ -1809,14 +1824,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "doux"
|
||||
version = "0.0.10"
|
||||
source = "git+https://github.com/sova-org/doux#7f4e548ae3a917e62cf4c9acb7540496684f0d8f"
|
||||
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",
|
||||
"mi-plaits-dsp",
|
||||
"ringbuf",
|
||||
"rosc",
|
||||
"rustyline",
|
||||
@@ -1826,8 +1840,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "doux"
|
||||
version = "0.0.12"
|
||||
source = "git+https://github.com/sova-org/doux?tag=v0.0.12#5b62d6634df217a00ced5e711fe98b77c9d3f79c"
|
||||
version = "0.0.14"
|
||||
source = "git+https://github.com/sova-org/doux?tag=v0.0.14#f0de4f4047adfced8fb2116edd3b33d260ba75c8"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"clap",
|
||||
@@ -1852,12 +1866,6 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "ecolor"
|
||||
version = "0.33.3"
|
||||
@@ -3255,16 +3263,6 @@ dependencies = [
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mi-plaits-dsp"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/sourcebox/mi-plaits-dsp-rs?rev=dc55bd55e73bd6f86fbbb4f8adc3b598d659fdb4#dc55bd55e73bd6f86fbbb4f8adc3b598d659fdb4"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"num-traits",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "micromath"
|
||||
version = "2.1.0"
|
||||
@@ -4135,6 +4133,15 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse_cfg"
|
||||
version = "4.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "905787a434a2c721408e7c9a252e85f3d93ca0f118a5283022636c0e05a7ea49"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
@@ -5145,15 +5152,6 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a7f4cb358863e55f8f1a3882f68601360cf6c42fc53ff2fe9aea41c33e24489"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spirv"
|
||||
version = "0.3.0+sdk-1.3.268.0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
members = ["crates/forth", "crates/markdown", "crates/project", "crates/ratatui", "plugins/cagire-plugins", "plugins/baseview", "plugins/egui-baseview", "plugins/nih-plug-egui", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
edition = "2021"
|
||||
authors = ["Raphaël Forment <raphael.forment@gmail.com>"]
|
||||
license = "AGPL-3.0"
|
||||
@@ -45,13 +45,14 @@ desktop = [
|
||||
"dep:egui_ratatui",
|
||||
"dep:image",
|
||||
]
|
||||
asio = ["doux/asio", "cpal/asio"]
|
||||
|
||||
[dependencies]
|
||||
cagire-forth = { path = "crates/forth" }
|
||||
cagire-markdown = { path = "crates/markdown" }
|
||||
cagire-project = { path = "crates/project" }
|
||||
cagire-ratatui = { path = "crates/ratatui" }
|
||||
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.12", features = ["native", "soundfont"] }
|
||||
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.14", features = ["native", "soundfont"] }
|
||||
rusty_link = "0.4"
|
||||
ratatui = "0.30"
|
||||
crossterm = "0.29"
|
||||
|
||||
@@ -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('/');
|
||||
}
|
||||
|
||||
@@ -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", features = ["native", "soundfont"] }
|
||||
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.13", 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"
|
||||
|
||||
@@ -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