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
|
echo "C:\Program Files\CMake\bin" >> $env:GITHUB_PATH
|
||||||
|
|
||||||
- name: Build
|
- 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
|
- 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
|
- name: Test
|
||||||
if: inputs.run-tests
|
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
|
- name: Clippy
|
||||||
if: inputs.run-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
|
- name: Bundle CLAP plugin
|
||||||
if: inputs.build-packages
|
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
|
- name: Install NSIS
|
||||||
if: inputs.build-packages
|
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.
|
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]
|
## [0.1.2]
|
||||||
|
|
||||||
### Forth Language
|
### Forth Language
|
||||||
|
|||||||
74
Cargo.lock
generated
74
Cargo.lock
generated
@@ -372,6 +372,20 @@ dependencies = [
|
|||||||
"libloading 0.8.9",
|
"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]]
|
[[package]]
|
||||||
name = "async-broadcast"
|
name = "async-broadcast"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
@@ -846,7 +860,7 @@ checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cagire"
|
name = "cagire"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
@@ -859,7 +873,7 @@ dependencies = [
|
|||||||
"cpal 0.17.1",
|
"cpal 0.17.1",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"doux 0.0.12",
|
"doux 0.0.14",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui_ratatui",
|
"egui_ratatui",
|
||||||
@@ -885,7 +899,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cagire-forth"
|
name = "cagire-forth"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
@@ -894,7 +908,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cagire-markdown"
|
name = "cagire-markdown"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"minimad",
|
"minimad",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
@@ -902,7 +916,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cagire-plugins"
|
name = "cagire-plugins"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"cagire",
|
"cagire",
|
||||||
@@ -911,7 +925,7 @@ dependencies = [
|
|||||||
"cagire-ratatui",
|
"cagire-ratatui",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"doux 0.0.10",
|
"doux 0.0.13",
|
||||||
"egui_ratatui",
|
"egui_ratatui",
|
||||||
"nih_plug",
|
"nih_plug",
|
||||||
"nih_plug_egui",
|
"nih_plug_egui",
|
||||||
@@ -926,7 +940,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cagire-project"
|
name = "cagire-project"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"brotli",
|
"brotli",
|
||||||
@@ -938,7 +952,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cagire-ratatui"
|
name = "cagire-ratatui"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand",
|
"rand",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
@@ -1452,6 +1466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb"
|
checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alsa 0.10.0",
|
"alsa 0.10.0",
|
||||||
|
"asio-sys",
|
||||||
"coreaudio-rs 0.13.0",
|
"coreaudio-rs 0.13.0",
|
||||||
"dasp_sample",
|
"dasp_sample",
|
||||||
"jack 0.13.5",
|
"jack 0.13.5",
|
||||||
@@ -1809,14 +1824,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "doux"
|
name = "doux"
|
||||||
version = "0.0.10"
|
version = "0.0.13"
|
||||||
source = "git+https://github.com/sova-org/doux#7f4e548ae3a917e62cf4c9acb7540496684f0d8f"
|
source = "git+https://github.com/sova-org/doux?tag=v0.0.13#b8150d907e4cc2764e82fdaa424df41ceef9b0d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"clap",
|
"clap",
|
||||||
"cpal 0.17.1",
|
"cpal 0.17.1",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"mi-plaits-dsp",
|
|
||||||
"ringbuf",
|
"ringbuf",
|
||||||
"rosc",
|
"rosc",
|
||||||
"rustyline",
|
"rustyline",
|
||||||
@@ -1826,8 +1840,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "doux"
|
name = "doux"
|
||||||
version = "0.0.12"
|
version = "0.0.14"
|
||||||
source = "git+https://github.com/sova-org/doux?tag=v0.0.12#5b62d6634df217a00ced5e711fe98b77c9d3f79c"
|
source = "git+https://github.com/sova-org/doux?tag=v0.0.14#f0de4f4047adfced8fb2116edd3b33d260ba75c8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -1852,12 +1866,6 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dyn-clone"
|
|
||||||
version = "1.0.20"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ecolor"
|
name = "ecolor"
|
||||||
version = "0.33.3"
|
version = "0.33.3"
|
||||||
@@ -3255,16 +3263,6 @@ dependencies = [
|
|||||||
"paste",
|
"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]]
|
[[package]]
|
||||||
name = "micromath"
|
name = "micromath"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
@@ -4135,6 +4133,15 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"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]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@@ -5145,15 +5152,6 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a7f4cb358863e55f8f1a3882f68601360cf6c42fc53ff2fe9aea41c33e24489"
|
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]]
|
[[package]]
|
||||||
name = "spirv"
|
name = "spirv"
|
||||||
version = "0.3.0+sdk-1.3.268.0"
|
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"]
|
members = ["crates/forth", "crates/markdown", "crates/project", "crates/ratatui", "plugins/cagire-plugins", "plugins/baseview", "plugins/egui-baseview", "plugins/nih-plug-egui", "xtask"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Raphaël Forment <raphael.forment@gmail.com>"]
|
authors = ["Raphaël Forment <raphael.forment@gmail.com>"]
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
@@ -45,13 +45,14 @@ desktop = [
|
|||||||
"dep:egui_ratatui",
|
"dep:egui_ratatui",
|
||||||
"dep:image",
|
"dep:image",
|
||||||
]
|
]
|
||||||
|
asio = ["doux/asio", "cpal/asio"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cagire-forth = { path = "crates/forth" }
|
cagire-forth = { path = "crates/forth" }
|
||||||
cagire-markdown = { path = "crates/markdown" }
|
cagire-markdown = { path = "crates/markdown" }
|
||||||
cagire-project = { path = "crates/project" }
|
cagire-project = { path = "crates/project" }
|
||||||
cagire-ratatui = { path = "crates/ratatui" }
|
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"
|
rusty_link = "0.4"
|
||||||
ratatui = "0.30"
|
ratatui = "0.30"
|
||||||
crossterm = "0.29"
|
crossterm = "0.29"
|
||||||
|
|||||||
@@ -1778,6 +1778,9 @@ fn emit_output(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (i, (k, v)) in params.iter().enumerate() {
|
for (i, (k, v)) in params.iter().enumerate() {
|
||||||
|
if v.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if !out.ends_with('/') {
|
if !out.ends_with('/') {
|
||||||
out.push('/');
|
out.push('/');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ cagire = { path = "../..", default-features = false, features = ["block-renderer
|
|||||||
cagire-forth = { path = "../../crates/forth" }
|
cagire-forth = { path = "../../crates/forth" }
|
||||||
cagire-project = { path = "../../crates/project" }
|
cagire-project = { path = "../../crates/project" }
|
||||||
cagire-ratatui = { path = "../../crates/ratatui" }
|
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 = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] }
|
||||||
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
|
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
|
||||||
egui_ratatui = "2.1"
|
egui_ratatui = "2.1"
|
||||||
|
|||||||
@@ -234,6 +234,7 @@ pub fn create_editor(
|
|||||||
// Read live snapshot from the audio thread
|
// Read live snapshot from the audio thread
|
||||||
let shared = editor.bridge.shared_state.load();
|
let shared = editor.bridge.shared_state.load();
|
||||||
editor.snapshot = SequencerSnapshot::from(shared.as_ref());
|
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
|
// Sync host tempo into LinkState so title bar shows real tempo
|
||||||
if shared.tempo > 0.0 {
|
if shared.tempo > 0.0 {
|
||||||
@@ -298,6 +299,11 @@ pub fn create_editor(
|
|||||||
let elapsed = editor.last_frame.elapsed();
|
let elapsed = editor.last_frame.elapsed();
|
||||||
editor.last_frame = Instant::now();
|
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 link = &editor.link;
|
||||||
let app = &editor.app;
|
let app = &editor.app;
|
||||||
let snapshot = &editor.snapshot;
|
let snapshot = &editor.snapshot;
|
||||||
|
|||||||
@@ -496,6 +496,11 @@ impl eframe::App for CagireDesktop {
|
|||||||
let elapsed = self.last_frame.elapsed();
|
let elapsed = self.last_frame.elapsed();
|
||||||
self.last_frame = std::time::Instant::now();
|
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 link = &self.link;
|
||||||
let app = &self.app;
|
let app = &self.app;
|
||||||
self.terminal
|
self.terminal
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ pub struct SharedSequencerState {
|
|||||||
pub event_count: usize,
|
pub event_count: usize,
|
||||||
pub tempo: f64,
|
pub tempo: f64,
|
||||||
pub beat: f64,
|
pub beat: f64,
|
||||||
|
pub playing: bool,
|
||||||
pub script_trace: Option<ExecutionTrace>,
|
pub script_trace: Option<ExecutionTrace>,
|
||||||
pub print_output: Option<String>,
|
pub print_output: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -180,6 +181,7 @@ pub struct SequencerSnapshot {
|
|||||||
pub event_count: usize,
|
pub event_count: usize,
|
||||||
pub tempo: f64,
|
pub tempo: f64,
|
||||||
pub beat: f64,
|
pub beat: f64,
|
||||||
|
pub playing: bool,
|
||||||
script_trace: Option<ExecutionTrace>,
|
script_trace: Option<ExecutionTrace>,
|
||||||
pub print_output: Option<String>,
|
pub print_output: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -192,6 +194,7 @@ impl From<&SharedSequencerState> for SequencerSnapshot {
|
|||||||
event_count: s.event_count,
|
event_count: s.event_count,
|
||||||
tempo: s.tempo,
|
tempo: s.tempo,
|
||||||
beat: s.beat,
|
beat: s.beat,
|
||||||
|
playing: s.playing,
|
||||||
script_trace: s.script_trace.clone(),
|
script_trace: s.script_trace.clone(),
|
||||||
print_output: s.print_output.clone(),
|
print_output: s.print_output.clone(),
|
||||||
}
|
}
|
||||||
@@ -207,6 +210,7 @@ impl SequencerSnapshot {
|
|||||||
event_count: 0,
|
event_count: 0,
|
||||||
tempo: 0.0,
|
tempo: 0.0,
|
||||||
beat: 0.0,
|
beat: 0.0,
|
||||||
|
playing: false,
|
||||||
script_trace: None,
|
script_trace: None,
|
||||||
print_output: None,
|
print_output: None,
|
||||||
}
|
}
|
||||||
@@ -306,6 +310,7 @@ struct PendingPattern {
|
|||||||
|
|
||||||
struct AudioState {
|
struct AudioState {
|
||||||
prev_beat: f64,
|
prev_beat: f64,
|
||||||
|
pause_beat: Option<f64>,
|
||||||
active_patterns: HashMap<PatternId, ActivePattern>,
|
active_patterns: HashMap<PatternId, ActivePattern>,
|
||||||
pending_starts: Vec<PendingPattern>,
|
pending_starts: Vec<PendingPattern>,
|
||||||
pending_stops: Vec<PendingPattern>,
|
pending_stops: Vec<PendingPattern>,
|
||||||
@@ -316,6 +321,7 @@ impl AudioState {
|
|||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
prev_beat: -1.0,
|
prev_beat: -1.0,
|
||||||
|
pause_beat: None,
|
||||||
active_patterns: HashMap::new(),
|
active_patterns: HashMap::new(),
|
||||||
pending_starts: Vec::new(),
|
pending_starts: Vec::new(),
|
||||||
pending_stops: Vec::new(),
|
pending_stops: Vec::new(),
|
||||||
@@ -572,6 +578,7 @@ pub struct SequencerState {
|
|||||||
soloed: std::collections::HashSet<(usize, usize)>,
|
soloed: std::collections::HashSet<(usize, usize)>,
|
||||||
last_tempo: f64,
|
last_tempo: f64,
|
||||||
last_beat: f64,
|
last_beat: f64,
|
||||||
|
last_playing: bool,
|
||||||
script_text: String,
|
script_text: String,
|
||||||
script_speed: crate::model::PatternSpeed,
|
script_speed: crate::model::PatternSpeed,
|
||||||
script_length: usize,
|
script_length: usize,
|
||||||
@@ -610,6 +617,7 @@ impl SequencerState {
|
|||||||
soloed: std::collections::HashSet::new(),
|
soloed: std::collections::HashSet::new(),
|
||||||
last_tempo: 0.0,
|
last_tempo: 0.0,
|
||||||
last_beat: 0.0,
|
last_beat: 0.0,
|
||||||
|
last_playing: false,
|
||||||
script_text: String::new(),
|
script_text: String::new(),
|
||||||
script_speed: crate::model::PatternSpeed::default(),
|
script_speed: crate::model::PatternSpeed::default(),
|
||||||
script_length: 16,
|
script_length: 16,
|
||||||
@@ -714,6 +722,7 @@ impl SequencerState {
|
|||||||
self.audio_state.active_patterns.clear();
|
self.audio_state.active_patterns.clear();
|
||||||
self.audio_state.pending_starts.clear();
|
self.audio_state.pending_starts.clear();
|
||||||
self.audio_state.pending_stops.clear();
|
self.audio_state.pending_stops.clear();
|
||||||
|
self.audio_state.pause_beat = None;
|
||||||
self.step_traces = Arc::new(HashMap::new());
|
self.step_traces = Arc::new(HashMap::new());
|
||||||
self.runs_counter.counts.clear();
|
self.runs_counter.counts.clear();
|
||||||
self.audio_state.flush_midi_notes = true;
|
self.audio_state.flush_midi_notes = true;
|
||||||
@@ -724,6 +733,7 @@ impl SequencerState {
|
|||||||
active.iter = 0;
|
active.iter = 0;
|
||||||
}
|
}
|
||||||
self.audio_state.prev_beat = -1.0;
|
self.audio_state.prev_beat = -1.0;
|
||||||
|
self.audio_state.pause_beat = None;
|
||||||
self.script_frontier = -1.0;
|
self.script_frontier = -1.0;
|
||||||
self.script_step = 0;
|
self.script_step = 0;
|
||||||
self.script_trace = None;
|
self.script_trace = None;
|
||||||
@@ -751,6 +761,7 @@ impl SequencerState {
|
|||||||
self.process_commands(input.commands);
|
self.process_commands(input.commands);
|
||||||
self.last_tempo = input.tempo;
|
self.last_tempo = input.tempo;
|
||||||
self.last_beat = input.beat;
|
self.last_beat = input.beat;
|
||||||
|
self.last_playing = input.playing;
|
||||||
|
|
||||||
if !input.playing {
|
if !input.playing {
|
||||||
return self.tick_paused();
|
return self.tick_paused();
|
||||||
@@ -758,14 +769,21 @@ 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;
|
||||||
|
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.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(
|
let steps = self.execute_steps(
|
||||||
input.beat,
|
input.beat,
|
||||||
frontier,
|
frontier,
|
||||||
@@ -822,6 +840,9 @@ impl SequencerState {
|
|||||||
self.pattern_cache.set(key.0, key.1, snapshot);
|
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.audio_state.prev_beat = -1.0;
|
||||||
self.script_frontier = -1.0;
|
self.script_frontier = -1.0;
|
||||||
self.script_step = 0;
|
self.script_step = 0;
|
||||||
@@ -1240,6 +1261,7 @@ impl SequencerState {
|
|||||||
event_count: self.event_count,
|
event_count: self.event_count,
|
||||||
tempo: self.last_tempo,
|
tempo: self.last_tempo,
|
||||||
beat: self.last_beat,
|
beat: self.last_beat,
|
||||||
|
playing: self.last_playing,
|
||||||
script_trace: self.script_trace.clone(),
|
script_trace: self.script_trace.clone(),
|
||||||
print_output: self.print_output.clone(),
|
print_output: self.print_output.clone(),
|
||||||
}
|
}
|
||||||
@@ -1975,10 +1997,8 @@ mod tests {
|
|||||||
|
|
||||||
assert!(!state.audio_state.pending_starts.is_empty());
|
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));
|
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)));
|
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)
|
// Should have commands from both patterns (2 patterns * 1 command each)
|
||||||
assert!(output.audio_commands.len() >= 2);
|
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