diff --git a/Cargo.toml b/Cargo.toml index 0480d71..11b0907 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/plugins/cagire-plugins/Cargo.toml b/plugins/cagire-plugins/Cargo.toml index 90276ee..527809d 100644 --- a/plugins/cagire-plugins/Cargo.toml +++ b/plugins/cagire-plugins/Cargo.toml @@ -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" } diff --git a/plugins/cagire-plugins/src/editor.rs b/plugins/cagire-plugins/src/editor.rs index 708f79a..b8e8033 100644 --- a/plugins/cagire-plugins/src/editor.rs +++ b/plugins/cagire-plugins/src/editor.rs @@ -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; diff --git a/plugins/cagire-plugins/src/input_egui.rs b/plugins/cagire-plugins/src/input_egui.rs deleted file mode 100644 index 525acce..0000000 --- a/plugins/cagire-plugins/src/input_egui.rs +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 - ) -} diff --git a/plugins/cagire-plugins/src/lib.rs b/plugins/cagire-plugins/src/lib.rs index 8e0dc2a..ca31fe9 100644 --- a/plugins/cagire-plugins/src/lib.rs +++ b/plugins/cagire-plugins/src/lib.rs @@ -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, + output_channels: usize, + scope_extract_buffer: Vec, fft_producer: Option>, _analysis: Option, pending_note_offs: Vec, @@ -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, ) -> 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, ) -> 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); diff --git a/src/app/mod.rs b/src/app/mod.rs index 6e190a6..5a4e02c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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) } diff --git a/src/bin/desktop/main.rs b/src/bin/desktop/main.rs index df0c035..22f83e5 100644 --- a/src/bin/desktop/main.rs +++ b/src/bin/desktop/main.rs @@ -275,7 +275,7 @@ impl CagireDesktop { std::thread::Builder::new() .name("sample-preload".into()) .spawn(move || { - cagire::init::preload_sample_heads(preload_entries, sr, ®istry); + cagire::engine::preload_sample_heads(preload_entries, sr, ®istry); }) .expect("failed to spawn preload thread"); } diff --git a/src/engine/audio.rs b/src/engine/audio.rs index 0e05393..80e6f50 100644 --- a/src/engine/audio.rs +++ b/src/engine/audio.rs @@ -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, 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, diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 6a25672..8877c3c 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -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, }; diff --git a/src/engine/realtime.rs b/src/engine/realtime.rs index f2f8222..0d33f88 100644 --- a/src/engine/realtime.rs +++ b/src/engine/realtime.rs @@ -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")] diff --git a/src/init.rs b/src/init.rs index 90364e5..b62ef58 100644 --- a/src/init.rs +++ b/src/init.rs @@ -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); - } -} diff --git a/src/input/mod.rs b/src/input/mod.rs index bd4da17..75a5370 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -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, ®istry); + crate::engine::preload_sample_heads(all_preload_entries, sr, ®istry); }) .expect("failed to spawn preload thread"); } diff --git a/src/input/modal.rs b/src/input/modal.rs index 8560624..cfb3996 100644 --- a/src/input/modal.rs +++ b/src/input/modal.rs @@ -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, ®istry); + crate::engine::preload_sample_heads(preload_entries, sr, ®istry); }) .expect("failed to spawn preload thread"); } diff --git a/src/lib.rs b/src/lib.rs index a4a1f55..06a3ed0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/main.rs b/src/main.rs index 8cf844f..09ff919 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, ®istry); + engine::preload_sample_heads(preload_entries, sr, ®istry); }) .expect("failed to spawn preload thread"); } diff --git a/src/midi.rs b/src/midi.rs index 5d396eb..99f7ae9 100644 --- a/src/midi.rs +++ b/src/midi.rs @@ -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 { - 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 { .collect() } +#[cfg(not(feature = "cli"))] +pub fn list_midi_outputs() -> Vec { + Vec::new() +} + +#[cfg(feature = "cli")] pub fn list_midi_inputs() -> Vec { - 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 { .collect() } +#[cfg(not(feature = "cli"))] +pub fn list_midi_inputs() -> Vec { + Vec::new() +} + pub struct MidiState { + #[cfg(feature = "cli")] output_conns: [Option; MAX_MIDI_OUTPUTS], + #[cfg(feature = "cli")] input_conns: [Option>; MAX_MIDI_INPUTS], pub selected_outputs: [Option; MAX_MIDI_OUTPUTS], pub selected_inputs: [Option; 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]); diff --git a/src/settings.rs b/src/settings.rs index 928af33..3ea35ea 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -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) {} }