Compare commits
17 Commits
v0.1.4
...
260bc9dbdf
| Author | SHA1 | Date | |
|---|---|---|---|
| 260bc9dbdf | |||
| 68bd62f57f | |||
| f1c83c66a0 | |||
| 30dfe7372d | |||
| faf541e536 | |||
| 85cacfe53e | |||
| c507552b7c | |||
| d0b2076bf6 | |||
| ab93acd17f | |||
| d72b36b8f1 | |||
| 3d9d2ad759 | |||
| 5b1353f7e7 | |||
| f78b4374b6 | |||
| dacc9bd6be | |||
| bfd52c0053 | |||
| 12172ce1e8 | |||
| 1513d80a8d |
@@ -22,6 +22,7 @@ All notable changes to this project will be documented in this file.
|
||||
### Engine
|
||||
- Audio input channel selection support.
|
||||
- Audio buffer sizing improved for multi-channel input.
|
||||
- MIDI output sends directly from dispatcher thread, bypassing UI-thread polling (~30x less jitter).
|
||||
|
||||
### Packaging
|
||||
- CI migrated from GitHub Actions to Gitea Actions.
|
||||
|
||||
24
Cargo.lock
generated
24
Cargo.lock
generated
@@ -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",
|
||||
@@ -1824,24 +1824,8 @@ dependencies = [
|
||||
|
||||
[[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"
|
||||
source = "git+https://github.com/sova-org/doux?tag=v0.0.14#f0de4f4047adfced8fb2116edd3b33d260ba75c8"
|
||||
version = "0.0.15"
|
||||
source = "git+https://github.com/sova-org/doux?tag=v0.0.15#29d8f055612f6141d7546d72b91e60026937b0fd"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"clap",
|
||||
|
||||
@@ -36,13 +36,12 @@ required-features = ["desktop"]
|
||||
[features]
|
||||
default = ["cli"]
|
||||
cli = ["dep:cpal", "dep:midir", "dep:confy", "dep:clap", "dep:thread-priority"]
|
||||
block-renderer = ["dep:soft_ratatui", "dep:rustc-hash", "dep:egui"]
|
||||
block-renderer = ["dep:soft_ratatui", "dep:rustc-hash", "dep:egui", "dep:egui_ratatui"]
|
||||
desktop = [
|
||||
"cli",
|
||||
"block-renderer",
|
||||
"cagire-forth/desktop",
|
||||
"dep:eframe",
|
||||
"dep:egui_ratatui",
|
||||
"dep:image",
|
||||
]
|
||||
asio = ["doux/asio", "cpal/asio"]
|
||||
@@ -52,7 +51,7 @@ 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.14", features = ["native", "soundfont"] }
|
||||
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.15", features = ["native", "soundfont"] }
|
||||
rusty_link = "0.4"
|
||||
ratatui = "0.30"
|
||||
crossterm = "0.29"
|
||||
@@ -92,7 +91,7 @@ winres = "0.1"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
||||
@@ -1187,7 +1187,7 @@ impl Forth {
|
||||
}
|
||||
let dur = steps * ctx.step_duration();
|
||||
cmd.set_param("fit", Value::Float(dur, None));
|
||||
cmd.set_param("dur", Value::Float(dur, None));
|
||||
cmd.set_param("gate", Value::Float(steps, None));
|
||||
}
|
||||
|
||||
Op::At => {
|
||||
@@ -1753,7 +1753,7 @@ fn cmd_param_float(cmd: &CmdRegister, name: &str) -> Option<f64> {
|
||||
fn is_tempo_scaled_param(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"attack" | "decay" | "release" | "envdelay" | "hold" | "chorusdelay"
|
||||
"attack" | "decay" | "release" | "envdelay" | "hold" | "chorusdelay" | "gate"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1769,7 +1769,7 @@ fn emit_output(
|
||||
let mut out = String::with_capacity(128);
|
||||
out.push('/');
|
||||
|
||||
let has_dur = params.iter().any(|(k, _)| *k == "dur");
|
||||
let has_gate = params.iter().any(|(k, _)| *k == "gate");
|
||||
let has_release = params.iter().any(|(k, _)| *k == "release");
|
||||
let delaytime_idx = params.iter().position(|(k, _)| *k == "delaytime");
|
||||
|
||||
@@ -1806,11 +1806,11 @@ fn emit_output(
|
||||
let _ = write!(&mut out, "delta/{delta_ticks}");
|
||||
}
|
||||
|
||||
if !has_dur {
|
||||
if !has_gate {
|
||||
if !out.ends_with('/') {
|
||||
out.push('/');
|
||||
}
|
||||
let _ = write!(&mut out, "dur/{}", step_duration * 4.0);
|
||||
let _ = write!(&mut out, "gate/{}", step_duration * 4.0);
|
||||
}
|
||||
|
||||
if !has_release {
|
||||
|
||||
@@ -131,7 +131,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set duration",
|
||||
desc: "Set MIDI note duration (for audio, use gate)",
|
||||
example: "0.5 dur",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
|
||||
@@ -14,4 +14,4 @@ pub const MAX_STEPS: usize = 1024;
|
||||
pub const DEFAULT_LENGTH: usize = 16;
|
||||
|
||||
pub use file::{load, load_str, save, FileError};
|
||||
pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};
|
||||
pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step};
|
||||
|
||||
@@ -206,39 +206,6 @@ impl LaunchQuantization {
|
||||
}
|
||||
}
|
||||
|
||||
/// How a pattern synchronizes when launched: restart or phase-lock.
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub enum SyncMode {
|
||||
#[default]
|
||||
Reset,
|
||||
PhaseLock,
|
||||
}
|
||||
|
||||
impl SyncMode {
|
||||
/// Human-readable label for display.
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Reset => "Reset",
|
||||
Self::PhaseLock => "Phase-Lock",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Reset => "Rst",
|
||||
Self::PhaseLock => "Plk",
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle between Reset and PhaseLock.
|
||||
pub fn toggle(&self) -> Self {
|
||||
match self {
|
||||
Self::Reset => Self::PhaseLock,
|
||||
Self::PhaseLock => Self::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What happens when a pattern finishes: loop, stop, or chain to another.
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub enum FollowUp {
|
||||
@@ -315,7 +282,7 @@ impl Default for Step {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sequence of steps with playback settings (speed, quantization, sync, follow-up).
|
||||
/// Sequence of steps with playback settings (speed, quantization, follow-up).
|
||||
#[derive(Clone)]
|
||||
pub struct Pattern {
|
||||
pub steps: Vec<Step>,
|
||||
@@ -324,7 +291,6 @@ pub struct Pattern {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
pub follow_up: FollowUp,
|
||||
}
|
||||
|
||||
@@ -361,8 +327,6 @@ struct SparsePattern {
|
||||
description: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_default_quantization")]
|
||||
quantization: LaunchQuantization,
|
||||
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
|
||||
sync_mode: SyncMode,
|
||||
#[serde(default, skip_serializing_if = "is_default_follow_up")]
|
||||
follow_up: FollowUp,
|
||||
}
|
||||
@@ -371,10 +335,6 @@ fn is_default_quantization(q: &LaunchQuantization) -> bool {
|
||||
*q == LaunchQuantization::default()
|
||||
}
|
||||
|
||||
fn is_default_sync_mode(s: &SyncMode) -> bool {
|
||||
*s == SyncMode::default()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LegacyPattern {
|
||||
steps: Vec<Step>,
|
||||
@@ -388,8 +348,6 @@ struct LegacyPattern {
|
||||
#[serde(default)]
|
||||
quantization: LaunchQuantization,
|
||||
#[serde(default)]
|
||||
sync_mode: SyncMode,
|
||||
#[serde(default)]
|
||||
follow_up: FollowUp,
|
||||
}
|
||||
|
||||
@@ -416,7 +374,6 @@ impl Serialize for Pattern {
|
||||
name: self.name.clone(),
|
||||
description: self.description.clone(),
|
||||
quantization: self.quantization,
|
||||
sync_mode: self.sync_mode,
|
||||
follow_up: self.follow_up,
|
||||
};
|
||||
sparse.serialize(serializer)
|
||||
@@ -452,7 +409,6 @@ impl<'de> Deserialize<'de> for Pattern {
|
||||
name: sparse.name,
|
||||
description: sparse.description,
|
||||
quantization: sparse.quantization,
|
||||
sync_mode: sparse.sync_mode,
|
||||
follow_up: sparse.follow_up,
|
||||
})
|
||||
}
|
||||
@@ -463,7 +419,6 @@ impl<'de> Deserialize<'de> for Pattern {
|
||||
name: legacy.name,
|
||||
description: legacy.description,
|
||||
quantization: legacy.quantization,
|
||||
sync_mode: legacy.sync_mode,
|
||||
follow_up: legacy.follow_up,
|
||||
}),
|
||||
}
|
||||
@@ -479,7 +434,6 @@ impl Default for Pattern {
|
||||
name: None,
|
||||
description: None,
|
||||
quantization: LaunchQuantization::default(),
|
||||
sync_mode: SyncMode::default(),
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ mod tests {
|
||||
for i in 0..16 {
|
||||
pattern.steps[i] = Step {
|
||||
active: true,
|
||||
script: format!("kick {i} note 0.5 dur"),
|
||||
script: format!("kick {i} note 0.5 gate"),
|
||||
source: None,
|
||||
name: Some(format!("step_{i}")),
|
||||
};
|
||||
|
||||
@@ -28,7 +28,6 @@ Each pattern is an independent sequence of steps with its own properties:
|
||||
| Length | Steps before the pattern loops (`1`-`1024`) | `16` |
|
||||
| Speed | Playback rate (`1/8x` to `8x`) | `1x` |
|
||||
| Quantization | When the pattern launches | `Bar` |
|
||||
| Sync Mode | Reset or Phase-Lock on re-trigger | `Reset` |
|
||||
| Follow Up | What happens when the pattern finishes an iteration | `Loop` |
|
||||
|
||||
Press `e` in the patterns view to edit these settings. After editing properties, you will have to hit the `c` key to _launch_ these changes. More about that later!
|
||||
|
||||
@@ -51,7 +51,7 @@ Cagire includes a complete synthesis and sampling engine. No external software i
|
||||
|
||||
```forth
|
||||
;; sawtooth wave + lowpass filter with envelope + chorus + reverb
|
||||
100 199 freq saw sound 250 8000 0.01 0.3 0.5 0.3 env lpf 0.2 chorus 0.8 verb 2 dur .
|
||||
100 199 freq saw sound 250 8000 0.01 0.3 0.5 0.3 env lpf 0.2 chorus 0.8 verb 2 gate .
|
||||
```
|
||||
|
||||
```forth
|
||||
@@ -61,7 +61,7 @@ Cagire includes a complete synthesis and sampling engine. No external software i
|
||||
|
||||
```forth
|
||||
;; white noise + sine wave + envelope = percussion
|
||||
white sine sound 100 freq 0.5 decay 2 dur .
|
||||
white sine sound 100 freq 0.5 decay 2 gate .
|
||||
```
|
||||
|
||||
```forth
|
||||
|
||||
@@ -33,7 +33,7 @@ You can also arm mute/solo changes:
|
||||
- Press `Shift+m` to clear all mutes
|
||||
- Press `Shift+x` to clear all solos
|
||||
|
||||
A pattern might not start immediately depending on the sync mode you have chosen.
|
||||
A pattern might not start immediately depending on its quantization setting.
|
||||
It might wait for the next beat/bar boundary.
|
||||
|
||||
## Status Indicators
|
||||
@@ -63,9 +63,4 @@ Launched changes don't execute immediately. They wait for a quantization boundar
|
||||
|
||||
Edit quantization in pattern properties (press `e` on a pattern).
|
||||
|
||||
## Sync Mode
|
||||
|
||||
When a pattern starts, its playback position depends on sync mode:
|
||||
|
||||
- **Reset**: Always start at step 0
|
||||
- **Phase-Lock**: Start at the current beat-aligned position (stays in sync with other patterns)
|
||||
Patterns always start at a beat-aligned position (phase-lock), staying in sync with other running patterns.
|
||||
|
||||
@@ -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.15", 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"
|
||||
|
||||
@@ -6,128 +6,22 @@ use std::time::Instant;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use crossbeam_channel::Sender;
|
||||
use egui_ratatui::RataguiBackend;
|
||||
use nih_plug::prelude::*;
|
||||
use nih_plug_egui::egui;
|
||||
use nih_plug_egui::{create_egui_editor, EguiState};
|
||||
use ratatui::Terminal;
|
||||
use soft_ratatui::embedded_graphics_unicodefonts::{
|
||||
mono_10x20_atlas, mono_6x13_atlas, mono_6x13_bold_atlas, mono_6x13_italic_atlas,
|
||||
mono_7x13_atlas, mono_7x13_bold_atlas, mono_7x13_italic_atlas, mono_8x13_atlas,
|
||||
mono_8x13_bold_atlas, mono_8x13_italic_atlas, mono_9x15_atlas, mono_9x15_bold_atlas,
|
||||
mono_9x18_atlas, mono_9x18_bold_atlas,
|
||||
};
|
||||
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
|
||||
|
||||
use cagire::block_renderer::BlockCharBackend;
|
||||
use cagire::app::App;
|
||||
use cagire::engine::{AudioCommand, LinkState, SequencerSnapshot};
|
||||
use cagire::input::{handle_key, handle_mouse, InputContext};
|
||||
use cagire::input_egui::{convert_egui_events, convert_egui_mouse, EguiMouseState};
|
||||
use cagire::model::{Dictionary, Rng, Variables};
|
||||
use cagire::terminal::{create_terminal, FontChoice, TerminalType};
|
||||
use cagire::theme;
|
||||
use cagire::views;
|
||||
|
||||
use cagire::input_egui::{convert_egui_events, convert_egui_mouse, EguiMouseState};
|
||||
use crate::params::CagireParams;
|
||||
use crate::PluginBridge;
|
||||
|
||||
type TerminalType = Terminal<RataguiBackend<BlockCharBackend>>;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum FontChoice {
|
||||
Size6x13,
|
||||
Size7x13,
|
||||
Size8x13,
|
||||
Size9x15,
|
||||
Size9x18,
|
||||
Size10x20,
|
||||
}
|
||||
|
||||
impl FontChoice {
|
||||
fn from_setting(s: &str) -> Self {
|
||||
match s {
|
||||
"6x13" => Self::Size6x13,
|
||||
"7x13" => Self::Size7x13,
|
||||
"9x15" => Self::Size9x15,
|
||||
"9x18" => Self::Size9x18,
|
||||
"10x20" => Self::Size10x20,
|
||||
_ => Self::Size8x13,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_setting(self) -> &'static str {
|
||||
match self {
|
||||
Self::Size6x13 => "6x13",
|
||||
Self::Size7x13 => "7x13",
|
||||
Self::Size8x13 => "8x13",
|
||||
Self::Size9x15 => "9x15",
|
||||
Self::Size9x18 => "9x18",
|
||||
Self::Size10x20 => "10x20",
|
||||
}
|
||||
}
|
||||
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Size6x13 => "6x13 (Compact)",
|
||||
Self::Size7x13 => "7x13",
|
||||
Self::Size8x13 => "8x13 (Default)",
|
||||
Self::Size9x15 => "9x15",
|
||||
Self::Size9x18 => "9x18",
|
||||
Self::Size10x20 => "10x20 (Large)",
|
||||
}
|
||||
}
|
||||
|
||||
const ALL: [Self; 6] = [
|
||||
Self::Size6x13,
|
||||
Self::Size7x13,
|
||||
Self::Size8x13,
|
||||
Self::Size9x15,
|
||||
Self::Size9x18,
|
||||
Self::Size10x20,
|
||||
];
|
||||
}
|
||||
|
||||
fn create_terminal(font: FontChoice) -> TerminalType {
|
||||
let (regular, bold, italic) = match font {
|
||||
FontChoice::Size6x13 => (
|
||||
mono_6x13_atlas(),
|
||||
Some(mono_6x13_bold_atlas()),
|
||||
Some(mono_6x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size7x13 => (
|
||||
mono_7x13_atlas(),
|
||||
Some(mono_7x13_bold_atlas()),
|
||||
Some(mono_7x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size8x13 => (
|
||||
mono_8x13_atlas(),
|
||||
Some(mono_8x13_bold_atlas()),
|
||||
Some(mono_8x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size9x15 => (mono_9x15_atlas(), Some(mono_9x15_bold_atlas()), None),
|
||||
FontChoice::Size9x18 => (mono_9x18_atlas(), Some(mono_9x18_bold_atlas()), None),
|
||||
FontChoice::Size10x20 => (mono_10x20_atlas(), None, None),
|
||||
};
|
||||
|
||||
let eg = SoftBackend::<EmbeddedGraphics>::new(80, 24, regular, bold, italic);
|
||||
let soft = SoftBackend {
|
||||
buffer: eg.buffer,
|
||||
cursor: eg.cursor,
|
||||
cursor_pos: eg.cursor_pos,
|
||||
char_width: eg.char_width,
|
||||
char_height: eg.char_height,
|
||||
blink_counter: eg.blink_counter,
|
||||
blinking_fast: eg.blinking_fast,
|
||||
blinking_slow: eg.blinking_slow,
|
||||
rgb_pixmap: eg.rgb_pixmap,
|
||||
always_redraw_list: eg.always_redraw_list,
|
||||
raster_backend: BlockCharBackend {
|
||||
inner: eg.raster_backend,
|
||||
},
|
||||
};
|
||||
Terminal::new(RataguiBackend::new("cagire", soft)).expect("terminal")
|
||||
}
|
||||
|
||||
struct EditorState {
|
||||
app: App,
|
||||
terminal: TerminalType,
|
||||
@@ -234,7 +128,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;
|
||||
editor.app.playback.playing = editor.playing.load(std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
// Sync host tempo into LinkState so title bar shows real tempo
|
||||
if shared.tempo > 0.0 {
|
||||
|
||||
@@ -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
|
||||
@@ -219,7 +220,6 @@ impl Plugin for CagirePlugin {
|
||||
source: s.source,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: pat.sync_mode,
|
||||
follow_up: pat.follow_up,
|
||||
};
|
||||
let _ = self.bridge.cmd_tx.send(SeqCommand::PatternUpdate {
|
||||
@@ -292,7 +292,7 @@ impl Plugin for CagirePlugin {
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
audio_sample_pos: self.sample_pos,
|
||||
corrected_audio_pos: self.sample_pos as f64,
|
||||
sr: self.sample_rate as f64,
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
|
||||
BIN
scripts/__pycache__/build.cpython-313.pyc
Normal file
BIN
scripts/__pycache__/build.cpython-313.pyc
Normal file
Binary file not shown.
BIN
scripts/__pycache__/build.cpython-314.pyc
Normal file
BIN
scripts/__pycache__/build.cpython-314.pyc
Normal file
Binary file not shown.
@@ -1,473 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export MACOSX_DEPLOYMENT_TARGET="12.0"
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
PLUGIN_NAME="cagire-plugins"
|
||||
LIB_NAME="cagire_plugins" # cargo converts hyphens to underscores
|
||||
OUT="releases"
|
||||
|
||||
PLATFORMS=(
|
||||
"aarch64-apple-darwin"
|
||||
"x86_64-apple-darwin"
|
||||
"x86_64-unknown-linux-gnu"
|
||||
"aarch64-unknown-linux-gnu"
|
||||
"x86_64-pc-windows-gnu"
|
||||
)
|
||||
|
||||
PLATFORM_LABELS=(
|
||||
"macOS aarch64 (native)"
|
||||
"macOS x86_64 (native)"
|
||||
"Linux x86_64 (cross)"
|
||||
"Linux aarch64 (cross)"
|
||||
"Windows x86_64 (cross)"
|
||||
)
|
||||
|
||||
PLATFORM_ALIASES=(
|
||||
"macos-arm64"
|
||||
"macos-x86_64"
|
||||
"linux-x86_64"
|
||||
"linux-aarch64"
|
||||
"windows-x86_64"
|
||||
)
|
||||
|
||||
# --- CLI argument parsing ---
|
||||
|
||||
cli_platforms=""
|
||||
cli_targets=""
|
||||
cli_yes=false
|
||||
cli_all=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--platforms) cli_platforms="$2"; shift 2 ;;
|
||||
--targets) cli_targets="$2"; shift 2 ;;
|
||||
--yes) cli_yes=true; shift ;;
|
||||
--all) cli_all=true; shift ;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --platforms <list> Comma-separated: macos-arm64,macos-x86_64,linux-x86_64,linux-aarch64,windows-x86_64"
|
||||
echo " --targets <list> Comma-separated: cli,desktop,plugins"
|
||||
echo " --all Build all platforms and targets"
|
||||
echo " --yes Skip confirmation prompt"
|
||||
echo ""
|
||||
echo "Without options, runs interactively."
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
resolve_platform_alias() {
|
||||
local alias="$1"
|
||||
for i in "${!PLATFORM_ALIASES[@]}"; do
|
||||
if [[ "${PLATFORM_ALIASES[$i]}" == "$alias" ]]; then
|
||||
echo "$i"
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo "Unknown platform: $alias" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Helpers ---
|
||||
|
||||
prompt_platforms() {
|
||||
echo "Select platform (0=all, comma-separated):"
|
||||
echo " 0) All"
|
||||
for i in "${!PLATFORMS[@]}"; do
|
||||
echo " $((i+1))) ${PLATFORM_LABELS[$i]}"
|
||||
done
|
||||
read -rp "> " choice
|
||||
|
||||
if [[ "$choice" == "0" || -z "$choice" ]]; then
|
||||
selected_platforms=("${PLATFORMS[@]}")
|
||||
selected_labels=("${PLATFORM_LABELS[@]}")
|
||||
else
|
||||
IFS=',' read -ra indices <<< "$choice"
|
||||
selected_platforms=()
|
||||
selected_labels=()
|
||||
for idx in "${indices[@]}"; do
|
||||
idx="${idx// /}"
|
||||
idx=$((idx - 1))
|
||||
if (( idx < 0 || idx >= ${#PLATFORMS[@]} )); then
|
||||
echo "Invalid platform index: $((idx+1))"
|
||||
exit 1
|
||||
fi
|
||||
selected_platforms+=("${PLATFORMS[$idx]}")
|
||||
selected_labels+=("${PLATFORM_LABELS[$idx]}")
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_targets() {
|
||||
echo ""
|
||||
echo "Select targets (0=all, comma-separated):"
|
||||
echo " 0) All"
|
||||
echo " 1) cagire"
|
||||
echo " 2) cagire-desktop"
|
||||
echo " 3) cagire-plugins (CLAP/VST3)"
|
||||
read -rp "> " choice
|
||||
|
||||
build_cagire=false
|
||||
build_desktop=false
|
||||
build_plugins=false
|
||||
|
||||
if [[ "$choice" == "0" || -z "$choice" ]]; then
|
||||
build_cagire=true
|
||||
build_desktop=true
|
||||
build_plugins=true
|
||||
else
|
||||
IFS=',' read -ra targets <<< "$choice"
|
||||
for t in "${targets[@]}"; do
|
||||
t="${t// /}"
|
||||
case "$t" in
|
||||
1) build_cagire=true ;;
|
||||
2) build_desktop=true ;;
|
||||
3) build_plugins=true ;;
|
||||
*) echo "Invalid target: $t"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
confirm_summary() {
|
||||
echo ""
|
||||
echo "=== Build Summary ==="
|
||||
echo ""
|
||||
echo "Platforms:"
|
||||
for label in "${selected_labels[@]}"; do
|
||||
echo " - $label"
|
||||
done
|
||||
echo ""
|
||||
echo "Targets:"
|
||||
$build_cagire && echo " - cagire"
|
||||
$build_desktop && echo " - cagire-desktop"
|
||||
$build_plugins && echo " - cagire-plugins (CLAP/VST3)"
|
||||
echo ""
|
||||
read -rp "Proceed? [Y/n] " yn
|
||||
case "${yn,,}" in
|
||||
n|no) echo "Aborted."; exit 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
platform_os() {
|
||||
case "$1" in
|
||||
*windows*) echo "windows" ;;
|
||||
*linux*) echo "linux" ;;
|
||||
*apple*) echo "macos" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
platform_arch() {
|
||||
case "$1" in
|
||||
aarch64*) echo "aarch64" ;;
|
||||
x86_64*) echo "x86_64" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
platform_suffix() {
|
||||
case "$1" in
|
||||
*windows*) echo ".exe" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_cross_target() {
|
||||
case "$1" in
|
||||
*linux*|*windows*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
native_target() {
|
||||
[[ "$1" == "aarch64-apple-darwin" ]]
|
||||
}
|
||||
|
||||
release_dir() {
|
||||
if native_target "$1"; then
|
||||
echo "target/release"
|
||||
else
|
||||
echo "target/$1/release"
|
||||
fi
|
||||
}
|
||||
|
||||
target_flag() {
|
||||
if native_target "$1"; then
|
||||
echo ""
|
||||
else
|
||||
echo "--target $1"
|
||||
fi
|
||||
}
|
||||
|
||||
builder_for() {
|
||||
if is_cross_target "$1"; then
|
||||
echo "cross"
|
||||
else
|
||||
echo "cargo"
|
||||
fi
|
||||
}
|
||||
|
||||
build_binary() {
|
||||
local platform="$1"
|
||||
shift
|
||||
local builder
|
||||
builder=$(builder_for "$platform")
|
||||
local tf
|
||||
tf=$(target_flag "$platform")
|
||||
# shellcheck disable=SC2086
|
||||
$builder build --release $tf "$@"
|
||||
}
|
||||
|
||||
bundle_plugins_native() {
|
||||
local platform="$1"
|
||||
local tf
|
||||
tf=$(target_flag "$platform")
|
||||
# shellcheck disable=SC2086
|
||||
cargo xtask bundle "$PLUGIN_NAME" --release $tf
|
||||
}
|
||||
|
||||
bundle_desktop_native() {
|
||||
local platform="$1"
|
||||
local tf
|
||||
tf=$(target_flag "$platform")
|
||||
# shellcheck disable=SC2086
|
||||
cargo bundle --release --features desktop --bin cagire-desktop $tf
|
||||
}
|
||||
|
||||
bundle_plugins_cross() {
|
||||
local platform="$1"
|
||||
local rd
|
||||
rd=$(release_dir "$platform")
|
||||
local os
|
||||
os=$(platform_os "$platform")
|
||||
local arch
|
||||
arch=$(platform_arch "$platform")
|
||||
|
||||
# Build the cdylib with cross
|
||||
# shellcheck disable=SC2046
|
||||
build_binary "$platform" -p "$PLUGIN_NAME"
|
||||
|
||||
# Determine source library file
|
||||
local src_lib
|
||||
case "$os" in
|
||||
linux) src_lib="$rd/lib${LIB_NAME}.so" ;;
|
||||
windows) src_lib="$rd/${LIB_NAME}.dll" ;;
|
||||
esac
|
||||
|
||||
if [[ ! -f "$src_lib" ]]; then
|
||||
echo " ERROR: Expected library not found: $src_lib"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Assemble CLAP bundle (flat file)
|
||||
local clap_out="$OUT/${PLUGIN_NAME}-${os}-${arch}.clap"
|
||||
cp "$src_lib" "$clap_out"
|
||||
echo " CLAP -> $clap_out"
|
||||
|
||||
# Assemble VST3 bundle (directory tree)
|
||||
local vst3_dir="$OUT/${PLUGIN_NAME}-${os}-${arch}.vst3"
|
||||
local vst3_contents
|
||||
case "$os" in
|
||||
linux)
|
||||
vst3_contents="$vst3_dir/Contents/${arch}-linux"
|
||||
mkdir -p "$vst3_contents"
|
||||
cp "$src_lib" "$vst3_contents/${PLUGIN_NAME}.so"
|
||||
;;
|
||||
windows)
|
||||
vst3_contents="$vst3_dir/Contents/${arch}-win"
|
||||
mkdir -p "$vst3_contents"
|
||||
cp "$src_lib" "$vst3_contents/${PLUGIN_NAME}.vst3"
|
||||
;;
|
||||
esac
|
||||
echo " VST3 -> $vst3_dir/"
|
||||
}
|
||||
|
||||
copy_artifacts() {
|
||||
local platform="$1"
|
||||
local rd
|
||||
rd=$(release_dir "$platform")
|
||||
local os
|
||||
os=$(platform_os "$platform")
|
||||
local arch
|
||||
arch=$(platform_arch "$platform")
|
||||
local suffix
|
||||
suffix=$(platform_suffix "$platform")
|
||||
|
||||
if $build_cagire; then
|
||||
local src="$rd/cagire${suffix}"
|
||||
local dst="$OUT/cagire-${os}-${arch}${suffix}"
|
||||
cp "$src" "$dst"
|
||||
echo " cagire -> $dst"
|
||||
fi
|
||||
|
||||
if $build_desktop; then
|
||||
local src="$rd/cagire-desktop${suffix}"
|
||||
local dst="$OUT/cagire-desktop-${os}-${arch}${suffix}"
|
||||
cp "$src" "$dst"
|
||||
echo " cagire-desktop -> $dst"
|
||||
|
||||
# macOS .app bundle
|
||||
if [[ "$os" == "macos" ]]; then
|
||||
local app_src="$rd/bundle/osx/Cagire.app"
|
||||
if [[ ! -d "$app_src" ]]; then
|
||||
echo " ERROR: .app bundle not found at $app_src"
|
||||
echo " Did 'cargo bundle' succeed?"
|
||||
return 1
|
||||
fi
|
||||
local app_dst="$OUT/Cagire-${arch}.app"
|
||||
rm -rf "$app_dst"
|
||||
cp -R "$app_src" "$app_dst"
|
||||
echo " Cagire.app -> $app_dst"
|
||||
scripts/make-dmg.sh "$app_dst" "$OUT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# NSIS installer for Windows targets
|
||||
if [[ "$os" == "windows" ]] && command -v makensis &>/dev/null; then
|
||||
echo " Building NSIS installer..."
|
||||
local version
|
||||
version=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
local abs_root
|
||||
abs_root=$(pwd)
|
||||
makensis -DVERSION="$version" \
|
||||
-DCLI_EXE="$abs_root/$rd/cagire.exe" \
|
||||
-DDESKTOP_EXE="$abs_root/$rd/cagire-desktop.exe" \
|
||||
-DICON="$abs_root/assets/Cagire.ico" \
|
||||
-DOUTDIR="$abs_root/$OUT" \
|
||||
nsis/cagire.nsi
|
||||
echo " Installer -> $OUT/cagire-${version}-windows-x86_64-setup.exe"
|
||||
fi
|
||||
|
||||
# AppImage for Linux targets
|
||||
if [[ "$os" == "linux" ]]; then
|
||||
if $build_cagire; then
|
||||
scripts/make-appimage.sh "$rd/cagire" "$arch" "$OUT"
|
||||
fi
|
||||
if $build_desktop; then
|
||||
scripts/make-appimage.sh "$rd/cagire-desktop" "$arch" "$OUT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Plugin artifacts for native targets (cross handled in bundle_plugins_cross)
|
||||
if $build_plugins && ! is_cross_target "$platform"; then
|
||||
local bundle_dir="target/bundled"
|
||||
|
||||
# CLAP
|
||||
local clap_src="$bundle_dir/${PLUGIN_NAME}.clap"
|
||||
if [[ -e "$clap_src" ]]; then
|
||||
local clap_dst="$OUT/${PLUGIN_NAME}-${os}-${arch}.clap"
|
||||
cp -r "$clap_src" "$clap_dst"
|
||||
echo " CLAP -> $clap_dst"
|
||||
fi
|
||||
|
||||
# VST3
|
||||
local vst3_src="$bundle_dir/${PLUGIN_NAME}.vst3"
|
||||
if [[ -d "$vst3_src" ]]; then
|
||||
local vst3_dst="$OUT/${PLUGIN_NAME}-${os}-${arch}.vst3"
|
||||
rm -rf "$vst3_dst"
|
||||
cp -r "$vst3_src" "$vst3_dst"
|
||||
echo " VST3 -> $vst3_dst/"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
|
||||
if $cli_all; then
|
||||
selected_platforms=("${PLATFORMS[@]}")
|
||||
selected_labels=("${PLATFORM_LABELS[@]}")
|
||||
build_cagire=true
|
||||
build_desktop=true
|
||||
build_plugins=true
|
||||
elif [[ -n "$cli_platforms" || -n "$cli_targets" ]]; then
|
||||
# Resolve platforms from CLI
|
||||
if [[ -n "$cli_platforms" ]]; then
|
||||
selected_platforms=()
|
||||
selected_labels=()
|
||||
IFS=',' read -ra aliases <<< "$cli_platforms"
|
||||
for alias in "${aliases[@]}"; do
|
||||
alias="${alias// /}"
|
||||
idx=$(resolve_platform_alias "$alias")
|
||||
selected_platforms+=("${PLATFORMS[$idx]}")
|
||||
selected_labels+=("${PLATFORM_LABELS[$idx]}")
|
||||
done
|
||||
else
|
||||
selected_platforms=("${PLATFORMS[@]}")
|
||||
selected_labels=("${PLATFORM_LABELS[@]}")
|
||||
fi
|
||||
|
||||
# Resolve targets from CLI
|
||||
build_cagire=false
|
||||
build_desktop=false
|
||||
build_plugins=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 ;;
|
||||
esac
|
||||
done
|
||||
else
|
||||
build_cagire=true
|
||||
build_desktop=true
|
||||
build_plugins=true
|
||||
fi
|
||||
else
|
||||
prompt_platforms
|
||||
prompt_targets
|
||||
fi
|
||||
|
||||
if ! $cli_yes && [[ -z "$cli_platforms" ]] && ! $cli_all; then
|
||||
confirm_summary
|
||||
fi
|
||||
|
||||
mkdir -p "$OUT"
|
||||
|
||||
step=0
|
||||
total=${#selected_platforms[@]}
|
||||
|
||||
for platform in "${selected_platforms[@]}"; do
|
||||
step=$((step + 1))
|
||||
echo ""
|
||||
echo "=== [$step/$total] $platform ==="
|
||||
|
||||
if $build_cagire; then
|
||||
echo " -> cagire"
|
||||
build_binary "$platform"
|
||||
fi
|
||||
|
||||
if $build_desktop; then
|
||||
echo " -> cagire-desktop"
|
||||
build_binary "$platform" --features desktop --bin cagire-desktop
|
||||
if ! is_cross_target "$platform"; then
|
||||
echo " -> bundling cagire-desktop .app"
|
||||
bundle_desktop_native "$platform"
|
||||
fi
|
||||
fi
|
||||
|
||||
if $build_plugins; then
|
||||
echo " -> cagire-plugins"
|
||||
if is_cross_target "$platform"; then
|
||||
bundle_plugins_cross "$platform"
|
||||
else
|
||||
bundle_plugins_native "$platform"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo " Copying artifacts..."
|
||||
copy_artifacts "$platform"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo ""
|
||||
ls -lhR "$OUT/"
|
||||
1029
scripts/build.py
Executable file
1029
scripts/build.py
Executable file
File diff suppressed because it is too large
Load Diff
@@ -1,141 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Usage: scripts/make-appimage.sh <binary-path> <arch> <output-dir>
|
||||
# Produces an AppImage from a Linux binary.
|
||||
# On native Linux with matching arch: uses linuxdeploy.
|
||||
# Otherwise (cross-compilation): builds AppImage via mksquashfs in Docker.
|
||||
|
||||
if [[ $# -ne 3 ]]; then
|
||||
echo "Usage: $0 <binary-path> <arch> <output-dir>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BINARY="$1"
|
||||
ARCH="$2"
|
||||
OUTDIR="$3"
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
CACHE_DIR="$REPO_ROOT/.cache"
|
||||
APP_NAME="$(basename "$BINARY")"
|
||||
|
||||
RUNTIME_URL="https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-${ARCH}"
|
||||
RUNTIME="$CACHE_DIR/runtime-${ARCH}"
|
||||
|
||||
build_appdir() {
|
||||
local appdir="$1"
|
||||
mkdir -p "$appdir/usr/bin"
|
||||
cp "$BINARY" "$appdir/usr/bin/cagire"
|
||||
chmod +x "$appdir/usr/bin/cagire"
|
||||
|
||||
mkdir -p "$appdir/usr/share/icons/hicolor/512x512/apps"
|
||||
cp "$REPO_ROOT/assets/Cagire.png" "$appdir/usr/share/icons/hicolor/512x512/apps/cagire.png"
|
||||
|
||||
cp "$REPO_ROOT/assets/cagire.desktop" "$appdir/cagire.desktop"
|
||||
|
||||
# AppRun entry point
|
||||
cat > "$appdir/AppRun" <<'APPRUN'
|
||||
#!/bin/sh
|
||||
SELF="$(readlink -f "$0")"
|
||||
HERE="$(dirname "$SELF")"
|
||||
exec "$HERE/usr/bin/cagire" "$@"
|
||||
APPRUN
|
||||
chmod +x "$appdir/AppRun"
|
||||
|
||||
# Symlink icon at root for AppImage spec
|
||||
ln -sf usr/share/icons/hicolor/512x512/apps/cagire.png "$appdir/cagire.png"
|
||||
ln -sf cagire.desktop "$appdir/.DirIcon" 2>/dev/null || true
|
||||
}
|
||||
|
||||
download_runtime() {
|
||||
mkdir -p "$CACHE_DIR"
|
||||
if [[ ! -f "$RUNTIME" ]]; then
|
||||
echo " Downloading AppImage runtime for $ARCH..."
|
||||
curl -fSL "$RUNTIME_URL" -o "$RUNTIME"
|
||||
fi
|
||||
}
|
||||
|
||||
run_native() {
|
||||
local linuxdeploy_url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${ARCH}.AppImage"
|
||||
local linuxdeploy="$CACHE_DIR/linuxdeploy-${ARCH}.AppImage"
|
||||
|
||||
mkdir -p "$CACHE_DIR"
|
||||
if [[ ! -f "$linuxdeploy" ]]; then
|
||||
echo " Downloading linuxdeploy for $ARCH..."
|
||||
curl -fSL "$linuxdeploy_url" -o "$linuxdeploy"
|
||||
chmod +x "$linuxdeploy"
|
||||
fi
|
||||
|
||||
local appdir
|
||||
appdir="$(mktemp -d)/AppDir"
|
||||
build_appdir "$appdir"
|
||||
|
||||
export ARCH
|
||||
export LDAI_RUNTIME_FILE="$RUNTIME"
|
||||
"$linuxdeploy" \
|
||||
--appimage-extract-and-run \
|
||||
--appdir "$appdir" \
|
||||
--desktop-file "$appdir/cagire.desktop" \
|
||||
--icon-file "$appdir/usr/share/icons/hicolor/512x512/apps/cagire.png" \
|
||||
--output appimage
|
||||
|
||||
local appimage
|
||||
appimage=$(ls -1t ./*.AppImage 2>/dev/null | head -1 || true)
|
||||
if [[ -z "$appimage" ]]; then
|
||||
echo " ERROR: No AppImage produced"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$OUTDIR"
|
||||
mv "$appimage" "$OUTDIR/${APP_NAME}-linux-${ARCH}.AppImage"
|
||||
echo " AppImage -> $OUTDIR/${APP_NAME}-linux-${ARCH}.AppImage"
|
||||
}
|
||||
|
||||
run_docker() {
|
||||
local platform
|
||||
case "$ARCH" in
|
||||
x86_64) platform="linux/amd64" ;;
|
||||
aarch64) platform="linux/arm64" ;;
|
||||
*) echo "Unsupported arch: $ARCH"; exit 1 ;;
|
||||
esac
|
||||
|
||||
local appdir
|
||||
appdir="$(mktemp -d)/AppDir"
|
||||
build_appdir "$appdir"
|
||||
|
||||
local image_tag="cagire-appimage-${ARCH}"
|
||||
|
||||
echo " Building Docker image $image_tag ($platform)..."
|
||||
docker build --platform "$platform" -q -t "$image_tag" - <<'DOCKERFILE'
|
||||
FROM ubuntu:22.04
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
squashfs-tools \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
DOCKERFILE
|
||||
|
||||
echo " Creating squashfs via Docker ($image_tag)..."
|
||||
docker run --rm --platform "$platform" \
|
||||
-v "$appdir:/appdir:ro" \
|
||||
-v "$CACHE_DIR:/cache" \
|
||||
"$image_tag" \
|
||||
mksquashfs /appdir /cache/appimage-${ARCH}.squashfs \
|
||||
-root-owned -noappend -comp gzip -no-progress
|
||||
|
||||
mkdir -p "$OUTDIR"
|
||||
local final="$OUTDIR/${APP_NAME}-linux-${ARCH}.AppImage"
|
||||
cat "$RUNTIME" "$CACHE_DIR/appimage-${ARCH}.squashfs" > "$final"
|
||||
chmod +x "$final"
|
||||
rm -f "$CACHE_DIR/appimage-${ARCH}.squashfs"
|
||||
echo " AppImage -> $final"
|
||||
}
|
||||
|
||||
HOST_ARCH="$(uname -m)"
|
||||
|
||||
download_runtime
|
||||
|
||||
echo " Building AppImage for ${APP_NAME} ($ARCH)..."
|
||||
|
||||
if [[ "$HOST_ARCH" == "$ARCH" ]] && [[ "$(uname -s)" == "Linux" ]]; then
|
||||
run_native
|
||||
else
|
||||
run_docker
|
||||
fi
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Usage: scripts/make-dmg.sh <app-path> <output-dir>
|
||||
# Produces a .dmg from a macOS .app bundle using only hdiutil.
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "Usage: $0 <app-path> <output-dir>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
APP_PATH="$1"
|
||||
OUTDIR="$2"
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
|
||||
if [[ ! -d "$APP_PATH" ]]; then
|
||||
echo "ERROR: $APP_PATH is not a directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LIPO_OUTPUT=$(lipo -info "$APP_PATH/Contents/MacOS/cagire-desktop" 2>/dev/null)
|
||||
|
||||
if [[ -z "$LIPO_OUTPUT" ]]; then
|
||||
echo "ERROR: could not determine architecture from $APP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$LIPO_OUTPUT" | grep -q "Architectures in the fat file"; then
|
||||
ARCH="universal"
|
||||
else
|
||||
ARCH=$(echo "$LIPO_OUTPUT" | awk '{print $NF}')
|
||||
case "$ARCH" in
|
||||
arm64) ARCH="aarch64" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
STAGING="$(mktemp -d)"
|
||||
trap 'rm -rf "$STAGING"' EXIT
|
||||
|
||||
cp -R "$APP_PATH" "$STAGING/Cagire.app"
|
||||
ln -s /Applications "$STAGING/Applications"
|
||||
cp "$REPO_ROOT/assets/DMG-README.txt" "$STAGING/README.txt"
|
||||
|
||||
DMG_NAME="Cagire-${ARCH}.dmg"
|
||||
mkdir -p "$OUTDIR"
|
||||
|
||||
hdiutil create -volname "Cagire" \
|
||||
-srcfolder "$STAGING" \
|
||||
-ov -format UDZO \
|
||||
"$OUTDIR/$DMG_NAME"
|
||||
|
||||
echo " DMG -> $OUTDIR/$DMG_NAME"
|
||||
9
scripts/platforms.toml
Normal file
9
scripts/platforms.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
# Cagire build targets — each triple defines a compilation platform.
|
||||
# Everything else (os, arch, cross, alias, label) is derived by build.py.
|
||||
triples = [
|
||||
"aarch64-apple-darwin",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"x86_64-pc-windows-gnu",
|
||||
]
|
||||
@@ -199,7 +199,6 @@ impl App {
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
} => {
|
||||
self.playback.staged_prop_changes.insert(
|
||||
@@ -210,7 +209,6 @@ impl App {
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -203,7 +203,6 @@ impl App {
|
||||
length: pat.length.to_string(),
|
||||
speed: pat.speed,
|
||||
quantization: pat.quantization,
|
||||
sync_mode: pat.sync_mode,
|
||||
follow_up: pat.follow_up,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ impl App {
|
||||
output_devices: {
|
||||
let outputs = crate::midi::list_midi_outputs();
|
||||
self.midi
|
||||
.selected_outputs
|
||||
.selected_outputs()
|
||||
.iter()
|
||||
.map(|opt| {
|
||||
opt.and_then(|idx| outputs.get(idx).map(|d| d.name.clone()))
|
||||
@@ -138,7 +138,6 @@ impl App {
|
||||
self.playback.queued_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
quantization: crate::model::LaunchQuantization::Immediate,
|
||||
sync_mode: crate::model::SyncMode::PhaseLock,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ impl App {
|
||||
bank,
|
||||
pattern,
|
||||
quantization: staged.quantization,
|
||||
sync_mode: staged.sync_mode,
|
||||
});
|
||||
}
|
||||
PatternChange::Stop { bank, pattern } => {
|
||||
@@ -68,7 +67,6 @@ impl App {
|
||||
source: s.source,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: pat.sync_mode,
|
||||
follow_up: pat.follow_up,
|
||||
};
|
||||
let _ = cmd_tx.send(SeqCommand::PatternUpdate {
|
||||
|
||||
@@ -29,7 +29,6 @@ impl App {
|
||||
self.playback.staged_changes.push(StagedChange {
|
||||
change: PatternChange::Stop { bank, pattern },
|
||||
quantization: pattern_data.quantization,
|
||||
sync_mode: pattern_data.sync_mode,
|
||||
});
|
||||
self.ui
|
||||
.set_status(format!("{} armed to stop", bp_label(bank, pattern)));
|
||||
@@ -37,7 +36,6 @@ impl App {
|
||||
self.playback.staged_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
quantization: pattern_data.quantization,
|
||||
sync_mode: pattern_data.sync_mode,
|
||||
});
|
||||
self.ui
|
||||
.set_status(format!("{} armed to play", bp_label(bank, pattern)));
|
||||
@@ -84,7 +82,6 @@ impl App {
|
||||
}
|
||||
pat.speed = props.speed;
|
||||
pat.quantization = props.quantization;
|
||||
pat.sync_mode = props.sync_mode;
|
||||
pat.follow_up = props.follow_up;
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use cagire::block_renderer::BlockCharBackend;
|
||||
use arc_swap::ArcSwap;
|
||||
use clap::Parser;
|
||||
use doux::EngineMetrics;
|
||||
use eframe::NativeOptions;
|
||||
use egui_ratatui::RataguiBackend;
|
||||
use ratatui::Terminal;
|
||||
use soft_ratatui::embedded_graphics_unicodefonts::{
|
||||
mono_10x20_atlas, mono_6x13_atlas, mono_6x13_bold_atlas, mono_6x13_italic_atlas,
|
||||
mono_7x13_atlas, mono_7x13_bold_atlas, mono_7x13_italic_atlas, mono_8x13_atlas,
|
||||
mono_8x13_bold_atlas, mono_8x13_italic_atlas, mono_9x15_atlas, mono_9x15_bold_atlas,
|
||||
mono_9x18_atlas, mono_9x18_bold_atlas,
|
||||
};
|
||||
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
|
||||
|
||||
use cagire::engine::{
|
||||
build_stream, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand, ScopeBuffer,
|
||||
build_stream, AnalysisHandle, AudioRef, AudioStreamConfig, LinkState, ScopeBuffer,
|
||||
SequencerHandle, SpectrumBuffer,
|
||||
};
|
||||
use cagire::terminal::{create_terminal, FontChoice, TerminalType};
|
||||
use cagire::init::{init, InitArgs};
|
||||
use cagire::input::{handle_key, handle_mouse, InputContext, InputResult};
|
||||
use cagire::input_egui::{convert_egui_events, convert_egui_mouse, EguiMouseState};
|
||||
use cagire::settings::Settings;
|
||||
use cagire::views;
|
||||
use crossbeam_channel::Receiver;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "cagire-desktop", about = "Cagire desktop application")]
|
||||
@@ -48,103 +39,6 @@ struct Args {
|
||||
buffer: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum FontChoice {
|
||||
Size6x13,
|
||||
Size7x13,
|
||||
Size8x13,
|
||||
Size9x15,
|
||||
Size9x18,
|
||||
Size10x20,
|
||||
}
|
||||
|
||||
impl FontChoice {
|
||||
fn from_setting(s: &str) -> Self {
|
||||
match s {
|
||||
"6x13" => Self::Size6x13,
|
||||
"7x13" => Self::Size7x13,
|
||||
"9x15" => Self::Size9x15,
|
||||
"9x18" => Self::Size9x18,
|
||||
"10x20" => Self::Size10x20,
|
||||
_ => Self::Size8x13,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_setting(self) -> &'static str {
|
||||
match self {
|
||||
Self::Size6x13 => "6x13",
|
||||
Self::Size7x13 => "7x13",
|
||||
Self::Size8x13 => "8x13",
|
||||
Self::Size9x15 => "9x15",
|
||||
Self::Size9x18 => "9x18",
|
||||
Self::Size10x20 => "10x20",
|
||||
}
|
||||
}
|
||||
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Size6x13 => "6x13 (Compact)",
|
||||
Self::Size7x13 => "7x13",
|
||||
Self::Size8x13 => "8x13 (Default)",
|
||||
Self::Size9x15 => "9x15",
|
||||
Self::Size9x18 => "9x18",
|
||||
Self::Size10x20 => "10x20 (Large)",
|
||||
}
|
||||
}
|
||||
|
||||
const ALL: [Self; 6] = [
|
||||
Self::Size6x13,
|
||||
Self::Size7x13,
|
||||
Self::Size8x13,
|
||||
Self::Size9x15,
|
||||
Self::Size9x18,
|
||||
Self::Size10x20,
|
||||
];
|
||||
}
|
||||
|
||||
type TerminalType = Terminal<RataguiBackend<BlockCharBackend>>;
|
||||
|
||||
fn create_terminal(font: FontChoice) -> TerminalType {
|
||||
let (regular, bold, italic) = match font {
|
||||
FontChoice::Size6x13 => (
|
||||
mono_6x13_atlas(),
|
||||
Some(mono_6x13_bold_atlas()),
|
||||
Some(mono_6x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size7x13 => (
|
||||
mono_7x13_atlas(),
|
||||
Some(mono_7x13_bold_atlas()),
|
||||
Some(mono_7x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size8x13 => (
|
||||
mono_8x13_atlas(),
|
||||
Some(mono_8x13_bold_atlas()),
|
||||
Some(mono_8x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size9x15 => (mono_9x15_atlas(), Some(mono_9x15_bold_atlas()), None),
|
||||
FontChoice::Size9x18 => (mono_9x18_atlas(), Some(mono_9x18_bold_atlas()), None),
|
||||
FontChoice::Size10x20 => (mono_10x20_atlas(), None, None),
|
||||
};
|
||||
|
||||
let eg = SoftBackend::<EmbeddedGraphics>::new(80, 24, regular, bold, italic);
|
||||
let soft = SoftBackend {
|
||||
buffer: eg.buffer,
|
||||
cursor: eg.cursor,
|
||||
cursor_pos: eg.cursor_pos,
|
||||
char_width: eg.char_width,
|
||||
char_height: eg.char_height,
|
||||
blink_counter: eg.blink_counter,
|
||||
blinking_fast: eg.blinking_fast,
|
||||
blinking_slow: eg.blinking_slow,
|
||||
rgb_pixmap: eg.rgb_pixmap,
|
||||
always_redraw_list: eg.always_redraw_list,
|
||||
raster_backend: BlockCharBackend {
|
||||
inner: eg.raster_backend,
|
||||
},
|
||||
};
|
||||
Terminal::new(RataguiBackend::new("cagire", soft)).expect("terminal")
|
||||
}
|
||||
|
||||
struct CagireDesktop {
|
||||
app: cagire::app::App,
|
||||
terminal: TerminalType,
|
||||
@@ -155,12 +49,11 @@ struct CagireDesktop {
|
||||
metrics: Arc<EngineMetrics>,
|
||||
scope_buffer: Arc<ScopeBuffer>,
|
||||
spectrum_buffer: Arc<SpectrumBuffer>,
|
||||
audio_sample_pos: Arc<AtomicU64>,
|
||||
audio_ref: Arc<ArcSwap<AudioRef>>,
|
||||
sample_rate_shared: Arc<AtomicU32>,
|
||||
_stream: Option<cpal::Stream>,
|
||||
_input_stream: Option<cpal::Stream>,
|
||||
_analysis_handle: Option<AnalysisHandle>,
|
||||
midi_rx: Receiver<MidiCommand>,
|
||||
device_lost: Arc<AtomicBool>,
|
||||
stream_error_rx: crossbeam_channel::Receiver<String>,
|
||||
current_font: FontChoice,
|
||||
@@ -202,12 +95,11 @@ impl CagireDesktop {
|
||||
metrics: b.metrics,
|
||||
scope_buffer: b.scope_buffer,
|
||||
spectrum_buffer: b.spectrum_buffer,
|
||||
audio_sample_pos: b.audio_sample_pos,
|
||||
audio_ref: b.audio_ref,
|
||||
sample_rate_shared: b.sample_rate_shared,
|
||||
_stream: b.stream,
|
||||
_input_stream: b.input_stream,
|
||||
_analysis_handle: b.analysis_handle,
|
||||
midi_rx: b.midi_rx,
|
||||
device_lost: b.device_lost,
|
||||
stream_error_rx: b.stream_error_rx,
|
||||
current_font,
|
||||
@@ -237,7 +129,6 @@ impl CagireDesktop {
|
||||
return;
|
||||
};
|
||||
let new_audio_rx = sequencer.swap_audio_channel();
|
||||
self.midi_rx = sequencer.swap_midi_channel();
|
||||
|
||||
let new_config = AudioStreamConfig {
|
||||
output_device: self.app.audio.config.output_device.clone(),
|
||||
@@ -262,7 +153,11 @@ impl CagireDesktop {
|
||||
}
|
||||
}
|
||||
|
||||
self.audio_sample_pos.store(0, Ordering::Release);
|
||||
self.audio_ref.store(Arc::new(AudioRef {
|
||||
sample_pos: 0,
|
||||
timestamp: std::time::Instant::now(),
|
||||
sample_rate: 44100.0,
|
||||
}));
|
||||
|
||||
let preload_entries: Vec<(String, std::path::PathBuf)> = restart_samples
|
||||
.iter()
|
||||
@@ -276,7 +171,7 @@ impl CagireDesktop {
|
||||
Arc::clone(&self.spectrum_buffer),
|
||||
Arc::clone(&self.metrics),
|
||||
restart_samples,
|
||||
Arc::clone(&self.audio_sample_pos),
|
||||
Arc::clone(&self.audio_ref),
|
||||
new_error_tx,
|
||||
&self.app.audio.config.sample_paths,
|
||||
Arc::clone(&self.device_lost),
|
||||
@@ -288,6 +183,7 @@ impl CagireDesktop {
|
||||
self.app.audio.config.sample_rate = info.sample_rate;
|
||||
self.app.audio.config.host_name = info.host_name;
|
||||
self.app.audio.config.channels = info.channels;
|
||||
self.app.audio.config.input_sample_rate = info.input_sample_rate;
|
||||
self.sample_rate_shared
|
||||
.store(info.sample_rate as u32, Ordering::Relaxed);
|
||||
self.app.audio.error = None;
|
||||
@@ -339,30 +235,23 @@ impl CagireDesktop {
|
||||
|
||||
let term = self.terminal.get_frame().area();
|
||||
let widget_rect = ctx.content_rect();
|
||||
for mouse in convert_egui_mouse(ctx, widget_rect, term, &mut self.egui_mouse) {
|
||||
let mut input_ctx = InputContext {
|
||||
app: &mut self.app,
|
||||
link: &self.link,
|
||||
snapshot: &seq_snapshot,
|
||||
playing: &self.playing,
|
||||
audio_tx: &sequencer.audio_tx,
|
||||
seq_cmd_tx: &sequencer.cmd_tx,
|
||||
nudge_us: &self.nudge_us,
|
||||
};
|
||||
let mouse_events = convert_egui_mouse(ctx, widget_rect, term, &mut self.egui_mouse);
|
||||
let key_events = convert_egui_events(ctx);
|
||||
|
||||
let mut input_ctx = InputContext {
|
||||
app: &mut self.app,
|
||||
link: &self.link,
|
||||
snapshot: &seq_snapshot,
|
||||
playing: &self.playing,
|
||||
audio_tx: &sequencer.audio_tx,
|
||||
seq_cmd_tx: &sequencer.cmd_tx,
|
||||
nudge_us: &self.nudge_us,
|
||||
};
|
||||
|
||||
for mouse in mouse_events {
|
||||
handle_mouse(&mut input_ctx, mouse, term);
|
||||
}
|
||||
|
||||
for key in convert_egui_events(ctx) {
|
||||
let mut input_ctx = InputContext {
|
||||
app: &mut self.app,
|
||||
link: &self.link,
|
||||
snapshot: &seq_snapshot,
|
||||
playing: &self.playing,
|
||||
audio_tx: &sequencer.audio_tx,
|
||||
seq_cmd_tx: &sequencer.cmd_tx,
|
||||
nudge_us: &self.nudge_us,
|
||||
};
|
||||
|
||||
for key in key_events {
|
||||
if let InputResult::Quit = handle_key(&mut input_ctx, key) {
|
||||
return true;
|
||||
}
|
||||
@@ -414,59 +303,6 @@ impl eframe::App for CagireDesktop {
|
||||
self.app.flush_dirty_patterns(&sequencer.cmd_tx);
|
||||
self.app.flush_dirty_script(&sequencer.cmd_tx);
|
||||
|
||||
while let Ok(midi_cmd) = self.midi_rx.try_recv() {
|
||||
match midi_cmd {
|
||||
MidiCommand::NoteOn {
|
||||
device,
|
||||
channel,
|
||||
note,
|
||||
velocity,
|
||||
} => {
|
||||
self.app.midi.send_note_on(device, channel, note, velocity);
|
||||
}
|
||||
MidiCommand::NoteOff {
|
||||
device,
|
||||
channel,
|
||||
note,
|
||||
} => {
|
||||
self.app.midi.send_note_off(device, channel, note);
|
||||
}
|
||||
MidiCommand::CC {
|
||||
device,
|
||||
channel,
|
||||
cc,
|
||||
value,
|
||||
} => {
|
||||
self.app.midi.send_cc(device, channel, cc, value);
|
||||
}
|
||||
MidiCommand::PitchBend {
|
||||
device,
|
||||
channel,
|
||||
value,
|
||||
} => {
|
||||
self.app.midi.send_pitch_bend(device, channel, value);
|
||||
}
|
||||
MidiCommand::Pressure {
|
||||
device,
|
||||
channel,
|
||||
value,
|
||||
} => {
|
||||
self.app.midi.send_pressure(device, channel, value);
|
||||
}
|
||||
MidiCommand::ProgramChange {
|
||||
device,
|
||||
channel,
|
||||
program,
|
||||
} => {
|
||||
self.app.midi.send_program_change(device, channel, program);
|
||||
}
|
||||
MidiCommand::Clock { device } => self.app.midi.send_realtime(device, 0xF8),
|
||||
MidiCommand::Start { device } => self.app.midi.send_realtime(device, 0xFA),
|
||||
MidiCommand::Stop { device } => self.app.midi.send_realtime(device, 0xFC),
|
||||
MidiCommand::Continue { device } => self.app.midi.send_realtime(device, 0xFB),
|
||||
}
|
||||
}
|
||||
|
||||
let should_quit = self.handle_input(ctx);
|
||||
if should_quit {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
|
||||
@@ -242,7 +242,7 @@ fn color_to_rgb(color: &Color, is_fg: bool) -> [u8; 3] {
|
||||
Color::Yellow => [255, 215, 0],
|
||||
Color::Blue => [0, 0, 139],
|
||||
Color::Magenta => [255, 0, 255],
|
||||
Color::Cyan => [0, 0, 255],
|
||||
Color::Cyan => [0, 255, 255],
|
||||
Color::Gray => [128, 128, 128],
|
||||
Color::DarkGray => [64, 64, 64],
|
||||
Color::LightRed => [255, 0, 0],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed};
|
||||
use crate::page::Page;
|
||||
use crate::state::{ColorScheme, DeviceKind, Modal, OptionsFocus, PatternField, ScriptField, SettingKind};
|
||||
|
||||
@@ -169,7 +169,6 @@ pub enum AppCommand {
|
||||
length: Option<usize>,
|
||||
speed: PatternSpeed,
|
||||
quantization: LaunchQuantization,
|
||||
sync_mode: SyncMode,
|
||||
follow_up: FollowUp,
|
||||
},
|
||||
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
//! Audio output stream (cpal) and FFT spectrum analysis.
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use ringbuf::{traits::*, HeapRb};
|
||||
use rustfft::{num_complex::Complex, FftPlanner};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Instant;
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
use std::sync::atomic::AtomicU64;
|
||||
/// Timestamped audio position reference for jitter-free tick interpolation.
|
||||
/// Published by the audio callback after each `process_block`, read by the
|
||||
/// sequencer to compute the correct sample position at any instant.
|
||||
#[derive(Clone)]
|
||||
pub struct AudioRef {
|
||||
pub sample_pos: u64,
|
||||
pub timestamp: Instant,
|
||||
pub sample_rate: f64,
|
||||
}
|
||||
|
||||
pub struct ScopeBuffer {
|
||||
pub samples: [AtomicU32; 256],
|
||||
@@ -282,6 +291,7 @@ pub struct AudioStreamInfo {
|
||||
pub sample_rate: f32,
|
||||
pub host_name: String,
|
||||
pub channels: u16,
|
||||
pub input_sample_rate: Option<f32>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
@@ -302,7 +312,7 @@ pub fn build_stream(
|
||||
spectrum_buffer: Arc<SpectrumBuffer>,
|
||||
metrics: Arc<EngineMetrics>,
|
||||
initial_samples: Vec<doux::sampling::SampleEntry>,
|
||||
audio_sample_pos: Arc<AtomicU64>,
|
||||
audio_ref: Arc<ArcSwap<AudioRef>>,
|
||||
error_tx: Sender<String>,
|
||||
sample_paths: &[std::path::PathBuf],
|
||||
device_lost: Arc<AtomicBool>,
|
||||
@@ -367,10 +377,16 @@ pub fn build_stream(
|
||||
dev
|
||||
});
|
||||
|
||||
let input_channels: usize = input_device
|
||||
let input_config = input_device
|
||||
.as_ref()
|
||||
.and_then(|dev| dev.default_input_config().ok());
|
||||
let input_channels: usize = input_config
|
||||
.as_ref()
|
||||
.and_then(|dev| dev.default_input_config().ok())
|
||||
.map_or(0, |cfg| cfg.channels() as usize);
|
||||
let input_sample_rate = input_config.and_then(|cfg| {
|
||||
let rate = cfg.sample_rate() as f32;
|
||||
(rate != sample_rate).then_some(rate)
|
||||
});
|
||||
|
||||
engine.input_channels = input_channels;
|
||||
|
||||
@@ -431,6 +447,7 @@ pub fn build_stream(
|
||||
let mut rt_set = false;
|
||||
let mut live_scratch = vec![0.0f32; 4096];
|
||||
let mut input_consumer = input_consumer;
|
||||
let mut current_pos: u64 = 0;
|
||||
|
||||
let stream = device
|
||||
.build_output_stream(
|
||||
@@ -447,8 +464,6 @@ pub fn build_stream(
|
||||
let buffer_samples = data.len() / channels;
|
||||
let buffer_time_ns = (buffer_samples as f64 / sr as f64 * 1e9) as u64;
|
||||
|
||||
audio_sample_pos.fetch_add(buffer_samples as u64, Ordering::Release);
|
||||
|
||||
while let Ok(cmd) = audio_rx.try_recv() {
|
||||
match cmd {
|
||||
AudioCommand::Evaluate { cmd, tick } => {
|
||||
@@ -490,6 +505,16 @@ pub fn build_stream(
|
||||
|
||||
engine.metrics.load.set_buffer_time(buffer_time_ns);
|
||||
engine.process_block(data, &[], &live_scratch[..raw_len]);
|
||||
|
||||
// Publish accurate audio reference AFTER process_block
|
||||
// so sample_pos matches doux's internal tick exactly.
|
||||
current_pos += buffer_samples as u64;
|
||||
audio_ref.store(Arc::new(AudioRef {
|
||||
sample_pos: current_pos,
|
||||
timestamp: Instant::now(),
|
||||
sample_rate: sr as f64,
|
||||
}));
|
||||
|
||||
scope_buffer.write(data);
|
||||
|
||||
// Feed mono mix to analysis thread via ring buffer (non-blocking)
|
||||
@@ -519,6 +544,7 @@ pub fn build_stream(
|
||||
sample_rate,
|
||||
host_name,
|
||||
channels: effective_channels,
|
||||
input_sample_rate,
|
||||
};
|
||||
Ok((stream, input_stream, info, analysis_handle, registry))
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use arc_swap::ArcSwap;
|
||||
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender};
|
||||
use crossbeam_channel::{Receiver, RecvTimeoutError};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BinaryHeap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::link::LinkState;
|
||||
use super::realtime::{precise_sleep_us, set_realtime_priority, warn_no_rt};
|
||||
use super::sequencer::MidiCommand;
|
||||
use super::timing::SyncTime;
|
||||
use crate::midi::{MidiOutputPorts, MAX_MIDI_OUTPUTS};
|
||||
|
||||
/// A MIDI command scheduled for dispatch at a specific time.
|
||||
#[derive(Clone)]
|
||||
@@ -46,13 +45,13 @@ impl Eq for TimedMidiCommand {}
|
||||
|
||||
const SPIN_THRESHOLD_US: SyncTime = 100;
|
||||
|
||||
/// Dispatcher loop — handles MIDI timing only.
|
||||
/// Dispatcher loop — handles MIDI timing and sends directly to MIDI ports.
|
||||
/// Audio commands bypass the dispatcher entirely and go straight to doux's
|
||||
/// sample-accurate scheduler via the audio thread channel.
|
||||
pub fn dispatcher_loop(
|
||||
cmd_rx: Receiver<TimedMidiCommand>,
|
||||
midi_tx: Arc<ArcSwap<Sender<MidiCommand>>>,
|
||||
link: Arc<LinkState>,
|
||||
ports: MidiOutputPorts,
|
||||
link: &LinkState,
|
||||
) {
|
||||
let has_rt = set_realtime_priority();
|
||||
if !has_rt {
|
||||
@@ -84,8 +83,8 @@ pub fn dispatcher_loop(
|
||||
while let Some(cmd) = queue.peek() {
|
||||
if cmd.target_time_us <= current_us + SPIN_THRESHOLD_US {
|
||||
let cmd = queue.pop().expect("pop after peek");
|
||||
wait_until_dispatch(cmd.target_time_us, &link, has_rt);
|
||||
dispatch_midi(cmd.command, &midi_tx);
|
||||
wait_until_dispatch(cmd.target_time_us, link, has_rt);
|
||||
dispatch_midi(cmd.command, &ports);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -106,15 +105,15 @@ fn wait_until_dispatch(target_us: SyncTime, link: &LinkState, has_rt: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_midi(cmd: MidiDispatch, midi_tx: &Arc<ArcSwap<Sender<MidiCommand>>>) {
|
||||
fn dispatch_midi(cmd: MidiDispatch, ports: &MidiOutputPorts) {
|
||||
match cmd {
|
||||
MidiDispatch::Send(midi_cmd) => {
|
||||
let _ = midi_tx.load().try_send(midi_cmd);
|
||||
ports.send_command(&midi_cmd);
|
||||
}
|
||||
MidiDispatch::FlushAll => {
|
||||
for dev in 0..4u8 {
|
||||
for dev in 0..MAX_MIDI_OUTPUTS as u8 {
|
||||
for chan in 0..16u8 {
|
||||
let _ = midi_tx.load().try_send(MidiCommand::CC {
|
||||
ports.send_command(&MidiCommand::CC {
|
||||
device: dev,
|
||||
channel: chan,
|
||||
cc: 123,
|
||||
|
||||
@@ -81,6 +81,20 @@ impl LinkState {
|
||||
self.link.commit_app_session_state(&state);
|
||||
}
|
||||
|
||||
pub fn start_playing(&self, beat: f64, time: i64, quantum: f64) {
|
||||
let mut state = SessionState::new();
|
||||
self.link.capture_app_session_state(&mut state);
|
||||
state.set_is_playing_and_request_beat_at_time(true, time, beat, quantum);
|
||||
self.link.commit_app_session_state(&state);
|
||||
}
|
||||
|
||||
pub fn stop_playing(&self, time: i64) {
|
||||
let mut state = SessionState::new();
|
||||
self.link.capture_app_session_state(&mut state);
|
||||
state.set_is_playing(false, time);
|
||||
self.link.commit_app_session_state(&state);
|
||||
}
|
||||
|
||||
pub fn capture_app_state(&self) -> SessionState {
|
||||
let mut state = SessionState::new();
|
||||
self.link.capture_app_session_state(&mut state);
|
||||
|
||||
@@ -5,9 +5,9 @@ pub mod realtime;
|
||||
pub mod sequencer;
|
||||
mod timing;
|
||||
|
||||
pub use timing::{substeps_in_window, StepTiming, SyncTime};
|
||||
pub use timing::{next_boundary, substeps_in_window, SyncTime};
|
||||
|
||||
pub use audio::{preload_sample_heads, AnalysisHandle, ScopeBuffer, SpectrumBuffer};
|
||||
pub use audio::{preload_sample_heads, AnalysisHandle, AudioRef, ScopeBuffer, SpectrumBuffer};
|
||||
|
||||
// Re-exported for the plugin crate (not used by the terminal binary).
|
||||
#[allow(unused_imports)]
|
||||
@@ -21,13 +21,13 @@ pub use audio::AudioStreamInfo;
|
||||
|
||||
pub use link::LinkState;
|
||||
pub use sequencer::{
|
||||
spawn_sequencer, AudioCommand, MidiCommand, PatternChange, PatternSnapshot, SeqCommand,
|
||||
spawn_sequencer, AudioCommand, PatternChange, PatternSnapshot, SeqCommand,
|
||||
SequencerConfig, SequencerHandle, SequencerSnapshot, StepSnapshot,
|
||||
};
|
||||
|
||||
// Re-exported for the plugin crate (not used by the terminal binary).
|
||||
#[allow(unused_imports)]
|
||||
pub use sequencer::{
|
||||
parse_midi_command, SequencerState, SharedSequencerState, TickInput, TickOutput,
|
||||
parse_midi_command, MidiCommand, SequencerState, SharedSequencerState, TickInput, TickOutput,
|
||||
TimestampedCommand,
|
||||
};
|
||||
|
||||
@@ -8,17 +8,25 @@ use rand::SeedableRng;
|
||||
use std::collections::HashMap;
|
||||
#[cfg(feature = "desktop")]
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::{AtomicI64, AtomicU64};
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::Arc;
|
||||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use super::audio::AudioRef;
|
||||
|
||||
use super::dispatcher::{dispatcher_loop, MidiDispatch, TimedMidiCommand};
|
||||
use super::realtime::set_realtime_priority;
|
||||
use super::{substeps_in_window, LinkState, StepTiming, SyncTime};
|
||||
use super::{next_boundary, substeps_in_window, LinkState, SyncTime};
|
||||
use crate::model::{
|
||||
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables,
|
||||
};
|
||||
use crate::model::{FollowUp, LaunchQuantization, SyncMode, MAX_BANKS, MAX_PATTERNS};
|
||||
use crate::model::{FollowUp, LaunchQuantization, MAX_BANKS, MAX_PATTERNS};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum SyncMode {
|
||||
Reset,
|
||||
PhaseLock,
|
||||
}
|
||||
use crate::state::LiveKeyState;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
@@ -114,7 +122,6 @@ pub enum SeqCommand {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
quantization: LaunchQuantization,
|
||||
sync_mode: SyncMode,
|
||||
},
|
||||
PatternStop {
|
||||
bank: usize,
|
||||
@@ -141,7 +148,6 @@ pub struct PatternSnapshot {
|
||||
pub speed: crate::model::PatternSpeed,
|
||||
pub length: usize,
|
||||
pub steps: Vec<StepSnapshot>,
|
||||
pub sync_mode: SyncMode,
|
||||
pub follow_up: FollowUp,
|
||||
}
|
||||
|
||||
@@ -170,7 +176,6 @@ 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>,
|
||||
}
|
||||
@@ -181,7 +186,6 @@ 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>,
|
||||
}
|
||||
@@ -194,7 +198,6 @@ 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(),
|
||||
}
|
||||
@@ -210,7 +213,6 @@ impl SequencerSnapshot {
|
||||
event_count: 0,
|
||||
tempo: 0.0,
|
||||
beat: 0.0,
|
||||
playing: false,
|
||||
script_trace: None,
|
||||
print_output: None,
|
||||
}
|
||||
@@ -261,7 +263,6 @@ impl SequencerSnapshot {
|
||||
pub struct SequencerHandle {
|
||||
pub cmd_tx: Sender<SeqCommand>,
|
||||
pub audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
|
||||
pub midi_tx: Arc<ArcSwap<Sender<MidiCommand>>>,
|
||||
shared_state: Arc<ArcSwap<SharedSequencerState>>,
|
||||
thread: JoinHandle<()>,
|
||||
}
|
||||
@@ -278,12 +279,6 @@ impl SequencerHandle {
|
||||
new_rx
|
||||
}
|
||||
|
||||
pub fn swap_midi_channel(&self) -> Receiver<MidiCommand> {
|
||||
let (new_tx, new_rx) = bounded::<MidiCommand>(256);
|
||||
self.midi_tx.store(Arc::new(new_tx));
|
||||
new_rx
|
||||
}
|
||||
|
||||
pub fn shutdown(self) {
|
||||
let _ = self.cmd_tx.send(SeqCommand::Shutdown);
|
||||
if let Err(e) = self.thread.join() {
|
||||
@@ -299,18 +294,24 @@ struct ActivePattern {
|
||||
step_index: usize,
|
||||
iter: usize,
|
||||
last_step_beat: f64,
|
||||
origin_beat: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PendingPattern {
|
||||
id: PatternId,
|
||||
quantization: LaunchQuantization,
|
||||
target_beat: Option<f64>,
|
||||
sync_mode: SyncMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum PlayState {
|
||||
Idle { pause_beat: Option<f64> },
|
||||
Playing { frontier: f64 },
|
||||
}
|
||||
|
||||
struct AudioState {
|
||||
prev_beat: f64,
|
||||
pause_beat: Option<f64>,
|
||||
play_state: PlayState,
|
||||
active_patterns: HashMap<PatternId, ActivePattern>,
|
||||
pending_starts: Vec<PendingPattern>,
|
||||
pending_stops: Vec<PendingPattern>,
|
||||
@@ -320,8 +321,7 @@ struct AudioState {
|
||||
impl AudioState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
prev_beat: -1.0,
|
||||
pause_beat: None,
|
||||
play_state: PlayState::Idle { pause_beat: None },
|
||||
active_patterns: HashMap::new(),
|
||||
pending_starts: Vec::new(),
|
||||
pending_stops: Vec::new(),
|
||||
@@ -331,7 +331,7 @@ impl AudioState {
|
||||
}
|
||||
|
||||
pub struct SequencerConfig {
|
||||
pub audio_sample_pos: Arc<AtomicU64>,
|
||||
pub audio_ref: Arc<ArcSwap<AudioRef>>,
|
||||
pub sample_rate: Arc<std::sync::atomic::AtomicU32>,
|
||||
pub cc_access: Option<Arc<dyn CcAccess>>,
|
||||
pub variables: Variables,
|
||||
@@ -351,16 +351,11 @@ pub fn spawn_sequencer(
|
||||
live_keys: Arc<LiveKeyState>,
|
||||
nudge_us: Arc<AtomicI64>,
|
||||
config: SequencerConfig,
|
||||
) -> (
|
||||
SequencerHandle,
|
||||
Receiver<AudioCommand>,
|
||||
Receiver<MidiCommand>,
|
||||
) {
|
||||
midi_ports: crate::midi::MidiOutputPorts,
|
||||
) -> (SequencerHandle, Receiver<AudioCommand>) {
|
||||
let (cmd_tx, cmd_rx) = bounded::<SeqCommand>(64);
|
||||
let (audio_tx, audio_rx) = unbounded::<AudioCommand>();
|
||||
let (midi_tx, midi_rx) = bounded::<MidiCommand>(256);
|
||||
let audio_tx = Arc::new(ArcSwap::from_pointee(audio_tx));
|
||||
let midi_tx = Arc::new(ArcSwap::from_pointee(midi_tx));
|
||||
|
||||
// Dispatcher channel — MIDI only (unbounded to avoid blocking the scheduler)
|
||||
let (dispatch_tx, dispatch_rx) = unbounded::<TimedMidiCommand>();
|
||||
@@ -377,13 +372,12 @@ pub fn spawn_sequencer(
|
||||
#[cfg(feature = "desktop")]
|
||||
let mouse_down = config.mouse_down;
|
||||
|
||||
// Spawn dispatcher thread (MIDI only — audio goes direct to doux)
|
||||
// Spawn dispatcher thread — sends MIDI directly to ports (no UI-thread hop)
|
||||
let dispatcher_link = Arc::clone(&link);
|
||||
let dispatcher_midi_tx = Arc::clone(&midi_tx);
|
||||
thread::Builder::new()
|
||||
.name("cagire-dispatcher".into())
|
||||
.spawn(move || {
|
||||
dispatcher_loop(dispatch_rx, dispatcher_midi_tx, dispatcher_link);
|
||||
dispatcher_loop(dispatch_rx, midi_ports, &dispatcher_link);
|
||||
})
|
||||
.expect("Failed to spawn dispatcher thread");
|
||||
|
||||
@@ -401,7 +395,7 @@ pub fn spawn_sequencer(
|
||||
shared_state_clone,
|
||||
live_keys,
|
||||
nudge_us,
|
||||
config.audio_sample_pos,
|
||||
config.audio_ref,
|
||||
config.sample_rate,
|
||||
config.cc_access,
|
||||
variables,
|
||||
@@ -419,11 +413,10 @@ pub fn spawn_sequencer(
|
||||
let handle = SequencerHandle {
|
||||
cmd_tx,
|
||||
audio_tx,
|
||||
midi_tx,
|
||||
shared_state,
|
||||
thread,
|
||||
};
|
||||
(handle, audio_rx, midi_rx)
|
||||
(handle, audio_rx)
|
||||
}
|
||||
|
||||
struct PatternCache {
|
||||
@@ -474,22 +467,6 @@ impl PatternSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_quantization_boundary(
|
||||
quantization: LaunchQuantization,
|
||||
beat: f64,
|
||||
prev_beat: f64,
|
||||
quantum: f64,
|
||||
) -> bool {
|
||||
match quantization {
|
||||
LaunchQuantization::Immediate => prev_beat >= 0.0,
|
||||
LaunchQuantization::Beat => StepTiming::NextBeat.crossed(prev_beat, beat, quantum),
|
||||
LaunchQuantization::Bar => StepTiming::NextBar.crossed(prev_beat, beat, quantum),
|
||||
LaunchQuantization::Bars2 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 2.0),
|
||||
LaunchQuantization::Bars4 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 4.0),
|
||||
LaunchQuantization::Bars8 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 8.0),
|
||||
}
|
||||
}
|
||||
|
||||
type StepKey = (usize, usize, usize);
|
||||
|
||||
struct RunsCounter {
|
||||
@@ -527,7 +504,7 @@ pub struct TickInput {
|
||||
pub fill: bool,
|
||||
pub nudge_secs: f64,
|
||||
pub current_time_us: SyncTime,
|
||||
pub audio_sample_pos: u64,
|
||||
pub corrected_audio_pos: f64,
|
||||
pub sr: f64,
|
||||
pub mouse_x: f64,
|
||||
pub mouse_y: f64,
|
||||
@@ -582,7 +559,7 @@ pub struct SequencerState {
|
||||
script_text: String,
|
||||
script_speed: crate::model::PatternSpeed,
|
||||
script_length: usize,
|
||||
script_frontier: f64,
|
||||
script_frontier: Option<f64>,
|
||||
script_step: usize,
|
||||
script_trace: Option<ExecutionTrace>,
|
||||
print_output: Option<String>,
|
||||
@@ -621,7 +598,7 @@ impl SequencerState {
|
||||
script_text: String::new(),
|
||||
script_speed: crate::model::PatternSpeed::default(),
|
||||
script_length: 16,
|
||||
script_frontier: -1.0,
|
||||
script_frontier: None,
|
||||
script_step: 0,
|
||||
script_trace: None,
|
||||
print_output: None,
|
||||
@@ -639,7 +616,7 @@ impl SequencerState {
|
||||
false
|
||||
}
|
||||
|
||||
fn process_commands(&mut self, commands: Vec<SeqCommand>) {
|
||||
fn process_commands(&mut self, commands: Vec<SeqCommand>, quantum: f64) {
|
||||
for cmd in commands {
|
||||
match cmd {
|
||||
SeqCommand::PatternUpdate {
|
||||
@@ -660,15 +637,15 @@ impl SequencerState {
|
||||
bank,
|
||||
pattern,
|
||||
quantization,
|
||||
sync_mode,
|
||||
} => {
|
||||
let id = PatternId { bank, pattern };
|
||||
self.audio_state.pending_stops.retain(|p| p.id != id);
|
||||
if !self.audio_state.pending_starts.iter().any(|p| p.id == id) {
|
||||
let target_beat = next_boundary(self.last_beat, quantization, quantum);
|
||||
self.audio_state.pending_starts.push(PendingPattern {
|
||||
id,
|
||||
quantization,
|
||||
sync_mode,
|
||||
target_beat,
|
||||
sync_mode: SyncMode::PhaseLock,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -680,9 +657,10 @@ impl SequencerState {
|
||||
let id = PatternId { bank, pattern };
|
||||
self.audio_state.pending_starts.retain(|p| p.id != id);
|
||||
if !self.audio_state.pending_stops.iter().any(|p| p.id == id) {
|
||||
let target_beat = next_boundary(self.last_beat, quantization, quantum);
|
||||
self.audio_state.pending_stops.push(PendingPattern {
|
||||
id,
|
||||
quantization,
|
||||
target_beat,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
}
|
||||
@@ -722,7 +700,8 @@ 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.audio_state.play_state = PlayState::Idle { pause_beat: None };
|
||||
self.script_frontier = None;
|
||||
self.step_traces = Arc::new(HashMap::new());
|
||||
self.runs_counter.counts.clear();
|
||||
self.audio_state.flush_midi_notes = true;
|
||||
@@ -732,9 +711,8 @@ impl SequencerState {
|
||||
active.step_index = 0;
|
||||
active.iter = 0;
|
||||
}
|
||||
self.audio_state.prev_beat = -1.0;
|
||||
self.audio_state.pause_beat = None;
|
||||
self.script_frontier = -1.0;
|
||||
self.audio_state.play_state = PlayState::Idle { pause_beat: None };
|
||||
self.script_frontier = None;
|
||||
self.script_step = 0;
|
||||
self.script_trace = None;
|
||||
self.variables.store(Arc::new(HashMap::new()));
|
||||
@@ -758,30 +736,26 @@ impl SequencerState {
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, input: TickInput) -> TickOutput {
|
||||
self.process_commands(input.commands);
|
||||
self.last_tempo = input.tempo;
|
||||
self.last_beat = input.beat;
|
||||
self.last_playing = input.playing;
|
||||
self.process_commands(input.commands, input.quantum);
|
||||
|
||||
if !input.playing {
|
||||
return self.tick_paused();
|
||||
}
|
||||
|
||||
let frontier = self.audio_state.prev_beat;
|
||||
let lookahead_end = input.lookahead_end;
|
||||
let resuming = frontier < 0.0;
|
||||
|
||||
let boundary_frontier = if resuming {
|
||||
self.audio_state.pause_beat.take().unwrap_or(input.beat)
|
||||
} else {
|
||||
frontier
|
||||
let (frontier, resuming) = match self.audio_state.play_state {
|
||||
PlayState::Playing { frontier } => (frontier, false),
|
||||
PlayState::Idle { pause_beat } => (pause_beat.unwrap_or(input.beat), true),
|
||||
};
|
||||
let lookahead_end = input.lookahead_end;
|
||||
|
||||
self.activate_pending(lookahead_end, boundary_frontier, input.quantum);
|
||||
self.deactivate_pending(lookahead_end, boundary_frontier, input.quantum);
|
||||
self.activate_pending(frontier, lookahead_end);
|
||||
self.deactivate_pending(frontier, lookahead_end);
|
||||
|
||||
if resuming {
|
||||
self.realign_phaselock_patterns(lookahead_end);
|
||||
self.reset_origins_on_resume(lookahead_end);
|
||||
}
|
||||
|
||||
let steps = self.execute_steps(
|
||||
@@ -793,7 +767,7 @@ impl SequencerState {
|
||||
input.fill,
|
||||
input.nudge_secs,
|
||||
input.current_time_us,
|
||||
input.audio_sample_pos,
|
||||
input.corrected_audio_pos,
|
||||
input.sr,
|
||||
input.mouse_x,
|
||||
input.mouse_y,
|
||||
@@ -808,7 +782,7 @@ impl SequencerState {
|
||||
input.quantum,
|
||||
input.fill,
|
||||
input.nudge_secs,
|
||||
input.audio_sample_pos,
|
||||
input.corrected_audio_pos,
|
||||
input.sr,
|
||||
input.mouse_x,
|
||||
input.mouse_y,
|
||||
@@ -818,7 +792,7 @@ impl SequencerState {
|
||||
let new_tempo = self.read_tempo_variable(steps.any_step_fired);
|
||||
self.apply_follow_ups();
|
||||
|
||||
self.audio_state.prev_beat = lookahead_end;
|
||||
self.audio_state.play_state = PlayState::Playing { frontier: lookahead_end };
|
||||
|
||||
let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
|
||||
TickOutput {
|
||||
@@ -840,11 +814,12 @@ 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;
|
||||
let pause_beat = match self.audio_state.play_state {
|
||||
PlayState::Playing { frontier } => Some(frontier),
|
||||
PlayState::Idle { pause_beat } => pause_beat,
|
||||
};
|
||||
self.audio_state.play_state = PlayState::Idle { pause_beat };
|
||||
self.script_frontier = None;
|
||||
self.script_step = 0;
|
||||
self.script_trace = None;
|
||||
self.print_output = None;
|
||||
@@ -858,35 +833,46 @@ 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 pause_beat(&self) -> Option<f64> {
|
||||
match self.audio_state.play_state {
|
||||
PlayState::Idle { pause_beat } => pause_beat,
|
||||
PlayState::Playing { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) {
|
||||
fn reset_origins_on_resume(&mut self, lookahead_end: f64) {
|
||||
for (id, active) in &mut self.audio_state.active_patterns {
|
||||
active.origin_beat = lookahead_end;
|
||||
let Some(pattern) = self.pattern_cache.get(id.bank, id.pattern) else {
|
||||
continue;
|
||||
};
|
||||
let subs_per_beat = 4.0 * pattern.speed.multiplier();
|
||||
let step = (lookahead_end * subs_per_beat).floor() as usize + 1;
|
||||
active.step_index = step % pattern.length;
|
||||
}
|
||||
self.script_frontier = Some(lookahead_end);
|
||||
}
|
||||
|
||||
fn activate_pending(&mut self, frontier: f64, lookahead_end: f64) {
|
||||
self.buf_activated.clear();
|
||||
for pending in &self.audio_state.pending_starts {
|
||||
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
||||
let should_activate = match pending.target_beat {
|
||||
None => true,
|
||||
Some(target) => target <= lookahead_end,
|
||||
};
|
||||
if should_activate {
|
||||
let origin_beat = match pending.target_beat {
|
||||
Some(t) if t > frontier => t - 1e-9,
|
||||
_ => frontier,
|
||||
};
|
||||
let start_step = match pending.sync_mode {
|
||||
SyncMode::Reset => 0,
|
||||
SyncMode::PhaseLock => {
|
||||
if let Some(pat) =
|
||||
self.pattern_cache.get(pending.id.bank, pending.id.pattern)
|
||||
{
|
||||
let speed_mult = pat.speed.multiplier();
|
||||
let subs_per_beat = 4.0 * speed_mult;
|
||||
let first_sub = (prev_beat * subs_per_beat).floor() as usize + 1;
|
||||
first_sub % pat.length
|
||||
let subs_per_beat = 4.0 * pat.speed.multiplier();
|
||||
(origin_beat * subs_per_beat).floor() as usize % pat.length
|
||||
} else {
|
||||
0
|
||||
}
|
||||
@@ -901,7 +887,8 @@ impl SequencerState {
|
||||
pattern: pending.id.pattern,
|
||||
step_index: start_step,
|
||||
iter: 0,
|
||||
last_step_beat: beat,
|
||||
last_step_beat: lookahead_end,
|
||||
origin_beat,
|
||||
},
|
||||
);
|
||||
self.buf_activated.push(pending.id);
|
||||
@@ -913,15 +900,18 @@ impl SequencerState {
|
||||
.retain(|p| !activated.contains(&p.id));
|
||||
}
|
||||
|
||||
fn deactivate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) {
|
||||
fn deactivate_pending(&mut self, _frontier: f64, lookahead_end: f64) {
|
||||
self.buf_stopped.clear();
|
||||
for pending in &self.audio_state.pending_stops {
|
||||
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
||||
let should_deactivate = match pending.target_beat {
|
||||
None => true,
|
||||
Some(target) => target <= lookahead_end,
|
||||
};
|
||||
if should_deactivate {
|
||||
self.audio_state.active_patterns.remove(&pending.id);
|
||||
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
|
||||
bank != pending.id.bank || pattern != pending.id.pattern
|
||||
});
|
||||
// Flush pending update so cache stays current for future launches
|
||||
let key = (pending.id.bank, pending.id.pattern);
|
||||
if let Some(snapshot) = self.pending_updates.remove(&key) {
|
||||
self.pattern_cache.set(key.0, key.1, snapshot);
|
||||
@@ -946,7 +936,7 @@ impl SequencerState {
|
||||
fill: bool,
|
||||
nudge_secs: f64,
|
||||
_current_time_us: SyncTime,
|
||||
audio_sample_pos: u64,
|
||||
corrected_audio_pos: f64,
|
||||
sr: f64,
|
||||
mouse_x: f64,
|
||||
mouse_y: f64,
|
||||
@@ -981,7 +971,8 @@ impl SequencerState {
|
||||
.copied()
|
||||
.unwrap_or_else(|| pattern.speed.multiplier());
|
||||
|
||||
let step_beats = substeps_in_window(frontier, lookahead_end, speed_mult);
|
||||
let pattern_frontier = frontier.max(active.origin_beat);
|
||||
let step_beats = substeps_in_window(pattern_frontier, lookahead_end, speed_mult);
|
||||
|
||||
for step_beat in step_beats {
|
||||
result.any_step_fired = true;
|
||||
@@ -994,7 +985,7 @@ impl SequencerState {
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let event_tick = Some(audio_sample_pos + (time_delta * sr).round() as u64);
|
||||
let event_tick = Some((corrected_audio_pos + time_delta * sr).round() as u64);
|
||||
|
||||
if let Some(step) = pattern.steps.get(step_idx) {
|
||||
let resolved_script = pattern.resolve_script(step_idx);
|
||||
@@ -1106,7 +1097,7 @@ impl SequencerState {
|
||||
quantum: f64,
|
||||
fill: bool,
|
||||
nudge_secs: f64,
|
||||
audio_sample_pos: u64,
|
||||
corrected_audio_pos: f64,
|
||||
sr: f64,
|
||||
mouse_x: f64,
|
||||
mouse_y: f64,
|
||||
@@ -1116,11 +1107,7 @@ impl SequencerState {
|
||||
return;
|
||||
}
|
||||
|
||||
let script_frontier = if self.script_frontier < 0.0 {
|
||||
frontier
|
||||
} else {
|
||||
self.script_frontier
|
||||
};
|
||||
let script_frontier = self.script_frontier.unwrap_or(frontier);
|
||||
|
||||
let speed_mult = self.script_speed.multiplier();
|
||||
let fire_beats = substeps_in_window(script_frontier, lookahead_end, speed_mult);
|
||||
@@ -1132,7 +1119,7 @@ impl SequencerState {
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let event_tick = Some(audio_sample_pos + (time_delta * sr).round() as u64);
|
||||
let event_tick = Some((corrected_audio_pos + time_delta * sr).round() as u64);
|
||||
|
||||
let step_in_cycle = self.script_step % self.script_length;
|
||||
|
||||
@@ -1187,7 +1174,7 @@ impl SequencerState {
|
||||
self.script_step += 1;
|
||||
}
|
||||
|
||||
self.script_frontier = lookahead_end;
|
||||
self.script_frontier = Some(lookahead_end);
|
||||
}
|
||||
|
||||
fn read_tempo_variable(&self, any_step_fired: bool) -> Option<f64> {
|
||||
@@ -1220,21 +1207,21 @@ impl SequencerState {
|
||||
FollowUp::Stop => {
|
||||
self.audio_state.pending_stops.push(PendingPattern {
|
||||
id: *completed_id,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
target_beat: None,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
}
|
||||
FollowUp::Chain { bank, pattern } => {
|
||||
self.audio_state.pending_stops.push(PendingPattern {
|
||||
id: *completed_id,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
target_beat: None,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
let target = PatternId { bank, pattern };
|
||||
if !self.audio_state.pending_starts.iter().any(|p| p.id == target) {
|
||||
self.audio_state.pending_starts.push(PendingPattern {
|
||||
id: target,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
target_beat: None,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
}
|
||||
@@ -1261,7 +1248,6 @@ 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(),
|
||||
}
|
||||
@@ -1279,7 +1265,7 @@ fn sequencer_loop(
|
||||
shared_state: Arc<ArcSwap<SharedSequencerState>>,
|
||||
live_keys: Arc<LiveKeyState>,
|
||||
nudge_us: Arc<AtomicI64>,
|
||||
audio_sample_pos: Arc<AtomicU64>,
|
||||
audio_ref: Arc<ArcSwap<AudioRef>>,
|
||||
sample_rate: Arc<std::sync::atomic::AtomicU32>,
|
||||
cc_access: Option<Arc<dyn CcAccess>>,
|
||||
variables: Variables,
|
||||
@@ -1325,9 +1311,19 @@ fn sequencer_loop(
|
||||
|
||||
let state = link.capture_app_state();
|
||||
let current_time_us = link.clock_micros() as SyncTime;
|
||||
let beat = state.beat_at_time(current_time_us as i64, quantum);
|
||||
let mut beat = state.beat_at_time(current_time_us as i64, quantum);
|
||||
let tempo = state.tempo();
|
||||
|
||||
let is_playing = playing.load(Ordering::Relaxed);
|
||||
if is_playing && !seq_state.last_playing {
|
||||
let anchor_beat = seq_state.pause_beat().unwrap_or(0.0);
|
||||
link.start_playing(anchor_beat, current_time_us as i64, quantum);
|
||||
let state = link.capture_app_state();
|
||||
beat = state.beat_at_time(current_time_us as i64, quantum);
|
||||
} else if !is_playing && seq_state.last_playing {
|
||||
link.stop_playing(current_time_us as i64);
|
||||
}
|
||||
|
||||
let lookahead_beats = if tempo > 0.0 {
|
||||
lookahead_secs * tempo / 60.0
|
||||
} else {
|
||||
@@ -1336,10 +1332,12 @@ fn sequencer_loop(
|
||||
let lookahead_end = beat + lookahead_beats;
|
||||
|
||||
let sr = sample_rate.load(Ordering::Relaxed) as f64;
|
||||
let audio_samples = audio_sample_pos.load(Ordering::Acquire);
|
||||
let ref_snapshot = audio_ref.load();
|
||||
let elapsed_secs = ref_snapshot.timestamp.elapsed().as_secs_f64();
|
||||
let corrected_pos = ref_snapshot.sample_pos as f64 + elapsed_secs * ref_snapshot.sample_rate;
|
||||
let input = TickInput {
|
||||
commands,
|
||||
playing: playing.load(Ordering::Relaxed),
|
||||
playing: is_playing,
|
||||
beat,
|
||||
lookahead_end,
|
||||
tempo,
|
||||
@@ -1347,7 +1345,7 @@ fn sequencer_loop(
|
||||
fill: live_keys.fill(),
|
||||
nudge_secs: nudge_us.load(Ordering::Relaxed) as f64 / 1_000_000.0,
|
||||
current_time_us,
|
||||
audio_sample_pos: audio_samples,
|
||||
corrected_audio_pos: corrected_pos,
|
||||
sr,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: f32::from_bits(mouse_x.load(Ordering::Relaxed)) as f64,
|
||||
@@ -1550,7 +1548,6 @@ mod tests {
|
||||
source: None,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: SyncMode::Reset,
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
@@ -1570,7 +1567,7 @@ mod tests {
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
audio_sample_pos: 0,
|
||||
corrected_audio_pos: 0.0,
|
||||
sr: 48000.0,
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
@@ -1589,7 +1586,7 @@ mod tests {
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
audio_sample_pos: 0,
|
||||
corrected_audio_pos: 0.0,
|
||||
sr: 48000.0,
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
@@ -1621,7 +1618,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
1.0,
|
||||
@@ -1649,7 +1645,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
1.0,
|
||||
));
|
||||
@@ -1697,7 +1692,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -1706,43 +1700,32 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quantization_boundaries() {
|
||||
assert!(check_quantization_boundary(
|
||||
LaunchQuantization::Immediate,
|
||||
1.5,
|
||||
1.0,
|
||||
4.0
|
||||
));
|
||||
assert!(check_quantization_boundary(
|
||||
LaunchQuantization::Beat,
|
||||
2.0,
|
||||
1.9,
|
||||
4.0
|
||||
));
|
||||
assert!(!check_quantization_boundary(
|
||||
LaunchQuantization::Beat,
|
||||
1.5,
|
||||
1.2,
|
||||
4.0
|
||||
));
|
||||
assert!(check_quantization_boundary(
|
||||
LaunchQuantization::Bar,
|
||||
4.0,
|
||||
3.9,
|
||||
4.0
|
||||
));
|
||||
assert!(!check_quantization_boundary(
|
||||
LaunchQuantization::Bar,
|
||||
3.5,
|
||||
3.2,
|
||||
4.0
|
||||
));
|
||||
assert!(!check_quantization_boundary(
|
||||
LaunchQuantization::Immediate,
|
||||
1.0,
|
||||
-1.0,
|
||||
4.0
|
||||
));
|
||||
fn test_next_boundary() {
|
||||
use super::super::next_boundary;
|
||||
|
||||
// Immediate returns None
|
||||
assert_eq!(next_boundary(1.5, LaunchQuantization::Immediate, 4.0), None);
|
||||
|
||||
// Beat: next integer beat
|
||||
assert_eq!(next_boundary(1.5, LaunchQuantization::Beat, 4.0), Some(2.0));
|
||||
assert_eq!(next_boundary(1.9, LaunchQuantization::Beat, 4.0), Some(2.0));
|
||||
// On exact beat boundary, targets next beat
|
||||
assert_eq!(next_boundary(2.0, LaunchQuantization::Beat, 4.0), Some(3.0));
|
||||
|
||||
// Bar (quantum=4): next multiple of 4
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bar, 4.0), Some(4.0));
|
||||
assert_eq!(next_boundary(3.9, LaunchQuantization::Bar, 4.0), Some(4.0));
|
||||
// On exact bar boundary, targets next bar
|
||||
assert_eq!(next_boundary(4.0, LaunchQuantization::Bar, 4.0), Some(8.0));
|
||||
|
||||
// Bars2 (quantum=4): next multiple of 8
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars2, 4.0), Some(8.0));
|
||||
|
||||
// Bars4 (quantum=4): next multiple of 16
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars4, 4.0), Some(16.0));
|
||||
|
||||
// Bars8 (quantum=4): next multiple of 32
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars8, 4.0), Some(32.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1763,7 +1746,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -1808,7 +1790,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -1844,7 +1825,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
SeqCommand::PatternStop {
|
||||
bank: 0,
|
||||
@@ -1879,13 +1859,11 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 1,
|
||||
quantization: LaunchQuantization::Beat,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
0.0,
|
||||
@@ -1921,7 +1899,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -1990,7 +1967,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
..tick_at(1.0, false)
|
||||
});
|
||||
@@ -2019,7 +1995,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
1.0,
|
||||
));
|
||||
@@ -2049,13 +2024,11 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
0.0,
|
||||
@@ -2088,7 +2061,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -2115,7 +2087,6 @@ mod tests {
|
||||
source: None,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: SyncMode::Reset,
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
@@ -2138,7 +2109,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -2186,13 +2156,11 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 1,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
0.5,
|
||||
@@ -2227,7 +2195,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
3.5,
|
||||
));
|
||||
@@ -2266,7 +2233,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
..tick_at(2.0, false)
|
||||
});
|
||||
@@ -2306,13 +2272,11 @@ mod tests {
|
||||
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,
|
||||
@@ -2344,7 +2308,6 @@ mod tests {
|
||||
source: None,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: SyncMode::PhaseLock,
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
@@ -2368,7 +2331,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::PhaseLock,
|
||||
}],
|
||||
3.5,
|
||||
));
|
||||
@@ -2410,7 +2372,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
1.0,
|
||||
));
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
use crate::model::LaunchQuantization;
|
||||
|
||||
/// Microsecond-precision timestamp for audio synchronization.
|
||||
pub type SyncTime = u64;
|
||||
|
||||
/// Timing boundary types for step and pattern scheduling.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StepTiming {
|
||||
/// Fire when a beat boundary is crossed.
|
||||
NextBeat,
|
||||
/// Fire when a bar/quantum boundary is crossed.
|
||||
NextBar,
|
||||
}
|
||||
|
||||
impl StepTiming {
|
||||
/// Returns true if the boundary was crossed between prev_beat and curr_beat.
|
||||
pub fn crossed(&self, prev_beat: f64, curr_beat: f64, quantum: f64) -> bool {
|
||||
if prev_beat < 0.0 {
|
||||
return false;
|
||||
/// Compute the exact next quantization boundary beat after `current_beat`.
|
||||
/// Returns `None` for Immediate (activate now), `Some(beat)` for all others.
|
||||
pub fn next_boundary(current_beat: f64, quantization: LaunchQuantization, quantum: f64) -> Option<f64> {
|
||||
match quantization {
|
||||
LaunchQuantization::Immediate => None,
|
||||
LaunchQuantization::Beat => Some(current_beat.floor() + 1.0),
|
||||
LaunchQuantization::Bar => {
|
||||
Some((current_beat / quantum).floor() * quantum + quantum)
|
||||
}
|
||||
match self {
|
||||
Self::NextBeat => prev_beat.floor() as i64 != curr_beat.floor() as i64,
|
||||
Self::NextBar => {
|
||||
(prev_beat / quantum).floor() as i64 != (curr_beat / quantum).floor() as i64
|
||||
}
|
||||
LaunchQuantization::Bars2 => {
|
||||
let p = quantum * 2.0;
|
||||
Some((current_beat / p).floor() * p + p)
|
||||
}
|
||||
LaunchQuantization::Bars4 => {
|
||||
let p = quantum * 4.0;
|
||||
Some((current_beat / p).floor() * p + p)
|
||||
}
|
||||
LaunchQuantization::Bars8 => {
|
||||
let p = quantum * 8.0;
|
||||
Some((current_beat / p).floor() * p + p)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +31,7 @@ impl StepTiming {
|
||||
/// Each entry is the exact beat at which that substep fires.
|
||||
/// Clamped to 64 results max to prevent runaway.
|
||||
pub fn substeps_in_window(frontier: f64, end: f64, speed: f64) -> Vec<f64> {
|
||||
if frontier < 0.0 || end <= frontier || speed <= 0.0 {
|
||||
if end <= frontier || speed <= 0.0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let substeps_per_beat = 4.0 * speed;
|
||||
@@ -55,9 +57,6 @@ mod tests {
|
||||
}
|
||||
|
||||
fn substeps_crossed(prev_beat: f64, curr_beat: f64, speed: f64) -> usize {
|
||||
if prev_beat < 0.0 {
|
||||
return 0;
|
||||
}
|
||||
let prev_substep = (prev_beat * 4.0 * speed).floor() as i64;
|
||||
let curr_substep = (curr_beat * 4.0 * speed).floor() as i64;
|
||||
(curr_substep - prev_substep).clamp(0, 16) as usize
|
||||
@@ -88,23 +87,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_step_timing_beat_crossed() {
|
||||
// Crossing from beat 0 to beat 1
|
||||
assert!(StepTiming::NextBeat.crossed(0.9, 1.1, 4.0));
|
||||
// Not crossing (both in same beat)
|
||||
assert!(!StepTiming::NextBeat.crossed(0.5, 0.9, 4.0));
|
||||
// Negative prev_beat returns false
|
||||
assert!(!StepTiming::NextBeat.crossed(-1.0, 1.0, 4.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_step_timing_bar_crossed() {
|
||||
// Crossing from bar 0 to bar 1 (quantum=4)
|
||||
assert!(StepTiming::NextBar.crossed(3.9, 4.1, 4.0));
|
||||
// Not crossing (both in same bar)
|
||||
assert!(!StepTiming::NextBar.crossed(2.0, 3.0, 4.0));
|
||||
// Crossing with different quantum
|
||||
assert!(StepTiming::NextBar.crossed(7.9, 8.1, 8.0));
|
||||
fn test_next_boundary() {
|
||||
assert_eq!(next_boundary(1.5, LaunchQuantization::Immediate, 4.0), None);
|
||||
assert_eq!(next_boundary(1.5, LaunchQuantization::Beat, 4.0), Some(2.0));
|
||||
assert_eq!(next_boundary(2.0, LaunchQuantization::Beat, 4.0), Some(3.0));
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bar, 4.0), Some(4.0));
|
||||
assert_eq!(next_boundary(4.0, LaunchQuantization::Bar, 4.0), Some(8.0));
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars2, 4.0), Some(8.0));
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars4, 4.0), Some(16.0));
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars8, 4.0), Some(32.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -126,9 +117,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_crossed_negative_prev() {
|
||||
// Negative prev_beat returns 0
|
||||
assert_eq!(substeps_crossed(-1.0, 0.5, 1.0), 0);
|
||||
fn test_substeps_crossed_same_position() {
|
||||
// Same position returns 0
|
||||
assert_eq!(substeps_crossed(0.5, 0.5, 1.0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -205,8 +196,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_in_window_negative_frontier() {
|
||||
let result = substeps_in_window(-1.0, 0.5, 1.0);
|
||||
fn test_substeps_in_window_reversed() {
|
||||
// end <= frontier returns empty
|
||||
let result = substeps_in_window(0.5, 0.3, 1.0);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
37
src/init.rs
37
src/init.rs
@@ -1,15 +1,16 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use arc_swap::ArcSwap;
|
||||
use doux::EngineMetrics;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::engine::{
|
||||
build_stream, preload_sample_heads, spawn_sequencer, AnalysisHandle, AudioStreamConfig,
|
||||
LinkState, MidiCommand, PatternChange, ScopeBuffer, SequencerConfig, SequencerHandle,
|
||||
SpectrumBuffer,
|
||||
build_stream, preload_sample_heads, spawn_sequencer, AnalysisHandle, AudioRef,
|
||||
AudioStreamConfig, LinkState, PatternChange, ScopeBuffer, SequencerConfig,
|
||||
SequencerHandle, SpectrumBuffer,
|
||||
};
|
||||
use crate::midi;
|
||||
use crate::model;
|
||||
@@ -37,12 +38,11 @@ pub struct Init {
|
||||
pub metrics: Arc<EngineMetrics>,
|
||||
pub scope_buffer: Arc<ScopeBuffer>,
|
||||
pub spectrum_buffer: Arc<SpectrumBuffer>,
|
||||
pub audio_sample_pos: Arc<AtomicU64>,
|
||||
pub audio_ref: Arc<ArcSwap<AudioRef>>,
|
||||
pub sample_rate_shared: Arc<AtomicU32>,
|
||||
pub stream: Option<cpal::Stream>,
|
||||
pub input_stream: Option<cpal::Stream>,
|
||||
pub analysis_handle: Option<AnalysisHandle>,
|
||||
pub midi_rx: Receiver<MidiCommand>,
|
||||
pub device_lost: Arc<AtomicBool>,
|
||||
pub stream_error_rx: crossbeam_channel::Receiver<String>,
|
||||
#[cfg(feature = "desktop")]
|
||||
@@ -83,8 +83,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
for (bank, pattern) in playing {
|
||||
app.playback.queued_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
quantization: model::LaunchQuantization::Immediate,
|
||||
sync_mode: model::SyncMode::PhaseLock,
|
||||
quantization: model::LaunchQuantization::Bar,
|
||||
});
|
||||
}
|
||||
app.ui.set_status(format!("Demo: {}", demo.name));
|
||||
@@ -96,8 +95,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
},
|
||||
quantization: model::LaunchQuantization::Immediate,
|
||||
sync_mode: model::SyncMode::PhaseLock,
|
||||
quantization: model::LaunchQuantization::Bar,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,7 +153,11 @@ pub fn init(args: InitArgs) -> Init {
|
||||
let scope_buffer = Arc::new(ScopeBuffer::new());
|
||||
let spectrum_buffer = Arc::new(SpectrumBuffer::new());
|
||||
|
||||
let audio_sample_pos = Arc::new(AtomicU64::new(0));
|
||||
let audio_ref = Arc::new(ArcSwap::from_pointee(AudioRef {
|
||||
sample_pos: 0,
|
||||
timestamp: Instant::now(),
|
||||
sample_rate: 44100.0,
|
||||
}));
|
||||
let sample_rate_shared = Arc::new(AtomicU32::new(44100));
|
||||
let mut initial_samples = Vec::new();
|
||||
for path in &app.audio.config.sample_paths {
|
||||
@@ -181,7 +183,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
let mouse_down = Arc::new(AtomicU32::new(0.0_f32.to_bits()));
|
||||
|
||||
let seq_config = SequencerConfig {
|
||||
audio_sample_pos: Arc::clone(&audio_sample_pos),
|
||||
audio_ref: Arc::clone(&audio_ref),
|
||||
sample_rate: Arc::clone(&sample_rate_shared),
|
||||
cc_access: Some(Arc::new(app.midi.cc_memory.clone()) as Arc<dyn model::CcAccess>),
|
||||
variables: Arc::clone(&app.variables),
|
||||
@@ -194,13 +196,14 @@ pub fn init(args: InitArgs) -> Init {
|
||||
mouse_down: Arc::clone(&mouse_down),
|
||||
};
|
||||
|
||||
let (sequencer, initial_audio_rx, midi_rx) = spawn_sequencer(
|
||||
let (sequencer, initial_audio_rx) = spawn_sequencer(
|
||||
Arc::clone(&link),
|
||||
Arc::clone(&playing),
|
||||
settings.link.quantum,
|
||||
Arc::clone(&app.live_keys),
|
||||
Arc::clone(&nudge_us),
|
||||
seq_config,
|
||||
app.midi.output_ports.clone(),
|
||||
);
|
||||
|
||||
let device_lost = Arc::new(AtomicBool::new(false));
|
||||
@@ -221,7 +224,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
Arc::clone(&spectrum_buffer),
|
||||
Arc::clone(&metrics),
|
||||
initial_samples,
|
||||
Arc::clone(&audio_sample_pos),
|
||||
Arc::clone(&audio_ref),
|
||||
stream_error_tx,
|
||||
&app.audio.config.sample_paths,
|
||||
Arc::clone(&device_lost),
|
||||
@@ -230,6 +233,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
app.audio.config.sample_rate = info.sample_rate;
|
||||
app.audio.config.host_name = info.host_name;
|
||||
app.audio.config.channels = info.channels;
|
||||
app.audio.config.input_sample_rate = info.input_sample_rate;
|
||||
sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed);
|
||||
app.audio.sample_registry = Some(Arc::clone(®istry));
|
||||
|
||||
@@ -264,12 +268,11 @@ pub fn init(args: InitArgs) -> Init {
|
||||
metrics,
|
||||
scope_buffer,
|
||||
spectrum_buffer,
|
||||
audio_sample_pos,
|
||||
audio_ref,
|
||||
sample_rate_shared,
|
||||
stream,
|
||||
input_stream,
|
||||
analysis_handle,
|
||||
midi_rx,
|
||||
device_lost,
|
||||
stream_error_rx,
|
||||
#[cfg(feature = "desktop")]
|
||||
|
||||
@@ -48,22 +48,20 @@ pub(crate) fn cycle_link_setting(ctx: &mut InputContext, right: bool) {
|
||||
pub(crate) fn cycle_midi_output(ctx: &mut InputContext, right: bool) {
|
||||
let slot = ctx.app.audio.midi_output_slot;
|
||||
let all_devices = crate::midi::list_midi_outputs();
|
||||
let selected = ctx.app.midi.selected_outputs();
|
||||
let available: Vec<(usize, &crate::midi::MidiDeviceInfo)> = all_devices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(idx, _)| {
|
||||
ctx.app.midi.selected_outputs[slot] == Some(*idx)
|
||||
|| !ctx
|
||||
.app
|
||||
.midi
|
||||
.selected_outputs
|
||||
selected[slot] == Some(*idx)
|
||||
|| !selected
|
||||
.iter()
|
||||
.enumerate()
|
||||
.any(|(s, sel)| s != slot && *sel == Some(*idx))
|
||||
})
|
||||
.collect();
|
||||
let total_options = available.len() + 1;
|
||||
let current_pos = ctx.app.midi.selected_outputs[slot]
|
||||
let current_pos = selected[slot]
|
||||
.and_then(|idx| available.iter().position(|(i, _)| *i == idx))
|
||||
.map(|p| p + 1)
|
||||
.unwrap_or(0);
|
||||
@@ -299,7 +297,7 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
KeyCode::Char('r') => ctx.dispatch(AppCommand::ResetPeakVoices),
|
||||
KeyCode::Char('t') if !ctx.app.plugin_mode => {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Evaluate {
|
||||
cmd: "/sound/sine/dur/0.5/decay/0.2".into(),
|
||||
cmd: "/sound/sine/gate/0.5/decay/0.2".into(),
|
||||
tick: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -461,7 +461,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
} => {
|
||||
let (bank, pattern) = (*bank, *pattern);
|
||||
@@ -472,7 +471,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
KeyCode::Left => match field {
|
||||
PatternPropsField::Speed => *speed = speed.prev(),
|
||||
PatternPropsField::Quantization => *quantization = quantization.prev(),
|
||||
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
||||
PatternPropsField::FollowUp => *follow_up = follow_up.prev_mode(),
|
||||
PatternPropsField::ChainBank => {
|
||||
if let FollowUp::Chain { bank: b, .. } = follow_up {
|
||||
@@ -489,7 +487,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
KeyCode::Right => match field {
|
||||
PatternPropsField::Speed => *speed = speed.next(),
|
||||
PatternPropsField::Quantization => *quantization = quantization.next(),
|
||||
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
||||
PatternPropsField::FollowUp => *follow_up = follow_up.next_mode(),
|
||||
PatternPropsField::ChainBank => {
|
||||
if let FollowUp::Chain { bank: b, .. } = follow_up {
|
||||
@@ -535,7 +532,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
let length_val = length.parse().ok();
|
||||
let speed_val = *speed;
|
||||
let quant_val = *quantization;
|
||||
let sync_val = *sync_mode;
|
||||
let follow_up_val = *follow_up;
|
||||
ctx.dispatch(AppCommand::StagePatternProps {
|
||||
bank,
|
||||
@@ -545,7 +541,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
length: length_val,
|
||||
speed: speed_val,
|
||||
quantization: quant_val,
|
||||
sync_mode: sync_val,
|
||||
follow_up: follow_up_val,
|
||||
});
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
@@ -795,7 +790,7 @@ fn execute_palette_entry(
|
||||
.audio_tx
|
||||
.load()
|
||||
.send(crate::engine::AudioCommand::Evaluate {
|
||||
cmd: "/sound/sine/dur/0.5/decay/0.2".into(),
|
||||
cmd: "/sound/sine/gate/0.5/decay/0.2".into(),
|
||||
tick: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ pub(super) fn handle_sample_explorer(ctx: &mut InputContext, key: KeyEvent) -> I
|
||||
TreeLineKind::File => {
|
||||
let folder = &entry.folder;
|
||||
let idx = entry.index;
|
||||
let cmd = format!("/sound/{folder}/n/{idx}/gain/1.00/dur/1");
|
||||
let cmd = format!("/sound/{folder}/n/{idx}/gain/1.00/gate/1");
|
||||
let _ = ctx
|
||||
.audio_tx
|
||||
.load()
|
||||
|
||||
@@ -21,3 +21,6 @@ pub mod block_renderer;
|
||||
|
||||
#[cfg(feature = "block-renderer")]
|
||||
pub mod input_egui;
|
||||
|
||||
#[cfg(feature = "block-renderer")]
|
||||
pub mod terminal;
|
||||
|
||||
66
src/main.rs
66
src/main.rs
@@ -96,12 +96,11 @@ fn main() -> io::Result<()> {
|
||||
let metrics = b.metrics;
|
||||
let scope_buffer = b.scope_buffer;
|
||||
let spectrum_buffer = b.spectrum_buffer;
|
||||
let audio_sample_pos = b.audio_sample_pos;
|
||||
let audio_ref = b.audio_ref;
|
||||
let sample_rate_shared = b.sample_rate_shared;
|
||||
let mut _stream = b.stream;
|
||||
let mut _input_stream = b.input_stream;
|
||||
let mut _analysis_handle = b.analysis_handle;
|
||||
let mut midi_rx = b.midi_rx;
|
||||
let device_lost = b.device_lost;
|
||||
let mut stream_error_rx = b.stream_error_rx;
|
||||
|
||||
@@ -125,7 +124,6 @@ fn main() -> io::Result<()> {
|
||||
_analysis_handle = None;
|
||||
|
||||
let new_audio_rx = sequencer.swap_audio_channel();
|
||||
midi_rx = sequencer.swap_midi_channel();
|
||||
|
||||
let new_config = AudioStreamConfig {
|
||||
output_device: app.audio.config.output_device.clone(),
|
||||
@@ -150,7 +148,11 @@ fn main() -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
audio_sample_pos.store(0, Ordering::Relaxed);
|
||||
audio_ref.store(Arc::new(engine::AudioRef {
|
||||
sample_pos: 0,
|
||||
timestamp: std::time::Instant::now(),
|
||||
sample_rate: 44100.0,
|
||||
}));
|
||||
|
||||
let preload_entries: Vec<(String, std::path::PathBuf)> = restart_samples
|
||||
.iter()
|
||||
@@ -164,7 +166,7 @@ fn main() -> io::Result<()> {
|
||||
Arc::clone(&spectrum_buffer),
|
||||
Arc::clone(&metrics),
|
||||
restart_samples,
|
||||
Arc::clone(&audio_sample_pos),
|
||||
Arc::clone(&audio_ref),
|
||||
new_error_tx,
|
||||
&app.audio.config.sample_paths,
|
||||
Arc::clone(&device_lost),
|
||||
@@ -176,6 +178,7 @@ fn main() -> io::Result<()> {
|
||||
app.audio.config.sample_rate = info.sample_rate;
|
||||
app.audio.config.host_name = info.host_name;
|
||||
app.audio.config.channels = info.channels;
|
||||
app.audio.config.input_sample_rate = info.input_sample_rate;
|
||||
sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed);
|
||||
app.audio.error = None;
|
||||
app.audio.sample_registry = Some(Arc::clone(®istry));
|
||||
@@ -233,59 +236,6 @@ fn main() -> io::Result<()> {
|
||||
}
|
||||
was_playing = app.playback.playing;
|
||||
|
||||
while let Ok(midi_cmd) = midi_rx.try_recv() {
|
||||
match midi_cmd {
|
||||
engine::MidiCommand::NoteOn {
|
||||
device,
|
||||
channel,
|
||||
note,
|
||||
velocity,
|
||||
} => {
|
||||
app.midi.send_note_on(device, channel, note, velocity);
|
||||
}
|
||||
engine::MidiCommand::NoteOff {
|
||||
device,
|
||||
channel,
|
||||
note,
|
||||
} => {
|
||||
app.midi.send_note_off(device, channel, note);
|
||||
}
|
||||
engine::MidiCommand::CC {
|
||||
device,
|
||||
channel,
|
||||
cc,
|
||||
value,
|
||||
} => {
|
||||
app.midi.send_cc(device, channel, cc, value);
|
||||
}
|
||||
engine::MidiCommand::PitchBend {
|
||||
device,
|
||||
channel,
|
||||
value,
|
||||
} => {
|
||||
app.midi.send_pitch_bend(device, channel, value);
|
||||
}
|
||||
engine::MidiCommand::Pressure {
|
||||
device,
|
||||
channel,
|
||||
value,
|
||||
} => {
|
||||
app.midi.send_pressure(device, channel, value);
|
||||
}
|
||||
engine::MidiCommand::ProgramChange {
|
||||
device,
|
||||
channel,
|
||||
program,
|
||||
} => {
|
||||
app.midi.send_program_change(device, channel, program);
|
||||
}
|
||||
engine::MidiCommand::Clock { device } => app.midi.send_realtime(device, 0xF8),
|
||||
engine::MidiCommand::Start { device } => app.midi.send_realtime(device, 0xFA),
|
||||
engine::MidiCommand::Stop { device } => app.midi.send_realtime(device, 0xFC),
|
||||
engine::MidiCommand::Continue { device } => app.midi.send_realtime(device, 0xFB),
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
app.metrics.active_voices = metrics.active_voices.load(Ordering::Relaxed) as usize;
|
||||
app.metrics.peak_voices = app.metrics.peak_voices.max(app.metrics.active_voices);
|
||||
|
||||
204
src/midi.rs
204
src/midi.rs
@@ -1,12 +1,124 @@
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::engine::sequencer::MidiCommand;
|
||||
use crate::model::CcAccess;
|
||||
|
||||
pub const MAX_MIDI_OUTPUTS: usize = 4;
|
||||
pub const MAX_MIDI_INPUTS: usize = 4;
|
||||
pub const MAX_MIDI_DEVICES: usize = 4;
|
||||
|
||||
/// Thread-safe MIDI output connections shared between the dispatcher (sending)
|
||||
/// and the UI thread (connect/disconnect). The dispatcher calls `send_command`
|
||||
/// directly after precise timing, eliminating the ~16ms jitter from UI polling.
|
||||
#[derive(Clone)]
|
||||
pub struct MidiOutputPorts {
|
||||
#[cfg(feature = "cli")]
|
||||
conns: Arc<Mutex<[Option<midir::MidiOutputConnection>; MAX_MIDI_OUTPUTS]>>,
|
||||
pub selected: Arc<Mutex<[Option<usize>; MAX_MIDI_OUTPUTS]>>,
|
||||
}
|
||||
|
||||
impl MidiOutputPorts {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "cli")]
|
||||
conns: Arc::new(Mutex::new([None, None, None, None])),
|
||||
selected: Arc::new(Mutex::new([None; MAX_MIDI_OUTPUTS])),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
pub fn connect(&self, slot: usize, port_index: usize) -> Result<(), String> {
|
||||
if slot >= MAX_MIDI_OUTPUTS {
|
||||
return Err("Invalid output slot".to_string());
|
||||
}
|
||||
let midi_out =
|
||||
midir::MidiOutput::new(&format!("cagire-out-{slot}")).map_err(|e| e.to_string())?;
|
||||
let ports = midi_out.ports();
|
||||
let port = ports.get(port_index).ok_or("MIDI output port not found")?;
|
||||
let conn = midi_out
|
||||
.connect(port, &format!("cagire-midi-out-{slot}"))
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.conns.lock()[slot] = Some(conn);
|
||||
self.selected.lock()[slot] = Some(port_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "cli"))]
|
||||
pub fn connect(&self, _slot: usize, _port_index: usize) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
pub fn disconnect(&self, slot: usize) {
|
||||
if slot < MAX_MIDI_OUTPUTS {
|
||||
self.conns.lock()[slot] = None;
|
||||
self.selected.lock()[slot] = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "cli"))]
|
||||
pub fn disconnect(&self, slot: usize) {
|
||||
if slot < MAX_MIDI_OUTPUTS {
|
||||
self.selected.lock()[slot] = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
fn send_message(&self, device: u8, message: &[u8]) {
|
||||
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
|
||||
let mut conns = self.conns.lock();
|
||||
if let Some(conn) = &mut conns[slot] {
|
||||
let _ = conn.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "cli"))]
|
||||
fn send_message(&self, _device: u8, _message: &[u8]) {}
|
||||
|
||||
/// Send a MidiCommand directly — called by the dispatcher thread.
|
||||
pub fn send_command(&self, cmd: &MidiCommand) {
|
||||
match cmd {
|
||||
MidiCommand::NoteOn { device, channel, note, velocity } => {
|
||||
let status = 0x90 | (channel & 0x0F);
|
||||
self.send_message(*device, &[status, note & 0x7F, velocity & 0x7F]);
|
||||
}
|
||||
MidiCommand::NoteOff { device, channel, note } => {
|
||||
let status = 0x80 | (channel & 0x0F);
|
||||
self.send_message(*device, &[status, note & 0x7F, 0]);
|
||||
}
|
||||
MidiCommand::CC { device, channel, cc, value } => {
|
||||
let status = 0xB0 | (channel & 0x0F);
|
||||
self.send_message(*device, &[status, cc & 0x7F, value & 0x7F]);
|
||||
}
|
||||
MidiCommand::PitchBend { device, channel, value } => {
|
||||
let status = 0xE0 | (channel & 0x0F);
|
||||
let lsb = (value & 0x7F) as u8;
|
||||
let msb = ((value >> 7) & 0x7F) as u8;
|
||||
self.send_message(*device, &[status, lsb, msb]);
|
||||
}
|
||||
MidiCommand::Pressure { device, channel, value } => {
|
||||
let status = 0xD0 | (channel & 0x0F);
|
||||
self.send_message(*device, &[status, value & 0x7F]);
|
||||
}
|
||||
MidiCommand::ProgramChange { device, channel, program } => {
|
||||
let status = 0xC0 | (channel & 0x0F);
|
||||
self.send_message(*device, &[status, program & 0x7F]);
|
||||
}
|
||||
MidiCommand::Clock { device } => self.send_message(*device, &[0xF8]),
|
||||
MidiCommand::Start { device } => self.send_message(*device, &[0xFA]),
|
||||
MidiCommand::Stop { device } => self.send_message(*device, &[0xFC]),
|
||||
MidiCommand::Continue { device } => self.send_message(*device, &[0xFB]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MidiOutputPorts {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw CC memory storage type
|
||||
type CcMemoryInner = Arc<Mutex<[[[u8; 128]; 16]; MAX_MIDI_DEVICES]>>;
|
||||
|
||||
@@ -95,11 +207,9 @@ pub fn list_midi_inputs() -> Vec<MidiDeviceInfo> {
|
||||
}
|
||||
|
||||
pub struct MidiState {
|
||||
#[cfg(feature = "cli")]
|
||||
output_conns: [Option<midir::MidiOutputConnection>; MAX_MIDI_OUTPUTS],
|
||||
#[cfg(feature = "cli")]
|
||||
input_conns: [Option<midir::MidiInputConnection<(CcMemoryInner, usize)>>; MAX_MIDI_INPUTS],
|
||||
pub selected_outputs: [Option<usize>; MAX_MIDI_OUTPUTS],
|
||||
pub output_ports: MidiOutputPorts,
|
||||
pub selected_inputs: [Option<usize>; MAX_MIDI_INPUTS],
|
||||
pub cc_memory: CcMemory,
|
||||
}
|
||||
@@ -113,51 +223,24 @@ impl Default for MidiState {
|
||||
impl MidiState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "cli")]
|
||||
output_conns: [None, None, None, None],
|
||||
#[cfg(feature = "cli")]
|
||||
input_conns: [None, None, None, None],
|
||||
selected_outputs: [None; MAX_MIDI_OUTPUTS],
|
||||
output_ports: MidiOutputPorts::new(),
|
||||
selected_inputs: [None; MAX_MIDI_INPUTS],
|
||||
cc_memory: CcMemory::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
pub fn connect_output(&mut self, slot: usize, port_index: usize) -> Result<(), String> {
|
||||
if slot >= MAX_MIDI_OUTPUTS {
|
||||
return Err("Invalid output slot".to_string());
|
||||
}
|
||||
let midi_out =
|
||||
midir::MidiOutput::new(&format!("cagire-out-{slot}")).map_err(|e| e.to_string())?;
|
||||
let ports = midi_out.ports();
|
||||
let port = ports.get(port_index).ok_or("MIDI output port not found")?;
|
||||
let conn = midi_out
|
||||
.connect(port, &format!("cagire-midi-out-{slot}"))
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.output_conns[slot] = Some(conn);
|
||||
self.selected_outputs[slot] = Some(port_index);
|
||||
Ok(())
|
||||
pub fn connect_output(&self, slot: usize, port_index: usize) -> Result<(), String> {
|
||||
self.output_ports.connect(slot, port_index)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "cli"))]
|
||||
pub fn connect_output(&mut self, _slot: usize, _port_index: usize) -> Result<(), String> {
|
||||
Ok(())
|
||||
pub fn disconnect_output(&self, slot: usize) {
|
||||
self.output_ports.disconnect(slot);
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
pub fn disconnect_output(&mut self, slot: usize) {
|
||||
if slot < MAX_MIDI_OUTPUTS {
|
||||
self.output_conns[slot] = None;
|
||||
self.selected_outputs[slot] = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "cli"))]
|
||||
pub fn disconnect_output(&mut self, slot: usize) {
|
||||
if slot < MAX_MIDI_OUTPUTS {
|
||||
self.selected_outputs[slot] = None;
|
||||
}
|
||||
pub fn selected_outputs(&self) -> [Option<usize>; MAX_MIDI_OUTPUTS] {
|
||||
*self.output_ports.selected.lock()
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
@@ -215,51 +298,4 @@ impl MidiState {
|
||||
self.selected_inputs[slot] = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
fn send_message(&mut self, device: u8, message: &[u8]) {
|
||||
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
|
||||
if let Some(conn) = &mut self.output_conns[slot] {
|
||||
let _ = conn.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "cli"))]
|
||||
fn send_message(&mut self, _device: u8, _message: &[u8]) {}
|
||||
|
||||
pub fn send_note_on(&mut self, device: u8, channel: u8, note: u8, velocity: u8) {
|
||||
let status = 0x90 | (channel & 0x0F);
|
||||
self.send_message(device, &[status, note & 0x7F, velocity & 0x7F]);
|
||||
}
|
||||
|
||||
pub fn send_note_off(&mut self, device: u8, channel: u8, note: u8) {
|
||||
let status = 0x80 | (channel & 0x0F);
|
||||
self.send_message(device, &[status, note & 0x7F, 0]);
|
||||
}
|
||||
|
||||
pub fn send_cc(&mut self, device: u8, channel: u8, cc: u8, value: u8) {
|
||||
let status = 0xB0 | (channel & 0x0F);
|
||||
self.send_message(device, &[status, cc & 0x7F, value & 0x7F]);
|
||||
}
|
||||
|
||||
pub fn send_pitch_bend(&mut self, device: u8, channel: u8, value: u16) {
|
||||
let status = 0xE0 | (channel & 0x0F);
|
||||
let lsb = (value & 0x7F) as u8;
|
||||
let msb = ((value >> 7) & 0x7F) as u8;
|
||||
self.send_message(device, &[status, lsb, msb]);
|
||||
}
|
||||
|
||||
pub fn send_pressure(&mut self, device: u8, channel: u8, value: u8) {
|
||||
let status = 0xD0 | (channel & 0x0F);
|
||||
self.send_message(device, &[status, value & 0x7F]);
|
||||
}
|
||||
|
||||
pub fn send_program_change(&mut self, device: u8, channel: u8, program: u8) {
|
||||
let status = 0xC0 | (channel & 0x0F);
|
||||
self.send_message(device, &[status, program & 0x7F]);
|
||||
}
|
||||
|
||||
pub fn send_realtime(&mut self, device: u8, msg: u8) {
|
||||
self.send_message(device, &[msg]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ pub use cagire_forth::{
|
||||
};
|
||||
pub use cagire_project::{
|
||||
load, load_str, save, share, Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed,
|
||||
Project, SyncMode, MAX_BANKS, MAX_PATTERNS,
|
||||
Project, MAX_BANKS, MAX_PATTERNS,
|
||||
};
|
||||
pub use script::ScriptEngine;
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ pub struct AudioConfig {
|
||||
pub lissajous_trails: bool,
|
||||
pub spectrum_mode: SpectrumMode,
|
||||
pub spectrum_peaks: bool,
|
||||
pub input_sample_rate: Option<f32>,
|
||||
}
|
||||
|
||||
impl Default for AudioConfig {
|
||||
@@ -154,6 +155,7 @@ impl Default for AudioConfig {
|
||||
lissajous_trails: false,
|
||||
spectrum_mode: SpectrumMode::default(),
|
||||
spectrum_peaks: false,
|
||||
input_sample_rate: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ pub enum PatternPropsField {
|
||||
Length,
|
||||
Speed,
|
||||
Quantization,
|
||||
SyncMode,
|
||||
FollowUp,
|
||||
ChainBank,
|
||||
ChainPattern,
|
||||
@@ -44,8 +43,7 @@ impl PatternPropsField {
|
||||
Self::Description => Self::Length,
|
||||
Self::Length => Self::Speed,
|
||||
Self::Speed => Self::Quantization,
|
||||
Self::Quantization => Self::SyncMode,
|
||||
Self::SyncMode => Self::FollowUp,
|
||||
Self::Quantization => Self::FollowUp,
|
||||
Self::FollowUp if follow_up_is_chain => Self::ChainBank,
|
||||
Self::FollowUp => Self::FollowUp,
|
||||
Self::ChainBank => Self::ChainPattern,
|
||||
@@ -60,8 +58,7 @@ impl PatternPropsField {
|
||||
Self::Length => Self::Description,
|
||||
Self::Speed => Self::Length,
|
||||
Self::Quantization => Self::Speed,
|
||||
Self::SyncMode => Self::Quantization,
|
||||
Self::FollowUp => Self::SyncMode,
|
||||
Self::FollowUp => Self::Quantization,
|
||||
Self::ChainBank => Self::FollowUp,
|
||||
Self::ChainPattern if follow_up_is_chain => Self::ChainBank,
|
||||
Self::ChainPattern => Self::FollowUp,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::model::{self, FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use crate::model::{self, FollowUp, LaunchQuantization, PatternSpeed};
|
||||
use crate::state::editor::{EuclideanField, PatternField, PatternPropsField, ScriptField};
|
||||
use crate::state::file_browser::FileBrowserState;
|
||||
|
||||
@@ -85,7 +85,6 @@ pub enum Modal {
|
||||
length: String,
|
||||
speed: PatternSpeed,
|
||||
quantization: LaunchQuantization,
|
||||
sync_mode: SyncMode,
|
||||
follow_up: FollowUp,
|
||||
},
|
||||
KeybindingsHelp {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use crate::engine::PatternChange;
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StagedChange {
|
||||
pub change: PatternChange,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
@@ -21,7 +20,6 @@ pub struct StagedPropChange {
|
||||
pub length: Option<usize>,
|
||||
pub speed: PatternSpeed,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
pub follow_up: FollowUp,
|
||||
}
|
||||
|
||||
|
||||
110
src/terminal.rs
Normal file
110
src/terminal.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use egui_ratatui::RataguiBackend;
|
||||
use ratatui::Terminal;
|
||||
use soft_ratatui::embedded_graphics_unicodefonts::{
|
||||
mono_10x20_atlas, mono_6x13_atlas, mono_6x13_bold_atlas, mono_6x13_italic_atlas,
|
||||
mono_7x13_atlas, mono_7x13_bold_atlas, mono_7x13_italic_atlas, mono_8x13_atlas,
|
||||
mono_8x13_bold_atlas, mono_8x13_italic_atlas, mono_9x15_atlas, mono_9x15_bold_atlas,
|
||||
mono_9x18_atlas, mono_9x18_bold_atlas,
|
||||
};
|
||||
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
|
||||
|
||||
use crate::block_renderer::BlockCharBackend;
|
||||
|
||||
pub type TerminalType = Terminal<RataguiBackend<BlockCharBackend>>;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum FontChoice {
|
||||
Size6x13,
|
||||
Size7x13,
|
||||
Size8x13,
|
||||
Size9x15,
|
||||
Size9x18,
|
||||
Size10x20,
|
||||
}
|
||||
|
||||
impl FontChoice {
|
||||
pub fn from_setting(s: &str) -> Self {
|
||||
match s {
|
||||
"6x13" => Self::Size6x13,
|
||||
"7x13" => Self::Size7x13,
|
||||
"9x15" => Self::Size9x15,
|
||||
"9x18" => Self::Size9x18,
|
||||
"10x20" => Self::Size10x20,
|
||||
_ => Self::Size8x13,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_setting(self) -> &'static str {
|
||||
match self {
|
||||
Self::Size6x13 => "6x13",
|
||||
Self::Size7x13 => "7x13",
|
||||
Self::Size8x13 => "8x13",
|
||||
Self::Size9x15 => "9x15",
|
||||
Self::Size9x18 => "9x18",
|
||||
Self::Size10x20 => "10x20",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Size6x13 => "6x13 (Compact)",
|
||||
Self::Size7x13 => "7x13",
|
||||
Self::Size8x13 => "8x13 (Default)",
|
||||
Self::Size9x15 => "9x15",
|
||||
Self::Size9x18 => "9x18",
|
||||
Self::Size10x20 => "10x20 (Large)",
|
||||
}
|
||||
}
|
||||
|
||||
pub const ALL: [Self; 6] = [
|
||||
Self::Size6x13,
|
||||
Self::Size7x13,
|
||||
Self::Size8x13,
|
||||
Self::Size9x15,
|
||||
Self::Size9x18,
|
||||
Self::Size10x20,
|
||||
];
|
||||
}
|
||||
|
||||
pub fn create_terminal(font: FontChoice) -> TerminalType {
|
||||
let (regular, bold, italic) = match font {
|
||||
FontChoice::Size6x13 => (
|
||||
mono_6x13_atlas(),
|
||||
Some(mono_6x13_bold_atlas()),
|
||||
Some(mono_6x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size7x13 => (
|
||||
mono_7x13_atlas(),
|
||||
Some(mono_7x13_bold_atlas()),
|
||||
Some(mono_7x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size8x13 => (
|
||||
mono_8x13_atlas(),
|
||||
Some(mono_8x13_bold_atlas()),
|
||||
Some(mono_8x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size9x15 => (mono_9x15_atlas(), Some(mono_9x15_bold_atlas()), None),
|
||||
FontChoice::Size9x18 => (mono_9x18_atlas(), Some(mono_9x18_bold_atlas()), None),
|
||||
FontChoice::Size10x20 => (mono_10x20_atlas(), None, None),
|
||||
};
|
||||
|
||||
// SoftBackend fields are copied individually to wrap raster_backend in BlockCharBackend.
|
||||
// If soft_ratatui changes its field layout, this will fail to compile — that's intentional.
|
||||
let eg = SoftBackend::<EmbeddedGraphics>::new(80, 24, regular, bold, italic);
|
||||
let soft = SoftBackend {
|
||||
buffer: eg.buffer,
|
||||
cursor: eg.cursor,
|
||||
cursor_pos: eg.cursor_pos,
|
||||
char_width: eg.char_width,
|
||||
char_height: eg.char_height,
|
||||
blink_counter: eg.blink_counter,
|
||||
blinking_fast: eg.blinking_fast,
|
||||
blinking_slow: eg.blinking_slow,
|
||||
rgb_pixmap: eg.rgb_pixmap,
|
||||
always_redraw_list: eg.always_redraw_list,
|
||||
raster_backend: BlockCharBackend {
|
||||
inner: eg.raster_backend,
|
||||
},
|
||||
};
|
||||
Terminal::new(RataguiBackend::new("cagire", soft)).expect("terminal")
|
||||
}
|
||||
@@ -519,6 +519,14 @@ fn render_status(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
label_style,
|
||||
)));
|
||||
|
||||
if let Some(input_sr) = app.audio.config.input_sample_rate {
|
||||
let warn_style = Style::new().fg(theme.flash.error_fg);
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" Input {input_sr:.0} Hz !!"),
|
||||
warn_style,
|
||||
)));
|
||||
}
|
||||
|
||||
if !app.plugin_mode {
|
||||
// Host
|
||||
lines.push(Line::from(Span::styled(
|
||||
@@ -946,7 +954,7 @@ fn render_midi_output(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
for slot in 0..4 {
|
||||
let is_focused = section_focused && app.audio.midi_output_slot == slot;
|
||||
let display = midi_display_name(&midi_outputs, app.midi.selected_outputs[slot]);
|
||||
let display = midi_display_name(&midi_outputs, app.midi.selected_outputs()[slot]);
|
||||
let prefix = if is_focused { "> " } else { " " };
|
||||
let style = if is_focused { highlight } else { label_style };
|
||||
let val_style = if is_focused { highlight } else { value_style };
|
||||
|
||||
@@ -507,11 +507,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
||||
};
|
||||
let props_indicator = if has_staged_props { "~" } else { "" };
|
||||
let quant_sync = if is_selected {
|
||||
format!(
|
||||
"{}:{} ",
|
||||
pattern.quantization.short_label(),
|
||||
pattern.sync_mode.short_label()
|
||||
)
|
||||
format!("{} ", pattern.quantization.short_label())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@@ -755,8 +751,6 @@ fn render_properties(
|
||||
let steps_label = format!("{}/{}", content_count, pattern.length);
|
||||
let speed_label = pattern.speed.label();
|
||||
let quant_label = pattern.quantization.label();
|
||||
let sync_label = pattern.sync_mode.label();
|
||||
|
||||
let label_style = Style::new().fg(theme.ui.text_muted);
|
||||
let value_style = Style::new().fg(theme.ui.text_primary);
|
||||
|
||||
@@ -781,10 +775,6 @@ fn render_properties(
|
||||
Span::styled(" Quant ", label_style),
|
||||
Span::styled(quant_label, value_style),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" Sync ", label_style),
|
||||
Span::styled(sync_label, value_style),
|
||||
]),
|
||||
];
|
||||
|
||||
if pattern.follow_up != FollowUp::Loop {
|
||||
|
||||
@@ -737,7 +737,6 @@ fn render_modal(
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
} => {
|
||||
use crate::model::FollowUp;
|
||||
@@ -766,7 +765,6 @@ fn render_modal(
|
||||
("Length", length.clone(), *field == PatternPropsField::Length),
|
||||
("Speed", speed_label, *field == PatternPropsField::Speed),
|
||||
("Quantization", quantization.label().to_string(), *field == PatternPropsField::Quantization),
|
||||
("Sync Mode", sync_mode.label().to_string(), *field == PatternPropsField::SyncMode),
|
||||
("Follow Up", follow_up_label, *field == PatternPropsField::FollowUp),
|
||||
];
|
||||
if is_chain {
|
||||
|
||||
@@ -21,9 +21,9 @@ fn with_params() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_dur() {
|
||||
fn auto_gate() {
|
||||
let outputs = expect_outputs(r#""kick" snd ."#, 1);
|
||||
assert!(outputs[0].contains("dur/"));
|
||||
assert!(outputs[0].contains("gate/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -94,7 +94,7 @@ fn param_only_emit() {
|
||||
assert!(outputs[0].contains("voice/0"));
|
||||
assert!(outputs[0].contains("freq/880"));
|
||||
assert!(!outputs[0].contains("sound/"));
|
||||
assert!(outputs[0].contains("dur/"));
|
||||
assert!(outputs[0].contains("gate/"));
|
||||
assert!(!outputs[0].contains("delaytime/"));
|
||||
}
|
||||
|
||||
@@ -141,8 +141,8 @@ fn polyphonic_with_at() {
|
||||
|
||||
#[test]
|
||||
fn explicit_dur_zero_is_infinite() {
|
||||
let outputs = expect_outputs("880 freq 0 dur .", 1);
|
||||
assert!(outputs[0].contains("dur/0"));
|
||||
let outputs = expect_outputs("880 freq 0 gate .", 1);
|
||||
assert!(outputs[0].contains("gate/0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -21,10 +21,10 @@ fn get_deltas(outputs: &[String]) -> Vec<f64> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_durs(outputs: &[String]) -> Vec<f64> {
|
||||
fn get_gates(outputs: &[String]) -> Vec<f64> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| parse_params(o).get("dur").copied().unwrap_or(0.0))
|
||||
.map(|o| parse_params(o).get("gate").copied().unwrap_or(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -88,10 +88,10 @@ fn alternating_sounds() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dur_is_step_duration() {
|
||||
fn gate_is_step_duration() {
|
||||
let outputs = expect_outputs(r#""kick" snd ."#, 1);
|
||||
let durs = get_durs(&outputs);
|
||||
assert!(approx_eq(durs[0], 0.5), "dur should be 4 * step_duration (0.5), got {}", durs[0]);
|
||||
let gates = get_gates(&outputs);
|
||||
assert!(approx_eq(gates[0], 0.5), "gate should be 4 * step_duration (0.5), got {}", gates[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -13,7 +13,7 @@ const SOUNDS = new Set([
|
||||
const PARAMS = new Set([
|
||||
'freq', 'note', 'gain', 'decay', 'attack', 'release', 'lpf', 'hpf', 'bpf',
|
||||
'verb', 'delay', 'pan', 'orbit', 'harmonics', 'distort', 'speed', 'voice',
|
||||
'dur', 'sustain', 'delaytime', 'delayfb', 'chorus', 'phaser', 'flanger',
|
||||
'dur', 'gate', 'sustain', 'delaytime', 'delayfb', 'chorus', 'phaser', 'flanger',
|
||||
'crush', 'fold', 'wrap', 'resonance', 'begin', 'end', 'velocity', 'chan',
|
||||
'dev', 'ccnum', 'ccout', 'bend', 'pressure', 'program', 'tilt', 'slope',
|
||||
'sub_gain', 'sub_oct', 'feedback', 'depth', 'sweep', 'comb', 'damping',
|
||||
|
||||
Reference in New Issue
Block a user