Feat: clean the codebase as much as possible

This commit is contained in:
2026-02-21 14:46:53 +01:00
parent ab353edc0b
commit 7a95207c58
17 changed files with 233 additions and 368 deletions

View File

@@ -34,12 +34,13 @@ path = "src/bin/desktop/main.rs"
required-features = ["desktop"]
[features]
default = []
block-renderer = ["dep:soft_ratatui", "dep:rustc-hash"]
default = ["cli"]
cli = ["dep:cpal", "dep:midir", "dep:confy", "dep:clap", "dep:thread-priority"]
block-renderer = ["dep:soft_ratatui", "dep:rustc-hash", "dep:egui"]
desktop = [
"cli",
"block-renderer",
"cagire-forth/desktop",
"dep:egui",
"dep:eframe",
"dep:egui_ratatui",
"dep:image",
@@ -54,8 +55,8 @@ doux = { git = "https://github.com/Bubobubobubobubo/doux", features = ["native"]
rusty_link = "0.4"
ratatui = "0.30"
crossterm = "0.29"
cpal = { version = "0.17", features = ["jack"] }
clap = { version = "4", features = ["derive"] }
cpal = { version = "0.17", features = ["jack"], optional = true }
clap = { version = "4", features = ["derive"], optional = true }
rand = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
@@ -64,12 +65,12 @@ tui-big-text = "0.8"
arboard = "3"
minimad = "0.13"
crossbeam-channel = "0.5"
confy = "2"
confy = { version = "2", optional = true }
rustfft = "6"
thread-priority = "1"
thread-priority = { version = "1", optional = true }
ringbuf = "0.4"
arc-swap = "1"
midir = "0.10"
midir = { version = "0.10", optional = true }
parking_lot = "0.12"
libc = "0.2"

View File

@@ -10,7 +10,7 @@ description = "Cagire as a CLAP/VST3 audio plugin"
crate-type = ["cdylib", "lib"]
[dependencies]
cagire = { path = "../..", features = ["block-renderer"] }
cagire = { path = "../..", default-features = false, features = ["block-renderer"] }
cagire-forth = { path = "../../crates/forth" }
cagire-project = { path = "../../crates/project" }
cagire-ratatui = { path = "../../crates/ratatui" }

View File

@@ -25,7 +25,7 @@ use cagire::model::{Dictionary, Rng, Variables};
use cagire::theme;
use cagire::views;
use crate::input_egui::{convert_egui_events, convert_egui_mouse};
use cagire::input_egui::{convert_egui_events, convert_egui_mouse};
use crate::params::CagireParams;
use crate::PluginBridge;

View File

@@ -1,258 +0,0 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use nih_plug_egui::egui;
use ratatui::layout::Rect;
pub fn convert_egui_mouse(
ctx: &egui::Context,
widget_rect: egui::Rect,
term: Rect,
) -> Vec<MouseEvent> {
let mut events = Vec::new();
if widget_rect.width() < 1.0
|| widget_rect.height() < 1.0
|| term.width == 0
|| term.height == 0
{
return events;
}
ctx.input(|i| {
let Some(pos) = i.pointer.latest_pos() else {
return;
};
if !widget_rect.contains(pos) {
return;
}
let col =
((pos.x - widget_rect.left()) / widget_rect.width() * term.width as f32) as u16;
let row =
((pos.y - widget_rect.top()) / widget_rect.height() * term.height as f32) as u16;
let col = col.min(term.width.saturating_sub(1));
let row = row.min(term.height.saturating_sub(1));
if i.pointer.button_clicked(egui::PointerButton::Primary) {
events.push(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: col,
row,
modifiers: KeyModifiers::empty(),
});
}
let scroll = i.raw_scroll_delta.y;
if scroll > 1.0 {
events.push(MouseEvent {
kind: MouseEventKind::ScrollUp,
column: col,
row,
modifiers: KeyModifiers::empty(),
});
} else if scroll < -1.0 {
events.push(MouseEvent {
kind: MouseEventKind::ScrollDown,
column: col,
row,
modifiers: KeyModifiers::empty(),
});
}
});
events
}
pub fn convert_egui_events(ctx: &egui::Context) -> Vec<KeyEvent> {
let mut events = Vec::new();
for event in &ctx.input(|i| i.events.clone()) {
if let Some(key_event) = convert_event(event) {
events.push(key_event);
}
}
events
}
fn convert_event(event: &egui::Event) -> Option<KeyEvent> {
match event {
egui::Event::Key {
key,
pressed,
modifiers,
..
} => {
if !*pressed {
return None;
}
let mods = convert_modifiers(*modifiers);
if is_character_key(*key)
&& !mods.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
{
return None;
}
let code = convert_key(*key)?;
Some(KeyEvent::new(code, mods))
}
egui::Event::Text(text) => {
if text.len() == 1 {
let c = text.chars().next()?;
if !c.is_control() {
return Some(KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty()));
}
}
None
}
egui::Event::Copy => Some(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)),
egui::Event::Cut => Some(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL)),
egui::Event::Paste(_) => Some(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL)),
_ => None,
}
}
fn convert_key(key: egui::Key) -> Option<KeyCode> {
Some(match key {
egui::Key::ArrowDown => KeyCode::Down,
egui::Key::ArrowLeft => KeyCode::Left,
egui::Key::ArrowRight => KeyCode::Right,
egui::Key::ArrowUp => KeyCode::Up,
egui::Key::Escape => KeyCode::Esc,
egui::Key::Tab => KeyCode::Tab,
egui::Key::Backspace => KeyCode::Backspace,
egui::Key::Enter => KeyCode::Enter,
egui::Key::Space => KeyCode::Char(' '),
egui::Key::Insert => KeyCode::Insert,
egui::Key::Delete => KeyCode::Delete,
egui::Key::Home => KeyCode::Home,
egui::Key::End => KeyCode::End,
egui::Key::PageUp => KeyCode::PageUp,
egui::Key::PageDown => KeyCode::PageDown,
egui::Key::F1 => KeyCode::F(1),
egui::Key::F2 => KeyCode::F(2),
egui::Key::F3 => KeyCode::F(3),
egui::Key::F4 => KeyCode::F(4),
egui::Key::F5 => KeyCode::F(5),
egui::Key::F6 => KeyCode::F(6),
egui::Key::F7 => KeyCode::F(7),
egui::Key::F8 => KeyCode::F(8),
egui::Key::F9 => KeyCode::F(9),
egui::Key::F10 => KeyCode::F(10),
egui::Key::F11 => KeyCode::F(11),
egui::Key::F12 => KeyCode::F(12),
egui::Key::A => KeyCode::Char('a'),
egui::Key::B => KeyCode::Char('b'),
egui::Key::C => KeyCode::Char('c'),
egui::Key::D => KeyCode::Char('d'),
egui::Key::E => KeyCode::Char('e'),
egui::Key::F => KeyCode::Char('f'),
egui::Key::G => KeyCode::Char('g'),
egui::Key::H => KeyCode::Char('h'),
egui::Key::I => KeyCode::Char('i'),
egui::Key::J => KeyCode::Char('j'),
egui::Key::K => KeyCode::Char('k'),
egui::Key::L => KeyCode::Char('l'),
egui::Key::M => KeyCode::Char('m'),
egui::Key::N => KeyCode::Char('n'),
egui::Key::O => KeyCode::Char('o'),
egui::Key::P => KeyCode::Char('p'),
egui::Key::Q => KeyCode::Char('q'),
egui::Key::R => KeyCode::Char('r'),
egui::Key::S => KeyCode::Char('s'),
egui::Key::T => KeyCode::Char('t'),
egui::Key::U => KeyCode::Char('u'),
egui::Key::V => KeyCode::Char('v'),
egui::Key::W => KeyCode::Char('w'),
egui::Key::X => KeyCode::Char('x'),
egui::Key::Y => KeyCode::Char('y'),
egui::Key::Z => KeyCode::Char('z'),
egui::Key::Num0 => KeyCode::Char('0'),
egui::Key::Num1 => KeyCode::Char('1'),
egui::Key::Num2 => KeyCode::Char('2'),
egui::Key::Num3 => KeyCode::Char('3'),
egui::Key::Num4 => KeyCode::Char('4'),
egui::Key::Num5 => KeyCode::Char('5'),
egui::Key::Num6 => KeyCode::Char('6'),
egui::Key::Num7 => KeyCode::Char('7'),
egui::Key::Num8 => KeyCode::Char('8'),
egui::Key::Num9 => KeyCode::Char('9'),
egui::Key::Minus => KeyCode::Char('-'),
egui::Key::Equals => KeyCode::Char('='),
egui::Key::OpenBracket => KeyCode::Char('['),
egui::Key::CloseBracket => KeyCode::Char(']'),
egui::Key::Semicolon => KeyCode::Char(';'),
egui::Key::Comma => KeyCode::Char(','),
egui::Key::Period => KeyCode::Char('.'),
egui::Key::Slash => KeyCode::Char('/'),
egui::Key::Backslash => KeyCode::Char('\\'),
egui::Key::Backtick => KeyCode::Char('`'),
egui::Key::Quote => KeyCode::Char('\''),
_ => return None,
})
}
fn convert_modifiers(mods: egui::Modifiers) -> KeyModifiers {
let mut result = KeyModifiers::empty();
if mods.shift {
result |= KeyModifiers::SHIFT;
}
if mods.ctrl || mods.command {
result |= KeyModifiers::CONTROL;
}
if mods.alt {
result |= KeyModifiers::ALT;
}
result
}
fn is_character_key(key: egui::Key) -> bool {
matches!(
key,
egui::Key::A
| egui::Key::B
| egui::Key::C
| egui::Key::D
| egui::Key::E
| egui::Key::F
| egui::Key::G
| egui::Key::H
| egui::Key::I
| egui::Key::J
| egui::Key::K
| egui::Key::L
| egui::Key::M
| egui::Key::N
| egui::Key::O
| egui::Key::P
| egui::Key::Q
| egui::Key::R
| egui::Key::S
| egui::Key::T
| egui::Key::U
| egui::Key::V
| egui::Key::W
| egui::Key::X
| egui::Key::Y
| egui::Key::Z
| egui::Key::Num0
| egui::Key::Num1
| egui::Key::Num2
| egui::Key::Num3
| egui::Key::Num4
| egui::Key::Num5
| egui::Key::Num6
| egui::Key::Num7
| egui::Key::Num8
| egui::Key::Num9
| egui::Key::Space
| egui::Key::Minus
| egui::Key::Equals
| egui::Key::OpenBracket
| egui::Key::CloseBracket
| egui::Key::Semicolon
| egui::Key::Comma
| egui::Key::Period
| egui::Key::Slash
| egui::Key::Backslash
| egui::Key::Backtick
| egui::Key::Quote
)
}

