Add MIDI input
This commit is contained in:
parent
f6a91abccd
commit
06314dbf57
|
|
@ -17,6 +17,18 @@ version = "0.2.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
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]]
|
[[package]]
|
||||||
name = "alsa"
|
name = "alsa"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
|
|
@ -187,6 +199,16 @@ dependencies = [
|
||||||
"static_assertions",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
|
|
@ -213,13 +235,33 @@ dependencies = [
|
||||||
"bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "cpal"
|
name = "cpal"
|
||||||
version = "0.15.3"
|
version = "0.15.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
|
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alsa",
|
"alsa 0.9.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"coreaudio-rs",
|
"coreaudio-rs",
|
||||||
"dasp_sample",
|
"dasp_sample",
|
||||||
|
|
@ -233,7 +275,7 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"windows",
|
"windows 0.54.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -415,6 +457,7 @@ dependencies = [
|
||||||
"dasp_rms",
|
"dasp_rms",
|
||||||
"dasp_sample",
|
"dasp_sample",
|
||||||
"dasp_signal",
|
"dasp_signal",
|
||||||
|
"midir",
|
||||||
"midly",
|
"midly",
|
||||||
"pathdiff",
|
"pathdiff",
|
||||||
"petgraph 0.6.5",
|
"petgraph 0.6.5",
|
||||||
|
|
@ -682,6 +725,22 @@ version = "2.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
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]]
|
[[package]]
|
||||||
name = "midly"
|
name = "midly"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|
@ -738,6 +797,17 @@ dependencies = [
|
||||||
"jni-sys",
|
"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]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
|
|
@ -1615,6 +1685,21 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
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]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.54.0"
|
version = "0.54.0"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ cpal = "0.15"
|
||||||
symphonia = { version = "0.5", features = ["all"] }
|
symphonia = { version = "0.5", features = ["all"] }
|
||||||
rtrb = "0.3"
|
rtrb = "0.3"
|
||||||
midly = "0.5"
|
midly = "0.5"
|
||||||
|
midir = "0.9"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
ratatui = "0.26"
|
ratatui = "0.26"
|
||||||
crossterm = "0.27"
|
crossterm = "0.27"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use crate::audio::project::Project;
|
||||||
use crate::audio::recording::{MidiRecordingState, RecordingState};
|
use crate::audio::recording::{MidiRecordingState, RecordingState};
|
||||||
use crate::audio::track::{Track, TrackId, TrackNode};
|
use crate::audio::track::{Track, TrackId, TrackNode};
|
||||||
use crate::command::{AudioEvent, Command, Query, QueryResponse};
|
use crate::command::{AudioEvent, Command, Query, QueryResponse};
|
||||||
|
use crate::io::MidiInputManager;
|
||||||
use petgraph::stable_graph::NodeIndex;
|
use petgraph::stable_graph::NodeIndex;
|
||||||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -23,6 +24,7 @@ pub struct Engine {
|
||||||
|
|
||||||
// Lock-free communication
|
// Lock-free communication
|
||||||
command_rx: rtrb::Consumer<Command>,
|
command_rx: rtrb::Consumer<Command>,
|
||||||
|
midi_command_rx: Option<rtrb::Consumer<Command>>,
|
||||||
event_tx: rtrb::Producer<AudioEvent>,
|
event_tx: rtrb::Producer<AudioEvent>,
|
||||||
query_rx: rtrb::Consumer<Query>,
|
query_rx: rtrb::Consumer<Query>,
|
||||||
query_response_tx: rtrb::Producer<QueryResponse>,
|
query_response_tx: rtrb::Producer<QueryResponse>,
|
||||||
|
|
@ -50,6 +52,9 @@ pub struct Engine {
|
||||||
|
|
||||||
// MIDI recording state
|
// MIDI recording state
|
||||||
midi_recording_state: Option<MidiRecordingState>,
|
midi_recording_state: Option<MidiRecordingState>,
|
||||||
|
|
||||||
|
// MIDI input manager for external MIDI devices
|
||||||
|
midi_input_manager: Option<MidiInputManager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
|
|
@ -76,6 +81,7 @@ impl Engine {
|
||||||
playing: false,
|
playing: false,
|
||||||
channels,
|
channels,
|
||||||
command_rx,
|
command_rx,
|
||||||
|
midi_command_rx: None,
|
||||||
event_tx,
|
event_tx,
|
||||||
query_rx,
|
query_rx,
|
||||||
query_response_tx,
|
query_response_tx,
|
||||||
|
|
@ -89,6 +95,7 @@ impl Engine {
|
||||||
input_rx: None,
|
input_rx: None,
|
||||||
recording_progress_counter: 0,
|
recording_progress_counter: 0,
|
||||||
midi_recording_state: None,
|
midi_recording_state: None,
|
||||||
|
midi_input_manager: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,6 +104,16 @@ impl Engine {
|
||||||
self.input_rx = Some(input_rx);
|
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<Command>) {
|
||||||
|
self.midi_command_rx = Some(midi_command_rx);
|
||||||
|
}
|
||||||
|
|
||||||
/// Add an audio track to the engine
|
/// Add an audio track to the engine
|
||||||
pub fn add_track(&mut self, track: Track) -> TrackId {
|
pub fn add_track(&mut self, track: Track) -> TrackId {
|
||||||
// For backwards compatibility, we'll extract the track data and add it to the project
|
// 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);
|
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
|
// Process all pending queries
|
||||||
while let Ok(query) = self.query_rx.pop() {
|
while let Ok(query) = self.query_rx.pop() {
|
||||||
self.handle_query(query);
|
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
|
// Node graph commands
|
||||||
Command::GraphAddNode(track_id, node_type, x, y) => {
|
Command::GraphAddNode(track_id, node_type, x, y) => {
|
||||||
eprintln!("[DEBUG] GraphAddNode received: track_id={}, node_type={}, x={}, y={}", 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));
|
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<TrackId>) {
|
||||||
|
let _ = self.command_tx.push(Command::SetActiveMidiTrack(track_id));
|
||||||
|
}
|
||||||
|
|
||||||
// Node graph operations
|
// Node graph operations
|
||||||
|
|
||||||
/// Add a node to a track's instrument graph
|
/// Add a node to a track's instrument graph
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,8 @@ pub enum Command {
|
||||||
SendMidiNoteOn(TrackId, u8, u8),
|
SendMidiNoteOn(TrackId, u8, u8),
|
||||||
/// Send a live MIDI note off event to a track's instrument (track_id, note)
|
/// Send a live MIDI note off event to a track's instrument (track_id, note)
|
||||||
SendMidiNoteOff(TrackId, u8),
|
SendMidiNoteOff(TrackId, u8),
|
||||||
|
/// Set the active MIDI track for external MIDI input routing (track_id or None)
|
||||||
|
SetActiveMidiTrack(Option<TrackId>),
|
||||||
|
|
||||||
// Node graph commands
|
// Node graph commands
|
||||||
/// Add a node to a track's instrument graph (track_id, node_type, position_x, position_y)
|
/// Add a node to a track's instrument graph (track_id, node_type, position_x, position_y)
|
||||||
|
|
|
||||||
|
|
@ -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<ActiveMidiConnection>,
|
||||||
|
active_track_id: Arc<Mutex<Option<TrackId>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Command>) -> Result<Self, String> {
|
||||||
|
let active_track_id = Arc::new(Mutex::new(None));
|
||||||
|
let mut connections = Vec::new();
|
||||||
|
|
||||||
|
// Wrap command producer in Arc<Mutex> 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<rtrb::Producer<Command>>,
|
||||||
|
active_track_id: &Arc<Mutex<Option<TrackId>>>,
|
||||||
|
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<TrackId>) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
pub mod audio_file;
|
pub mod audio_file;
|
||||||
pub mod midi_file;
|
pub mod midi_file;
|
||||||
|
pub mod midi_input;
|
||||||
pub mod wav_writer;
|
pub mod wav_writer;
|
||||||
|
|
||||||
pub use audio_file::{AudioFile, WaveformPeak};
|
pub use audio_file::{AudioFile, WaveformPeak};
|
||||||
pub use midi_file::load_midi_file;
|
pub use midi_file::load_midi_file;
|
||||||
|
pub use midi_input::MidiInputManager;
|
||||||
pub use wav_writer::WavWriter;
|
pub use wav_writer::WavWriter;
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ impl AudioSystem {
|
||||||
let channels = default_output_config.channels() as u32;
|
let channels = default_output_config.channels() as u32;
|
||||||
|
|
||||||
// Create queues
|
// 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 (event_tx, event_rx) = rtrb::RingBuffer::new(256);
|
||||||
let (query_tx, query_rx) = rtrb::RingBuffer::new(16); // Smaller buffer for synchronous queries
|
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);
|
let (query_response_tx, query_response_rx) = rtrb::RingBuffer::new(16);
|
||||||
|
|
@ -72,6 +72,21 @@ impl AudioSystem {
|
||||||
engine.set_input_rx(input_rx);
|
engine.set_input_rx(input_rx);
|
||||||
let controller = engine.get_controller(command_tx, query_tx, query_response_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
|
// Build output stream
|
||||||
let output_config: cpal::StreamConfig = default_output_config.clone().into();
|
let output_config: cpal::StreamConfig = default_output_config.clone().into();
|
||||||
let mut output_buffer = vec![0.0f32; 16384];
|
let mut output_buffer = vec![0.0f32; 16384];
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,18 @@ version = "0.2.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
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]]
|
[[package]]
|
||||||
name = "alsa"
|
name = "alsa"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
|
|
@ -584,7 +596,7 @@ dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
"block",
|
"block",
|
||||||
"cocoa-foundation",
|
"cocoa-foundation",
|
||||||
"core-foundation",
|
"core-foundation 0.10.0",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
"foreign-types",
|
"foreign-types",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -599,7 +611,7 @@ checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
"block",
|
"block",
|
||||||
"core-foundation",
|
"core-foundation 0.10.0",
|
||||||
"core-graphics-types",
|
"core-graphics-types",
|
||||||
"libc",
|
"libc",
|
||||||
"objc",
|
"objc",
|
||||||
|
|
@ -653,6 +665,16 @@ dependencies = [
|
||||||
"version_check",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
|
@ -676,7 +698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.0",
|
||||||
"core-graphics-types",
|
"core-graphics-types",
|
||||||
"foreign-types",
|
"foreign-types",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -689,7 +711,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.0",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -713,13 +735,33 @@ dependencies = [
|
||||||
"bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "cpal"
|
name = "cpal"
|
||||||
version = "0.15.3"
|
version = "0.15.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
|
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alsa",
|
"alsa 0.9.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"coreaudio-rs",
|
"coreaudio-rs",
|
||||||
"dasp_sample",
|
"dasp_sample",
|
||||||
|
|
@ -1024,6 +1066,7 @@ dependencies = [
|
||||||
"dasp_rms",
|
"dasp_rms",
|
||||||
"dasp_sample",
|
"dasp_sample",
|
||||||
"dasp_signal",
|
"dasp_signal",
|
||||||
|
"midir",
|
||||||
"midly",
|
"midly",
|
||||||
"pathdiff",
|
"pathdiff",
|
||||||
"petgraph 0.6.5",
|
"petgraph 0.6.5",
|
||||||
|
|
@ -2496,6 +2539,22 @@ dependencies = [
|
||||||
"autocfg",
|
"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]]
|
[[package]]
|
||||||
name = "midly"
|
name = "midly"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|
@ -2629,6 +2688,17 @@ version = "1.0.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
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]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.29.0"
|
version = "0.29.0"
|
||||||
|
|
@ -3691,7 +3761,7 @@ checksum = "6a24763657bff09769a8ccf12c8b8a50416fb035fe199263b4c5071e4e3f006f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ashpd",
|
"ashpd",
|
||||||
"block2",
|
"block2",
|
||||||
"core-foundation",
|
"core-foundation 0.10.0",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"gobject-sys",
|
"gobject-sys",
|
||||||
|
|
@ -4544,7 +4614,7 @@ checksum = "3731d04d4ac210cd5f344087733943b9bfb1a32654387dad4d1c70de21aee2c9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"core-foundation",
|
"core-foundation 0.10.0",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dispatch",
|
"dispatch",
|
||||||
|
|
@ -5763,6 +5833,21 @@ dependencies = [
|
||||||
"windows-version",
|
"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]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.54.0"
|
version = "0.54.0"
|
||||||
|
|
@ -6324,7 +6409,7 @@ dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"nix",
|
"nix 0.29.0",
|
||||||
"ordered-stream",
|
"ordered-stream",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
|
|
|
||||||
|
|
@ -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<Mutex<AudioState>>>,
|
||||||
|
track_id: Option<u32>,
|
||||||
|
) -> 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]
|
#[tauri::command]
|
||||||
pub async fn audio_load_midi_file(
|
pub async fn audio_load_midi_file(
|
||||||
state: tauri::State<'_, Arc<Mutex<AudioState>>>,
|
state: tauri::State<'_, Arc<Mutex<AudioState>>>,
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,7 @@ pub fn run() {
|
||||||
audio::audio_update_midi_clip_notes,
|
audio::audio_update_midi_clip_notes,
|
||||||
audio::audio_send_midi_note_on,
|
audio::audio_send_midi_note_on,
|
||||||
audio::audio_send_midi_note_off,
|
audio::audio_send_midi_note_off,
|
||||||
|
audio::audio_set_active_midi_track,
|
||||||
audio::audio_get_pool_file_info,
|
audio::audio_get_pool_file_info,
|
||||||
audio::audio_get_pool_waveform,
|
audio::audio_get_pool_waveform,
|
||||||
audio::graph_add_node,
|
audio::graph_add_node,
|
||||||
|
|
|
||||||
|
|
@ -2938,6 +2938,18 @@ class TimelineWindowV2 extends Widget {
|
||||||
if (track.object.type === 'midi' || track.object.type === 'audio') {
|
if (track.object.type === 'midi' || track.object.type === 'audio') {
|
||||||
setTimeout(() => this.context.reloadNodeEditor?.(), 50);
|
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
|
// Update the stage UI to reflect selection changes
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue