From 06314dbf57eb61e91775699f1b087cec6487932d Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 3 Nov 2025 06:16:17 -0500 Subject: [PATCH] Add MIDI input --- daw-backend/Cargo.lock | 89 ++++++++++++++- daw-backend/Cargo.toml | 1 + daw-backend/src/audio/engine.rs | 44 +++++++ daw-backend/src/command/types.rs | 2 + daw-backend/src/io/midi_input.rs | 190 +++++++++++++++++++++++++++++++ daw-backend/src/io/mod.rs | 2 + daw-backend/src/lib.rs | 17 ++- src-tauri/Cargo.lock | 101 ++++++++++++++-- src-tauri/src/audio.rs | 14 +++ src-tauri/src/lib.rs | 1 + src/widgets.js | 12 ++ 11 files changed, 462 insertions(+), 11 deletions(-) create mode 100644 daw-backend/src/io/midi_input.rs diff --git a/daw-backend/Cargo.lock b/daw-backend/Cargo.lock index 109da10..c30f2ac 100644 --- a/daw-backend/Cargo.lock +++ b/daw-backend/Cargo.lock @@ -17,6 +17,18 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "alsa" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2562ad8dcf0f789f65c6fdaad8a8a9708ed6b488e649da28c01656ad66b8b47" +dependencies = [ + "alsa-sys", + "bitflags 1.3.2", + "libc", + "nix", +] + [[package]] name = "alsa" version = "0.9.1" @@ -187,6 +199,16 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -213,13 +235,33 @@ dependencies = [ "bindgen", ] +[[package]] +name = "coremidi" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7847ca018a67204508b77cb9e6de670125075f7464fff5f673023378fa34f5" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "coremidi-sys", +] + +[[package]] +name = "coremidi-sys" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9504310988d938e49fff1b5f1e56e3dafe39bb1bae580c19660b58b83a191e" +dependencies = [ + "core-foundation-sys", +] + [[package]] name = "cpal" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" dependencies = [ - "alsa", + "alsa 0.9.1", "core-foundation-sys", "coreaudio-rs", "dasp_sample", @@ -233,7 +275,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows", + "windows 0.54.0", ] [[package]] @@ -415,6 +457,7 @@ dependencies = [ "dasp_rms", "dasp_sample", "dasp_signal", + "midir", "midly", "pathdiff", "petgraph 0.6.5", @@ -682,6 +725,22 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "midir" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a456444d83e7ead06ae6a5c0a215ed70282947ff3897fb45fcb052b757284731" +dependencies = [ + "alsa 0.7.1", + "bitflags 1.3.2", + "coremidi", + "js-sys", + "libc", + "wasm-bindgen", + "web-sys", + "windows 0.43.0", +] + [[package]] name = "midly" version = "0.5.3" @@ -738,6 +797,17 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -1615,6 +1685,21 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows" version = "0.54.0" diff --git a/daw-backend/Cargo.toml b/daw-backend/Cargo.toml index f973e3e..7a67cc8 100644 --- a/daw-backend/Cargo.toml +++ b/daw-backend/Cargo.toml @@ -8,6 +8,7 @@ cpal = "0.15" symphonia = { version = "0.5", features = ["all"] } rtrb = "0.3" midly = "0.5" +midir = "0.9" serde = { version = "1.0", features = ["derive"] } ratatui = "0.26" crossterm = "0.27" diff --git a/daw-backend/src/audio/engine.rs b/daw-backend/src/audio/engine.rs index 1c5ea4f..096d8d3 100644 --- a/daw-backend/src/audio/engine.rs +++ b/daw-backend/src/audio/engine.rs @@ -7,6 +7,7 @@ use crate::audio::project::Project; use crate::audio::recording::{MidiRecordingState, RecordingState}; use crate::audio::track::{Track, TrackId, TrackNode}; use crate::command::{AudioEvent, Command, Query, QueryResponse}; +use crate::io::MidiInputManager; use petgraph::stable_graph::NodeIndex; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use std::sync::Arc; @@ -23,6 +24,7 @@ pub struct Engine { // Lock-free communication command_rx: rtrb::Consumer, + midi_command_rx: Option>, event_tx: rtrb::Producer, query_rx: rtrb::Consumer, query_response_tx: rtrb::Producer, @@ -50,6 +52,9 @@ pub struct Engine { // MIDI recording state midi_recording_state: Option, + + // MIDI input manager for external MIDI devices + midi_input_manager: Option, } impl Engine { @@ -76,6 +81,7 @@ impl Engine { playing: false, channels, command_rx, + midi_command_rx: None, event_tx, query_rx, query_response_tx, @@ -89,6 +95,7 @@ impl Engine { input_rx: None, recording_progress_counter: 0, midi_recording_state: None, + midi_input_manager: None, } } @@ -97,6 +104,16 @@ impl Engine { self.input_rx = Some(input_rx); } + /// Set the MIDI input manager for external MIDI devices + pub fn set_midi_input_manager(&mut self, manager: MidiInputManager) { + self.midi_input_manager = Some(manager); + } + + /// Set the MIDI command receiver for external MIDI input + pub fn set_midi_command_rx(&mut self, midi_command_rx: rtrb::Consumer) { + self.midi_command_rx = Some(midi_command_rx); + } + /// Add an audio track to the engine pub fn add_track(&mut self, track: Track) -> TrackId { // For backwards compatibility, we'll extract the track data and add it to the project @@ -182,6 +199,21 @@ impl Engine { self.handle_command(cmd); } + // Process all pending MIDI commands + loop { + let midi_cmd = if let Some(ref mut midi_rx) = self.midi_command_rx { + midi_rx.pop().ok() + } else { + None + }; + + if let Some(cmd) = midi_cmd { + self.handle_command(cmd); + } else { + break; + } + } + // Process all pending queries while let Ok(query) = self.query_rx.pop() { self.handle_query(query); @@ -711,6 +743,13 @@ impl Engine { } } + Command::SetActiveMidiTrack(track_id) => { + // Update the active MIDI track for external MIDI input routing + if let Some(ref midi_manager) = self.midi_input_manager { + midi_manager.set_active_track(track_id); + } + } + // Node graph commands Command::GraphAddNode(track_id, node_type, x, y) => { eprintln!("[DEBUG] GraphAddNode received: track_id={}, node_type={}, x={}, y={}", track_id, node_type, x, y); @@ -2093,6 +2132,11 @@ impl EngineController { let _ = self.command_tx.push(Command::SendMidiNoteOff(track_id, note)); } + /// Set the active MIDI track for external MIDI input routing + pub fn set_active_midi_track(&mut self, track_id: Option) { + let _ = self.command_tx.push(Command::SetActiveMidiTrack(track_id)); + } + // Node graph operations /// Add a node to a track's instrument graph diff --git a/daw-backend/src/command/types.rs b/daw-backend/src/command/types.rs index d1f9ee7..099bdad 100644 --- a/daw-backend/src/command/types.rs +++ b/daw-backend/src/command/types.rs @@ -113,6 +113,8 @@ pub enum Command { SendMidiNoteOn(TrackId, u8, u8), /// Send a live MIDI note off event to a track's instrument (track_id, note) SendMidiNoteOff(TrackId, u8), + /// Set the active MIDI track for external MIDI input routing (track_id or None) + SetActiveMidiTrack(Option), // Node graph commands /// Add a node to a track's instrument graph (track_id, node_type, position_x, position_y) diff --git a/daw-backend/src/io/midi_input.rs b/daw-backend/src/io/midi_input.rs new file mode 100644 index 0000000..2554ef8 --- /dev/null +++ b/daw-backend/src/io/midi_input.rs @@ -0,0 +1,190 @@ +use crate::audio::track::TrackId; +use crate::command::Command; +use midir::{MidiInput, MidiInputConnection}; +use std::sync::{Arc, Mutex}; + +/// Manages external MIDI input devices and routes MIDI to the currently active track +pub struct MidiInputManager { + connections: Vec, + active_track_id: Arc>>, +} + +struct ActiveMidiConnection { + #[allow(dead_code)] + device_name: String, + #[allow(dead_code)] + connection: MidiInputConnection<()>, +} + +impl MidiInputManager { + /// Create a new MIDI input manager and auto-connect to all available devices + pub fn new(command_tx: rtrb::Producer) -> Result { + let active_track_id = Arc::new(Mutex::new(None)); + let mut connections = Vec::new(); + + // Wrap command producer in Arc for sharing across MIDI callbacks + let shared_command_tx = Arc::new(Mutex::new(command_tx)); + + // Initialize MIDI input + let mut midi_in = MidiInput::new("Lightningbeam") + .map_err(|e| format!("Failed to initialize MIDI input: {}", e))?; + + // Get all available MIDI input ports + let ports = midi_in.ports(); + + println!("MIDI Input: Found {} device(s)", ports.len()); + + // We need to recreate MidiInput for each connection since connect() consumes it + // Store port info first + let mut port_infos = Vec::new(); + for port in &ports { + if let Ok(port_name) = midi_in.port_name(port) { + port_infos.push((port.clone(), port_name)); + } + } + + // Connect to each available device + for (port, port_name) in port_infos { + println!("MIDI: Connecting to device: {}", port_name); + + // Recreate MidiInput for this connection + midi_in = MidiInput::new("Lightningbeam") + .map_err(|e| format!("Failed to recreate MIDI input: {}", e))?; + + let device_name = port_name.clone(); + let cmd_tx = shared_command_tx.clone(); + let active_id = active_track_id.clone(); + + match midi_in.connect( + &port, + &format!("lightningbeam-{}", port_name), + move |_timestamp, message, _| { + Self::on_midi_message(message, &cmd_tx, &active_id, &device_name); + }, + (), + ) { + Ok(connection) => { + connections.push(ActiveMidiConnection { + device_name: port_name.clone(), + connection, + }); + println!("MIDI: Connected to: {}", port_name); + + // Need to recreate MidiInput for next iteration + midi_in = MidiInput::new("Lightningbeam") + .map_err(|e| format!("Failed to recreate MIDI input: {}", e))?; + } + Err(e) => { + eprintln!("MIDI: Failed to connect to {}: {}", port_name, e); + // Recreate MidiInput to continue with other ports + midi_in = MidiInput::new("Lightningbeam") + .map_err(|e| format!("Failed to recreate MIDI input: {}", e))?; + } + } + } + + println!("MIDI Input: Connected to {} device(s)", connections.len()); + + Ok(Self { + connections, + active_track_id, + }) + } + + /// MIDI input callback - parses MIDI messages and sends commands to audio engine + fn on_midi_message( + message: &[u8], + command_tx: &Mutex>, + active_track_id: &Arc>>, + device_name: &str, + ) { + if message.is_empty() { + return; + } + + // Get the currently active track + let track_id = { + let active = active_track_id.lock().unwrap(); + match *active { + Some(id) => id, + None => { + // No active track, ignore MIDI input + return; + } + } + }; + + let status_byte = message[0]; + let status = status_byte & 0xF0; + let _channel = status_byte & 0x0F; + + match status { + 0x90 => { + // Note On + if message.len() >= 3 { + let note = message[1]; + let velocity = message[2]; + + // Treat velocity 0 as Note Off (per MIDI spec) + if velocity == 0 { + let mut tx = command_tx.lock().unwrap(); + let _ = tx.push(Command::SendMidiNoteOff(track_id, note)); + println!("MIDI [{}] Note Off: {} (velocity 0)", device_name, note); + } else { + let mut tx = command_tx.lock().unwrap(); + let _ = tx.push(Command::SendMidiNoteOn(track_id, note, velocity)); + println!("MIDI [{}] Note On: {} vel {}", device_name, note, velocity); + } + } + } + 0x80 => { + // Note Off + if message.len() >= 3 { + let note = message[1]; + let mut tx = command_tx.lock().unwrap(); + let _ = tx.push(Command::SendMidiNoteOff(track_id, note)); + println!("MIDI [{}] Note Off: {}", device_name, note); + } + } + 0xB0 => { + // Control Change + if message.len() >= 3 { + let controller = message[1]; + let value = message[2]; + println!("MIDI [{}] CC: {} = {}", device_name, controller, value); + // TODO: Map to automation lanes in Phase 5 + } + } + 0xE0 => { + // Pitch Bend + if message.len() >= 3 { + let lsb = message[1] as u16; + let msb = message[2] as u16; + let value = (msb << 7) | lsb; + println!("MIDI [{}] Pitch Bend: {}", device_name, value); + // TODO: Map to pitch automation in Phase 5 + } + } + _ => { + // Other MIDI messages (aftertouch, program change, etc.) + // Ignore for now + } + } + } + + /// Set the currently active MIDI track + pub fn set_active_track(&self, track_id: Option) { + let mut active = self.active_track_id.lock().unwrap(); + *active = track_id; + + match track_id { + Some(id) => println!("MIDI Input: Routing to track {}", id), + None => println!("MIDI Input: No active track"), + } + } + + /// Get the number of connected devices + pub fn device_count(&self) -> usize { + self.connections.len() + } +} diff --git a/daw-backend/src/io/mod.rs b/daw-backend/src/io/mod.rs index 50e026a..e3542b4 100644 --- a/daw-backend/src/io/mod.rs +++ b/daw-backend/src/io/mod.rs @@ -1,7 +1,9 @@ pub mod audio_file; pub mod midi_file; +pub mod midi_input; pub mod wav_writer; pub use audio_file::{AudioFile, WaveformPeak}; pub use midi_file::load_midi_file; +pub use midi_input::MidiInputManager; pub use wav_writer::WavWriter; diff --git a/daw-backend/src/lib.rs b/daw-backend/src/lib.rs index 8259378..cddd71f 100644 --- a/daw-backend/src/lib.rs +++ b/daw-backend/src/lib.rs @@ -57,7 +57,7 @@ impl AudioSystem { let channels = default_output_config.channels() as u32; // Create queues - let (command_tx, command_rx) = rtrb::RingBuffer::new(256); + let (command_tx, command_rx) = rtrb::RingBuffer::new(512); // Larger buffer for MIDI + UI commands let (event_tx, event_rx) = rtrb::RingBuffer::new(256); let (query_tx, query_rx) = rtrb::RingBuffer::new(16); // Smaller buffer for synchronous queries let (query_response_tx, query_response_rx) = rtrb::RingBuffer::new(16); @@ -72,6 +72,21 @@ impl AudioSystem { engine.set_input_rx(input_rx); let controller = engine.get_controller(command_tx, query_tx, query_response_rx); + // Initialize MIDI input manager for external MIDI devices + // Create a separate command channel for MIDI input + let (midi_command_tx, midi_command_rx) = rtrb::RingBuffer::new(256); + match io::MidiInputManager::new(midi_command_tx) { + Ok(midi_manager) => { + println!("MIDI input initialized successfully"); + engine.set_midi_input_manager(midi_manager); + engine.set_midi_command_rx(midi_command_rx); + } + Err(e) => { + eprintln!("Warning: Failed to initialize MIDI input: {}", e); + eprintln!("External MIDI controllers will not be available"); + } + } + // Build output stream let output_config: cpal::StreamConfig = default_output_config.clone().into(); let mut output_buffer = vec![0.0f32; 16384]; diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f8812d3..4952019 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -58,6 +58,18 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "alsa" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2562ad8dcf0f789f65c6fdaad8a8a9708ed6b488e649da28c01656ad66b8b47" +dependencies = [ + "alsa-sys", + "bitflags 1.3.2", + "libc", + "nix 0.24.3", +] + [[package]] name = "alsa" version = "0.9.1" @@ -584,7 +596,7 @@ dependencies = [ "bitflags 2.8.0", "block", "cocoa-foundation", - "core-foundation", + "core-foundation 0.10.0", "core-graphics", "foreign-types", "libc", @@ -599,7 +611,7 @@ checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" dependencies = [ "bitflags 2.8.0", "block", - "core-foundation", + "core-foundation 0.10.0", "core-graphics-types", "libc", "objc", @@ -653,6 +665,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.0" @@ -676,7 +698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.8.0", - "core-foundation", + "core-foundation 0.10.0", "core-graphics-types", "foreign-types", "libc", @@ -689,7 +711,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.8.0", - "core-foundation", + "core-foundation 0.10.0", "libc", ] @@ -713,13 +735,33 @@ dependencies = [ "bindgen", ] +[[package]] +name = "coremidi" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7847ca018a67204508b77cb9e6de670125075f7464fff5f673023378fa34f5" +dependencies = [ + "core-foundation 0.9.4", + "core-foundation-sys", + "coremidi-sys", +] + +[[package]] +name = "coremidi-sys" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9504310988d938e49fff1b5f1e56e3dafe39bb1bae580c19660b58b83a191e" +dependencies = [ + "core-foundation-sys", +] + [[package]] name = "cpal" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" dependencies = [ - "alsa", + "alsa 0.9.1", "core-foundation-sys", "coreaudio-rs", "dasp_sample", @@ -1024,6 +1066,7 @@ dependencies = [ "dasp_rms", "dasp_sample", "dasp_signal", + "midir", "midly", "pathdiff", "petgraph 0.6.5", @@ -2496,6 +2539,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "midir" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a456444d83e7ead06ae6a5c0a215ed70282947ff3897fb45fcb052b757284731" +dependencies = [ + "alsa 0.7.1", + "bitflags 1.3.2", + "coremidi", + "js-sys", + "libc", + "wasm-bindgen", + "web-sys", + "windows 0.43.0", +] + [[package]] name = "midly" version = "0.5.3" @@ -2629,6 +2688,17 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "nix" version = "0.29.0" @@ -3691,7 +3761,7 @@ checksum = "6a24763657bff09769a8ccf12c8b8a50416fb035fe199263b4c5071e4e3f006f" dependencies = [ "ashpd", "block2", - "core-foundation", + "core-foundation 0.10.0", "core-foundation-sys", "glib-sys", "gobject-sys", @@ -4544,7 +4614,7 @@ checksum = "3731d04d4ac210cd5f344087733943b9bfb1a32654387dad4d1c70de21aee2c9" dependencies = [ "bitflags 2.8.0", "cocoa", - "core-foundation", + "core-foundation 0.10.0", "core-graphics", "crossbeam-channel", "dispatch", @@ -5763,6 +5833,21 @@ dependencies = [ "windows-version", ] +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows" version = "0.54.0" @@ -6324,7 +6409,7 @@ dependencies = [ "futures-core", "futures-util", "hex", - "nix", + "nix 0.29.0", "ordered-stream", "serde", "serde_repr", diff --git a/src-tauri/src/audio.rs b/src-tauri/src/audio.rs index ac3a841..51cf695 100644 --- a/src-tauri/src/audio.rs +++ b/src-tauri/src/audio.rs @@ -510,6 +510,20 @@ pub async fn audio_send_midi_note_off( } } +#[tauri::command] +pub async fn audio_set_active_midi_track( + state: tauri::State<'_, Arc>>, + track_id: Option, +) -> Result<(), String> { + let mut audio_state = state.lock().unwrap(); + if let Some(controller) = &mut audio_state.controller { + controller.set_active_midi_track(track_id); + Ok(()) + } else { + Err("Audio not initialized".to_string()) + } +} + #[tauri::command] pub async fn audio_load_midi_file( state: tauri::State<'_, Arc>>, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6445b59..29902ff 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -217,6 +217,7 @@ pub fn run() { audio::audio_update_midi_clip_notes, audio::audio_send_midi_note_on, audio::audio_send_midi_note_off, + audio::audio_set_active_midi_track, audio::audio_get_pool_file_info, audio::audio_get_pool_waveform, audio::graph_add_node, diff --git a/src/widgets.js b/src/widgets.js index 4903b18..ae2afeb 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -2938,6 +2938,18 @@ class TimelineWindowV2 extends Widget { if (track.object.type === 'midi' || track.object.type === 'audio') { setTimeout(() => this.context.reloadNodeEditor?.(), 50); } + + // Set active MIDI track for external MIDI input routing + if (track.object.type === 'midi') { + invoke('audio_set_active_midi_track', { trackId: track.object.audioTrackId }).catch(err => { + console.error('Failed to set active MIDI track:', err); + }); + } + } else { + // Non-audio track selected, clear active MIDI track + invoke('audio_set_active_midi_track', { trackId: null }).catch(err => { + console.error('Failed to clear active MIDI track:', err); + }); } // Update the stage UI to reflect selection changes