View File

@@ -1,5 +1,4 @@
mod editor;
mod input_egui;
mod params;
use std::collections::HashMap;
@@ -51,6 +50,8 @@ pub struct CagirePlugin {
rng: Rng,
cmd_buffer: String,
audio_buffer: Vec<f32>,
output_channels: usize,
scope_extract_buffer: Vec<f32>,
fft_producer: Option<ringbuf::HeapProd<f32>>,
_analysis: Option<AnalysisHandle>,
pending_note_offs: Vec<PendingNoteOff>,
@@ -88,6 +89,8 @@ impl Default for CagirePlugin {
rng,
cmd_buffer: String::with_capacity(256),
audio_buffer: Vec::new(),
output_channels: 2,
scope_extract_buffer: Vec::new(),
fft_producer: None,
_analysis: None,
pending_note_offs: Vec::new(),
@@ -105,19 +108,36 @@ impl Plugin for CagirePlugin {
const EMAIL: &'static str = "raphael.forment@gmail.com";
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout {
main_input_channels: None,
main_output_channels: Some(new_nonzero_u32(2)),
aux_input_ports: &[],
aux_output_ports: &[],
names: PortNames {
layout: Some("Stereo"),
main_input: None,
main_output: Some("Output"),
aux_inputs: &[],
aux_outputs: &[],
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[
AudioIOLayout {
main_input_channels: None,
main_output_channels: Some(new_nonzero_u32(2)),
aux_input_ports: &[],
aux_output_ports: &[new_nonzero_u32(2); 7],
names: PortNames {
layout: Some("Multi-output"),
main_input: None,
main_output: Some("Orbit 0"),
aux_inputs: &[],
aux_outputs: &[
"Orbit 1", "Orbit 2", "Orbit 3", "Orbit 4", "Orbit 5", "Orbit 6", "Orbit 7",
],
},
},
}];
AudioIOLayout {
main_input_channels: None,
main_output_channels: Some(new_nonzero_u32(2)),
aux_input_ports: &[],
aux_output_ports: &[],
names: PortNames {
layout: Some("Stereo"),
main_input: None,
main_output: Some("Output"),
aux_inputs: &[],
aux_outputs: &[],
},
},
];
const MIDI_INPUT: MidiConfig = MidiConfig::MidiCCs;
const MIDI_OUTPUT: MidiConfig = MidiConfig::MidiCCs;
@@ -139,7 +159,7 @@ impl Plugin for CagirePlugin {
fn initialize(
&mut self,
_audio_io_layout: &AudioIOLayout,
audio_io_layout: &AudioIOLayout,
buffer_config: &BufferConfig,
_context: &mut impl InitContext<Self>,
) -> bool {
@@ -147,6 +167,9 @@ impl Plugin for CagirePlugin {
self.sample_pos = 0;
self.prev_beat = -1.0;
let num_aux = audio_io_layout.aux_output_ports.len();
self.output_channels = 2 + num_aux * 2;
self.seq_state = Some(SequencerState::new(
Arc::clone(&self.variables),
Arc::clone(&self.dict),
@@ -156,7 +179,7 @@ impl Plugin for CagirePlugin {
let engine = doux::Engine::new_with_channels(
self.sample_rate,
2,
self.output_channels,
64,
);
self.bridge
@@ -217,7 +240,7 @@ impl Plugin for CagirePlugin {
fn process(
&mut self,
buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers,
aux: &mut AuxiliaryBuffers,
context: &mut impl ProcessContext<Self>,
) -> ProcessStatus {
let Some(seq_state) = &mut self.seq_state else {
@@ -399,34 +422,45 @@ impl Plugin for CagirePlugin {
engine.evaluate(cmd_ref);
}
// Process audio block — doux writes interleaved stereo into our buffer
let num_samples = buffer_len * 2;
self.audio_buffer.resize(num_samples, 0.0);
// Process audio block — doux writes interleaved into our buffer
let total_samples = buffer_len * self.output_channels;
self.audio_buffer.resize(total_samples, 0.0);
self.audio_buffer.fill(0.0);
engine.process_block(&mut self.audio_buffer, &[], &[]);
// Feed scope and spectrum analysis
self.bridge.scope_buffer.write(&self.audio_buffer);
// Feed scope and spectrum analysis (orbit 0 only)
if self.output_channels == 2 {
self.bridge.scope_buffer.write(&self.audio_buffer);
} else {
self.scope_extract_buffer.resize(buffer_len * 2, 0.0);
for i in 0..buffer_len {
self.scope_extract_buffer[i * 2] =
self.audio_buffer[i * self.output_channels];
self.scope_extract_buffer[i * 2 + 1] =
self.audio_buffer[i * self.output_channels + 1];
}
self.bridge.scope_buffer.write(&self.scope_extract_buffer);
}
if let Some(producer) = &mut self.fft_producer {
for chunk in self.audio_buffer.chunks(2) {
let mono = (chunk[0] + chunk.get(1).copied().unwrap_or(0.0)) * 0.5;
let _ = producer.try_push(mono);
let stride = self.output_channels;
for i in 0..buffer_len {
let left = self.audio_buffer[i * stride];
let right = self.audio_buffer[i * stride + 1];
let _ = producer.try_push((left + right) * 0.5);
}
}
// Copy interleaved doux output nih-plug channel slices
let mut channel_iter = buffer.iter_samples();
for frame_idx in 0..buffer_len {
if let Some(mut frame) = channel_iter.next() {
let left = self.audio_buffer[frame_idx * 2];
let right = self.audio_buffer[frame_idx * 2 + 1];
if let Some(sample) = frame.get_mut(0) {
*sample = left;
}
if let Some(sample) = frame.get_mut(1) {
*sample = right;
}
}
// De-interleave doux output into nih-plug channel buffers
let stride = self.output_channels;
deinterleave_stereo(&self.audio_buffer, buffer.as_slice(), stride, 0, buffer_len);
for (aux_idx, aux_buf) in aux.outputs.iter_mut().enumerate() {
deinterleave_stereo(
&self.audio_buffer,
aux_buf.as_slice(),
stride,
(aux_idx + 1) * 2,
buffer_len,
);
}
self.sample_pos += buffer_len as u64;
@@ -445,6 +479,7 @@ impl ClapPlugin for CagirePlugin {
ClapFeature::Instrument,
ClapFeature::Synthesizer,
ClapFeature::Stereo,
ClapFeature::Surround,
];
}
@@ -454,8 +489,23 @@ impl Vst3Plugin for CagirePlugin {
Vst3SubCategory::Instrument,
Vst3SubCategory::Synth,
Vst3SubCategory::Stereo,
Vst3SubCategory::Surround,
];
}
fn deinterleave_stereo(
interleaved: &[f32],
out: &mut [&mut [f32]],
stride: usize,
ch_offset: usize,
len: usize,
) {
let (left, right) = out.split_at_mut(1);
for (i, (l, r)) in left[0][..len].iter_mut().zip(right[0][..len].iter_mut()).enumerate() {
*l = interleaved[i * stride + ch_offset];
*r = interleaved[i * stride + ch_offset + 1];
}
}
nih_export_clap!(CagirePlugin);
nih_export_vst3!(CagirePlugin);

View File

@@ -86,12 +86,7 @@ impl App {
Self::build(variables, dict, rng, false)
}
#[allow(dead_code)]
pub fn with_shared(variables: Variables, dict: Dictionary, rng: Rng) -> Self {
Self::build(variables, dict, rng, false)
}
#[allow(dead_code)]
#[allow(dead_code)] // used by plugin crate
pub fn new_plugin(variables: Variables, dict: Dictionary, rng: Rng) -> Self {
Self::build(variables, dict, rng, true)
}

View File

@@ -275,7 +275,7 @@ impl CagireDesktop {
std::thread::Builder::new()
.name("sample-preload".into())
.spawn(move || {
cagire::init::preload_sample_heads(preload_entries, sr, &registry);
cagire::engine::preload_sample_heads(preload_entries, sr, &registry);
})
.expect("failed to spawn preload thread");
}

View File

@@ -1,14 +1,11 @@
use cpal::traits::{DeviceTrait, StreamTrait};
use cpal::Stream;
use crossbeam_channel::Receiver;
use doux::{Engine, EngineMetrics};
use ringbuf::{traits::*, HeapRb};
use rustfft::{num_complex::Complex, FftPlanner};
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use super::AudioCommand;
#[cfg(feature = "cli")]
use std::sync::atomic::AtomicU64;
pub struct ScopeBuffer {
pub samples: [AtomicU32; 256],
@@ -230,6 +227,36 @@ fn analysis_loop(
}
}
pub fn preload_sample_heads(
entries: Vec<(String, std::path::PathBuf)>,
target_sr: f32,
registry: &doux::SampleRegistry,
) {
let mut batch = Vec::with_capacity(entries.len());
for (name, path) in &entries {
match doux::sampling::decode_sample_head(path, target_sr) {
Ok(data) => batch.push((name.clone(), Arc::new(data))),
Err(e) => eprintln!("preload {name}: {e}"),
}
}
if !batch.is_empty() {
registry.insert_batch(batch);
}
}
#[cfg(feature = "cli")]
use cpal::traits::{DeviceTrait, StreamTrait};
#[cfg(feature = "cli")]
use cpal::Stream;
#[cfg(feature = "cli")]
use crossbeam_channel::Receiver;
#[cfg(feature = "cli")]
use doux::{Engine, EngineMetrics};
#[cfg(feature = "cli")]
use super::AudioCommand;
#[cfg(feature = "cli")]
pub struct AudioStreamConfig {
pub output_device: Option<String>,
pub channels: u16,
@@ -237,12 +264,14 @@ pub struct AudioStreamConfig {
pub max_voices: usize,
}
#[cfg(feature = "cli")]
pub struct AudioStreamInfo {
pub sample_rate: f32,
pub host_name: String,
pub channels: u16,
}
#[cfg(feature = "cli")]
pub fn build_stream(
config: &AudioStreamConfig,
audio_rx: Receiver<AudioCommand>,

View File

@@ -7,16 +7,20 @@ mod timing;
pub use timing::{substeps_in_window, StepTiming, SyncTime};
// AnalysisHandle and SequencerHandle are used by src/bin/desktop.rs
// Used by plugin and desktop crates via the lib; not by the terminal binary directly.
#[allow(unused_imports)]
pub use audio::{
build_stream, spawn_analysis_thread, AnalysisHandle, AudioStreamConfig, ScopeBuffer,
SpectrumBuffer,
preload_sample_heads, spawn_analysis_thread, AnalysisHandle, ScopeBuffer, SpectrumBuffer,
};
#[cfg(feature = "cli")]
#[allow(unused_imports)]
pub use audio::{build_stream, AudioStreamConfig, AudioStreamInfo};
pub use link::LinkState;
#[allow(unused_imports)]
pub use sequencer::{
parse_midi_command, spawn_sequencer, AudioCommand, MidiCommand, PatternChange,
PatternSnapshot, SeqCommand, SequencerConfig, SequencerHandle, SequencerSnapshot,
SequencerState, SharedSequencerState, StepSnapshot, TickInput, TickOutput, TimestampedCommand,
parse_midi_command, spawn_sequencer, AudioCommand, MidiCommand, PatternChange, PatternSnapshot,
SeqCommand, SequencerConfig, SequencerHandle, SequencerSnapshot, SequencerState,
SharedSequencerState, StepSnapshot, TickInput, TickOutput, TimestampedCommand,
};

View File

@@ -28,27 +28,16 @@ mod memory {
false
}
}
#[allow(dead_code)]
pub fn is_memory_locked() -> bool {
MLOCKALL_SUCCESS.load(Ordering::Relaxed)
}
}
#[cfg(target_os = "linux")]
pub use memory::{is_memory_locked, lock_memory};
pub use memory::lock_memory;
#[cfg(not(target_os = "linux"))]
pub fn lock_memory() -> bool {
true
}
#[cfg(not(target_os = "linux"))]
#[allow(dead_code)]
pub fn is_memory_locked() -> bool {
false
}
/// Attempts to set realtime scheduling priority for the current thread.
/// Returns true if RT priority was successfully set, false otherwise.
#[cfg(target_os = "macos")]
@@ -105,7 +94,7 @@ pub fn set_realtime_priority() -> bool {
/// - Configured rtprio limits in /etc/security/limits.conf:
/// @audio - rtprio 95
/// @audio - memlock unlimited
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", feature = "cli"))]
pub fn set_realtime_priority() -> bool {
use thread_priority::unix::{
set_thread_priority_and_policy, thread_native_id, NormalThreadSchedulePolicy,
@@ -138,17 +127,27 @@ pub fn set_realtime_priority() -> bool {
false
}
#[cfg(all(target_os = "linux", not(feature = "cli")))]
pub fn set_realtime_priority() -> bool {
false
}
#[cfg(not(any(unix, target_os = "windows")))]
pub fn set_realtime_priority() -> bool {
false
}
#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "cli"))]
pub fn set_realtime_priority() -> bool {
use thread_priority::{set_current_thread_priority, ThreadPriority};
set_current_thread_priority(ThreadPriority::Max).is_ok()
}
#[cfg(all(target_os = "windows", not(feature = "cli")))]
pub fn set_realtime_priority() -> bool {
false
}
/// High-precision sleep using clock_nanosleep on Linux.
/// Uses monotonic clock for jitter-free sleeping.
#[cfg(target_os = "linux")]

View File

@@ -7,8 +7,9 @@ use doux::EngineMetrics;
use crate::app::App;
use crate::engine::{
build_stream, spawn_sequencer, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand,
PatternChange, ScopeBuffer, SequencerConfig, SequencerHandle, SpectrumBuffer,
build_stream, preload_sample_heads, spawn_sequencer, AnalysisHandle, AudioStreamConfig,
LinkState, MidiCommand, PatternChange, ScopeBuffer, SequencerConfig, SequencerHandle,
SpectrumBuffer,
};
use crate::midi;
use crate::model;
@@ -230,19 +231,3 @@ pub fn init(args: InitArgs) -> Init {
}
}
pub fn preload_sample_heads(
entries: Vec<(String, std::path::PathBuf)>,
target_sr: f32,
registry: &doux::SampleRegistry,
) {
let mut batch = Vec::with_capacity(entries.len());
for (name, path) in &entries {
match doux::sampling::decode_sample_head(path, target_sr) {
Ok(data) => batch.push((name.clone(), Arc::new(data))),
Err(e) => eprintln!("preload {name}: {e}"),
}
}
if !batch.is_empty() {
registry.insert_batch(batch);
}
}

View File

@@ -209,7 +209,7 @@ fn load_project_samples(ctx: &mut InputContext) {
std::thread::Builder::new()
.name("sample-preload".into())
.spawn(move || {
crate::init::preload_sample_heads(all_preload_entries, sr, &registry);
crate::engine::preload_sample_heads(all_preload_entries, sr, &registry);
})
.expect("failed to spawn preload thread");
}

View File

@@ -208,7 +208,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
std::thread::Builder::new()
.name("sample-preload".into())
.spawn(move || {
crate::init::preload_sample_heads(preload_entries, sr, &registry);
crate::engine::preload_sample_heads(preload_entries, sr, &registry);
})
.expect("failed to spawn preload thread");
}

View File

@@ -1,6 +1,7 @@
pub use cagire_forth as forth;
pub mod app;
#[cfg(feature = "cli")]
pub mod init;
pub mod commands;
pub mod engine;
@@ -18,5 +19,5 @@ pub mod widgets;
#[cfg(feature = "block-renderer")]
pub mod block_renderer;
#[cfg(feature = "desktop")]
#[cfg(feature = "block-renderer")]
pub mod input_egui;

View File

@@ -149,7 +149,7 @@ fn main() -> io::Result<()> {
std::thread::Builder::new()
.name("sample-preload".into())
.spawn(move || {
init::preload_sample_heads(preload_entries, sr, &registry);
engine::preload_sample_heads(preload_entries, sr, &registry);
})
.expect("failed to spawn preload thread");
}

View File

@@ -1,8 +1,6 @@
use parking_lot::Mutex;
use std::sync::Arc;
use midir::{MidiInput, MidiOutput};
use crate::model::CcAccess;
pub const MAX_MIDI_OUTPUTS: usize = 4;
@@ -22,12 +20,12 @@ impl CcMemory {
Self(Arc::new(Mutex::new([[[0u8; 128]; 16]; MAX_MIDI_DEVICES])))
}
#[cfg(feature = "cli")]
fn inner(&self) -> &CcMemoryInner {
&self.0
}
/// Set a CC value (for testing)
#[allow(dead_code)]
#[allow(dead_code)] // used by integration tests
pub fn set_cc(&self, device: usize, channel: usize, cc: usize, value: u8) {
let mut mem = self.0.lock();
mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)] = value;
@@ -52,8 +50,9 @@ pub struct MidiDeviceInfo {
pub name: String,
}
#[cfg(feature = "cli")]
pub fn list_midi_outputs() -> Vec<MidiDeviceInfo> {
let Ok(midi_out) = MidiOutput::new("cagire-probe") else {
let Ok(midi_out) = midir::MidiOutput::new("cagire-probe") else {
return Vec::new();
};
midi_out
@@ -68,8 +67,14 @@ pub fn list_midi_outputs() -> Vec<MidiDeviceInfo> {
.collect()
}
#[cfg(not(feature = "cli"))]
pub fn list_midi_outputs() -> Vec<MidiDeviceInfo> {
Vec::new()
}
#[cfg(feature = "cli")]
pub fn list_midi_inputs() -> Vec<MidiDeviceInfo> {
let Ok(midi_in) = MidiInput::new("cagire-probe") else {
let Ok(midi_in) = midir::MidiInput::new("cagire-probe") else {
return Vec::new();
};
midi_in
@@ -84,8 +89,15 @@ pub fn list_midi_inputs() -> Vec<MidiDeviceInfo> {
.collect()
}
#[cfg(not(feature = "cli"))]
pub fn list_midi_inputs() -> Vec<MidiDeviceInfo> {
Vec::new()
}
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 selected_inputs: [Option<usize>; MAX_MIDI_INPUTS],
@@ -101,7 +113,9 @@ 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],
selected_inputs: [None; MAX_MIDI_INPUTS],
@@ -109,11 +123,13 @@ impl MidiState {
}
}
#[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 = MidiOutput::new(&format!("cagire-out-{slot}")).map_err(|e| e.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
@@ -124,6 +140,12 @@ impl MidiState {
Ok(())
}
#[cfg(not(feature = "cli"))]
pub fn connect_output(&mut self, _slot: usize, _port_index: usize) -> Result<(), String> {
Ok(())
}
#[cfg(feature = "cli")]
pub fn disconnect_output(&mut self, slot: usize) {
if slot < MAX_MIDI_OUTPUTS {
self.output_conns[slot] = None;
@@ -131,11 +153,20 @@ impl MidiState {
}
}
#[cfg(not(feature = "cli"))]
pub fn disconnect_output(&mut self, slot: usize) {
if slot < MAX_MIDI_OUTPUTS {
self.selected_outputs[slot] = None;
}
}
#[cfg(feature = "cli")]
pub fn connect_input(&mut self, slot: usize, port_index: usize) -> Result<(), String> {
if slot >= MAX_MIDI_INPUTS {
return Err("Invalid input slot".to_string());
}
let midi_in = MidiInput::new(&format!("cagire-in-{slot}")).map_err(|e| e.to_string())?;
let midi_in =
midir::MidiInput::new(&format!("cagire-in-{slot}")).map_err(|e| e.to_string())?;
let ports = midi_in.ports();
let port = ports.get(port_index).ok_or("MIDI input port not found")?;
@@ -165,6 +196,12 @@ impl MidiState {
Ok(())
}
#[cfg(not(feature = "cli"))]
pub fn connect_input(&mut self, _slot: usize, _port_index: usize) -> Result<(), String> {
Ok(())
}
#[cfg(feature = "cli")]
pub fn disconnect_input(&mut self, slot: usize) {
if slot < MAX_MIDI_INPUTS {
self.input_conns[slot] = None;
@@ -172,6 +209,14 @@ impl MidiState {
}
}
#[cfg(not(feature = "cli"))]
pub fn disconnect_input(&mut self, slot: usize) {
if slot < MAX_MIDI_INPUTS {
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] {
@@ -179,6 +224,9 @@ impl MidiState {
}
}
#[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]);

View File

@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
use crate::state::{ColorScheme, MainLayout};
#[cfg(feature = "cli")]
const APP_NAME: &str = "cagire";
#[derive(Debug, Default, Serialize, Deserialize)]
@@ -116,6 +117,7 @@ impl Default for LinkSettings {
}
impl Settings {
#[cfg(feature = "cli")]
pub fn load() -> Self {
let mut settings: Self = confy::load(APP_NAME, None).unwrap_or_default();
if settings.audio.channels == 0 {
@@ -127,10 +129,19 @@ impl Settings {
settings
}
#[cfg(not(feature = "cli"))]
pub fn load() -> Self {
Self::default()
}
#[cfg(feature = "cli")]
pub fn save(&self) {
if let Err(e) = confy::store(APP_NAME, None, self) {
eprintln!("Failed to save settings: {e}");
}
}
#[cfg(not(feature = "cli"))]
pub fn save(&self) {}
}