From c9443d58770eb73550c4a0a2ea61c6adb3729037 Mon Sep 17 00:00:00 2001 From: jess Date: Mon, 30 Mar 2026 21:41:05 -0700 Subject: [PATCH] expand MDK with missing types and trait methods --- au-o2-gui/src/engine/host.rs | 5 + oxforge/Cargo.toml | 1 + oxforge/src/mdk/gui.rs | 43 ++++++ oxforge/src/mdk/mod.rs | 147 ++++++++++++++++++++- oxforge/src/mdk/recording.rs | 22 +++ oxide-modules/hilbert/src/lib.rs | 1 + oxide-modules/region_player/src/lib.rs | 2 +- oxide-modules/spiral_visualizer/src/lib.rs | 2 +- 8 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 oxforge/src/mdk/gui.rs diff --git a/au-o2-gui/src/engine/host.rs b/au-o2-gui/src/engine/host.rs index 6e92435..809689e 100644 --- a/au-o2-gui/src/engine/host.rs +++ b/au-o2-gui/src/engine/host.rs @@ -52,6 +52,11 @@ impl ModuleHost { } } + pub fn load_builtin(&mut self, name: &str, config: &GlobalConfig) -> u32 { + let instance = T::new(config); + self.load_builtin_boxed(Box::new(instance), name) + } + pub fn load_builtin_boxed(&mut self, instance: Box, name: &str) -> u32 { let id = self.next_id; self.next_id += 1; diff --git a/oxforge/Cargo.toml b/oxforge/Cargo.toml index ca16149..fbc195e 100644 --- a/oxforge/Cargo.toml +++ b/oxforge/Cargo.toml @@ -15,6 +15,7 @@ path = "src/main.rs" [dependencies] clap = { version = "4.5.48", features = ["derive"] } +crossbeam-channel = "0.5.12" serde = { version = "1.0.228", features = ["derive"] } toml = "0.9.7" uuid = { version = "1.18.1", features = ["v4", "serde"] } diff --git a/oxforge/src/mdk/gui.rs b/oxforge/src/mdk/gui.rs new file mode 100644 index 0000000..9ec3ef3 --- /dev/null +++ b/oxforge/src/mdk/gui.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +#[derive(Clone)] +pub struct AudioFenceHandle { + gui_to_audio: Arc>>, + audio_to_gui: Arc>>, +} + +impl AudioFenceHandle { + pub fn new() -> Self { + Self { + gui_to_audio: Arc::new(Mutex::new(Vec::new())), + audio_to_gui: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub fn drain_changes(&mut self) -> Vec<(String, f32)> { + let mut lock = self.gui_to_audio.lock().unwrap(); + lock.drain(..).collect() + } + + pub fn write_readback(&self, key: &str, value: f32) { + let mut lock = self.audio_to_gui.lock().unwrap(); + lock.insert(key.to_string(), value); + } + + pub fn push_change(&self, key: String, value: f32) { + let mut lock = self.gui_to_audio.lock().unwrap(); + lock.push((key, value)); + } + + pub fn read_current(&self) -> HashMap { + let lock = self.audio_to_gui.lock().unwrap(); + lock.clone() + } +} + +impl Default for AudioFenceHandle { + fn default() -> Self { + Self::new() + } +} diff --git a/oxforge/src/mdk/mod.rs b/oxforge/src/mdk/mod.rs index 308e103..c848de2 100644 --- a/oxforge/src/mdk/mod.rs +++ b/oxforge/src/mdk/mod.rs @@ -1,3 +1,4 @@ +pub mod gui; pub mod types; pub mod recording; @@ -7,6 +8,7 @@ pub use recording::*; use serde::{Deserialize, Serialize}; use std::any::Any; use std::collections::HashMap; +use std::sync::{Arc, Mutex}; use uuid::Uuid; pub use serde; @@ -80,6 +82,8 @@ pub struct GuiElement { pub controls: String, } +// --- MIDI types --- + #[derive(Debug, Clone, Copy)] pub struct MidiEvent { pub timing: u32, @@ -92,12 +96,45 @@ pub enum MidiMessage { NoteOff { key: u8, velocity: u8 }, } +pub struct MidiInput<'a> { + pub events: &'a [MidiEvent], +} + +impl<'a> MidiInput<'a> { + pub fn new(events: &'a [MidiEvent]) -> Self { + Self { events } + } + + pub fn events(&self) -> &[MidiEvent] { + self.events + } +} + +pub struct MidiOutput { + pub port_name: String, + pub buffer: Arc>>, +} + +impl MidiOutput { + pub fn new(port_name: String, buffer: Arc>>) -> Self { + Self { port_name, buffer } + } + + pub fn send(&self, event: MidiEvent) { + let mut lock = self.buffer.lock().unwrap(); + lock.push(event); + } +} + +// --- Transport / timing --- + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum TransportState { Playing, #[default] Stopped, } + #[derive(Debug, Clone, Copy, Default)] pub struct MusicalTime { pub sample_pos: u64, @@ -106,8 +143,13 @@ pub struct MusicalTime { pub time_signature_numerator: u8, pub time_signature_denominator: u8, pub state: TransportState, + pub cycle_active: bool, + pub cycle_start_sample: u64, + pub cycle_end_sample: u64, } +// --- GUI messages --- + #[derive(Debug, Clone)] pub enum ToGuiMessage { Log(String), @@ -162,6 +204,8 @@ impl ToGuiQueue { } } +// --- Global config --- + #[derive(Clone, Debug, Default)] pub struct GlobalConfig { pub instance_id: Uuid, @@ -169,6 +213,8 @@ pub struct GlobalConfig { pub buffer_size: u32, } +// --- Audio I/O types --- + pub struct MainAudioInput<'a> { pub buffer: &'a [f32] } impl<'a> MainAudioInput<'a> { pub fn iter(&self) -> impl Iterator { self.buffer.iter() } @@ -181,6 +227,8 @@ impl<'a> MainAudioOutput<'a> { pub fn buffer_mut(&mut self) -> &mut [f32] { self.buffer } } +// --- Chain types --- + pub struct ChainInput<'a> { pub data: &'a (dyn Any + Send), } @@ -199,7 +247,7 @@ impl<'a> ChainOutput<'a> { } } -// --- Lane/Bus view types for module port access --- +// --- Lane/Bus view types --- pub struct LaneRef<'a> { real: &'a [f32], @@ -311,15 +359,16 @@ pub struct PortDeclaration { pub struct ModuleContract { pub realtime: bool, pub min_buffer_samples: Option, + pub latency_samples: u32, } impl Default for ModuleContract { fn default() -> Self { - Self { realtime: true, min_buffer_samples: None } + Self { realtime: true, min_buffer_samples: None, latency_samples: 0 } } } -// --- Port view (runtime data passed to process) --- +// --- Port view --- pub struct PortView<'a> { buses_in: HashMap>, @@ -375,6 +424,8 @@ pub struct Ports<'a> { pub chain_in: Option>, pub chain_out: Option>, pub port: PortView<'a>, + pub midi_out: Option, + pub midi_in: Option>, } impl<'a> Default for Ports<'a> { @@ -385,20 +436,110 @@ impl<'a> Default for Ports<'a> { chain_in: None, chain_out: None, port: PortView::new(), + midi_out: None, + midi_in: None, } } } +// --- Process context --- + pub struct ProcessContext { pub time: MusicalTime, pub params: HashMap, pub to_gui: ToGuiQueue, + pub sample_rate: u32, + pub error_log: ErrorChannel, + pub error_report: ErrorChannel, } +// --- Parameter descriptors --- + +#[derive(Debug, Clone)] +pub struct ParameterDescriptor { + pub key: String, + pub label: String, + pub kind: ParamKind, +} + +#[derive(Debug, Clone)] +pub enum ParamKind { + Float { min: f32, max: f32, default: f32, step: Option }, + Bool { default: bool }, + Choice { options: Vec, default: usize }, + Int { min: i32, max: i32, default: i32 }, +} + +// --- Module GUI descriptor --- + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GuiToolkit { + Framebuffer, + Native, +} + +impl Default for GuiToolkit { + fn default() -> Self { + Self::Native + } +} + +#[derive(Debug, Clone)] +pub struct ModuleGuiDescriptor { + pub width: u32, + pub height: u32, + pub title: String, + pub toolkit: GuiToolkit, +} + +// --- Error types --- + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorKind { + Panic, + ProcessError, + PortError, + ContractViolation, +} + +#[derive(Debug, Clone)] +pub struct ModuleError { + pub module_id: u32, + pub module_name: String, + pub error_kind: ErrorKind, + pub message: String, +} + +impl ModuleError { + pub fn new(module_id: u32, module_name: String, error_kind: ErrorKind, message: String) -> Self { + Self { module_id, module_name, error_kind, message } + } +} + +#[derive(Clone)] +pub struct ErrorChannel { + tx: crossbeam_channel::Sender, +} + +impl ErrorChannel { + pub fn new(tx: crossbeam_channel::Sender) -> Self { + Self { tx } + } + + pub fn send(&self, error: ModuleError) { + let _ = self.tx.send(error); + } +} + +// --- OxideModule trait --- + pub trait OxideModule: Send + Sync { fn new(config: &GlobalConfig) -> Self where Self: Sized; fn process(&mut self, ports: Ports, context: &ProcessContext); fn contract(&self) -> ModuleContract { ModuleContract::default() } fn port_declarations(&self) -> Vec { Vec::new() } fn receive_data(&mut self, _key: &str, _data: Box) {} + fn param_descriptors(&self) -> Vec { Vec::new() } + fn gui_descriptor(&self) -> Option { None } + fn has_gui(&self) -> bool { false } } diff --git a/oxforge/src/mdk/recording.rs b/oxforge/src/mdk/recording.rs index 17a4b80..55f01b3 100644 --- a/oxforge/src/mdk/recording.rs +++ b/oxforge/src/mdk/recording.rs @@ -18,6 +18,9 @@ pub enum RecorderMessage { tempo: f32, time_sig_num: u8, }, + CycleBoundary { + boundary_sample: u64, + }, } pub struct PlaybackRegion { @@ -26,4 +29,23 @@ pub struct PlaybackRegion { pub start_sample: u64, pub audio_l: Vec, pub audio_r: Vec, + pub fade_in_samples: u64, + pub fade_out_samples: u64, +} + +#[derive(Debug, Clone)] +pub struct MidiPlaybackNote { + pub start_tick: u64, + pub duration_ticks: u64, + pub note: u8, + pub velocity: u8, + pub channel: u8, +} + +#[derive(Debug, Clone)] +pub struct MidiPlaybackRegion { + pub bus_name: String, + pub region_id: Uuid, + pub start_beat: f64, + pub notes: Vec, } diff --git a/oxide-modules/hilbert/src/lib.rs b/oxide-modules/hilbert/src/lib.rs index 3bbc822..9360f46 100644 --- a/oxide-modules/hilbert/src/lib.rs +++ b/oxide-modules/hilbert/src/lib.rs @@ -40,6 +40,7 @@ impl OxideModule for HilbertModule { ModuleContract { realtime: true, min_buffer_samples: Some(self.fft_size), + latency_samples: 0, } } diff --git a/oxide-modules/region_player/src/lib.rs b/oxide-modules/region_player/src/lib.rs index 132dcb8..b195dde 100644 --- a/oxide-modules/region_player/src/lib.rs +++ b/oxide-modules/region_player/src/lib.rs @@ -49,7 +49,7 @@ impl OxideModule for RegionPlayerModule { } fn contract(&self) -> ModuleContract { - ModuleContract { realtime: true, min_buffer_samples: None } + ModuleContract { realtime: true, min_buffer_samples: None, latency_samples: 0 } } fn receive_data(&mut self, key: &str, data: Box) { diff --git a/oxide-modules/spiral_visualizer/src/lib.rs b/oxide-modules/spiral_visualizer/src/lib.rs index 6d3291f..2666483 100644 --- a/oxide-modules/spiral_visualizer/src/lib.rs +++ b/oxide-modules/spiral_visualizer/src/lib.rs @@ -110,6 +110,6 @@ impl OxideModule for SpiralVisualizer { } fn contract(&self) -> ModuleContract { - ModuleContract { realtime: true, min_buffer_samples: None } + ModuleContract { realtime: true, min_buffer_samples: None, latency_samples: 0 } } }