diff --git a/Cargo.lock b/Cargo.lock index 44ed95e2..53db48fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,18 @@ dependencies = [ "nix 0.23.2", ] +[[package]] +name = "alsa" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44" +dependencies = [ + "alsa-sys", + "bitflags", + "libc", + "nix 0.24.3", +] + [[package]] name = "alsa-sys" version = "0.3.1" @@ -856,6 +868,26 @@ 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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79a6deed0c97b2d40abbab77e4c97f81d71e162600423382c277dd640019116c" +dependencies = [ + "core-foundation-sys", +] + [[package]] name = "cosmic-text" version = "0.6.0" @@ -881,7 +913,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f342c1b63e185e9953584ff2199726bf53850d96610a310e3aca09e9405a2d0b" dependencies = [ - "alsa", + "alsa 0.6.0", "core-foundation-sys", "coreaudio-rs", "jni", @@ -2252,6 +2284,22 @@ dependencies = [ "nih_plug", ] +[[package]] +name = "midir" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a456444d83e7ead06ae6a5c0a215ed70282947ff3897fb45fcb052b757284731" +dependencies = [ + "alsa 0.7.0", + "bitflags", + "coremidi", + "js-sys", + "libc", + "wasm-bindgen", + "web-sys", + "windows 0.43.0", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2363,6 +2411,7 @@ dependencies = [ "libc", "log", "midi-consts", + "midir", "nih_plug_derive", "objc", "parking_lot 0.12.1", @@ -4564,6 +4613,21 @@ dependencies = [ "windows_x86_64_msvc 0.37.0", ] +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.1", +] + [[package]] name = "windows-sys" version = "0.42.0" diff --git a/Cargo.toml b/Cargo.toml index 99861066..88491e5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ assert_process_allocs = ["dep:assert_no_alloc"] # Enables an export target for standalone binaries through the # `nih_export_standalone()` function. Disabled by default as this requires # building additional dependencies for audio and MIDI handling. -standalone = ["dep:baseview", "dep:clap", "dep:cpal", "dep:jack", "dep:rtrb"] +standalone = ["dep:baseview", "dep:clap", "dep:cpal", "dep:jack", "dep:midir", "dep:rtrb"] # Enables the `nih_export_vst3!()` macro. Enabled by default. This feature # exists mostly for GPL-compliance reasons, since even if you don't use the VST3 # wrapper you might otherwise still include a couple (unused) symbols from the @@ -113,6 +113,7 @@ cpal = { version = "0.14.1", optional = true } # Current upstream JACK always links to libjack, even when using the default # dynamic loading feature jack = { git = "https://github.com/robbert-vdh/rust-jack.git", tag = "tmp-handle-library-failure", optional = true } +midir = { version = "0.9.1", optional = true } rtrb = { version = "0.2.2", optional = true } # Used for the `vst3` feature diff --git a/src/wrapper/standalone/backend/cpal.rs b/src/wrapper/standalone/backend/cpal.rs index 845ca404..97135ec6 100644 --- a/src/wrapper/standalone/backend/cpal.rs +++ b/src/wrapper/standalone/backend/cpal.rs @@ -6,6 +6,8 @@ use cpal::{ StreamConfig, }; use crossbeam::sync::{Parker, Unparker}; +use midir::{MidiInput, MidiInputPort, MidiOutput, MidiOutputPort}; +use parking_lot::Mutex; use rtrb::RingBuffer; use super::super::config::WrapperConfig; @@ -24,7 +26,9 @@ pub struct CpalMidir { input: Option, output: CpalDevice, - // TODO: MIDI + midi_input: Option>, + midi_output: Option>, +} /// All data needed for a CPAL input or output stream. struct CpalDevice { @@ -33,6 +37,20 @@ struct CpalDevice { pub sample_format: SampleFormat, } +/// All data needed to create a Midir input stream. +struct MidirInputDevice { + pub backend: MidiInput, + pub port: MidiInputPort, + // The name can be retrieved from the port, not sure why the connect function needs the name + // again +} + +/// All data needed to create a Midir output stream. +struct MidirOutputDevice { + pub backend: MidiOutput, + pub port: MidiOutputPort, +} + impl Backend

for CpalMidir { fn run( &mut self, @@ -311,12 +329,97 @@ impl CpalMidir { nih_warn!("Auxiliary outputs are not supported with this audio backend"); } + let midi_input = match &config.midi_input { + Some(midi_input_name) => { + // Midir lets us preemptively ignore MIDI messages we'll never use like active + // sensing and timing, but for maximum flexibility with NIH-plug's SysEx parsing + // types (which could technically be used to also parse those things) we won't do + // that. + let midi_backend = MidiInput::new(P::NAME) + .context("Could not initialize the MIDI input backend")?; + let available_ports = midi_backend.ports(); + + // In case there somehow is a MIDI port with an empty name, we'll still want to + // preserve the behavior of an empty argument resulting in a listing of options. + let found_port = if !midi_input_name.is_empty() { + // This API is a bit weird + available_ports + .iter() + .find(|port| midi_backend.port_name(port).as_deref() == Ok(midi_input_name)) + } else { + None + }; + + match found_port { + Some(port) => Some(Mutex::new(MidirInputDevice { + backend: midi_backend, + port: port.clone(), + })), + None => { + let mut message = format!( + "Unknown input MIDI device '{midi_input_name}'. Available devices are:" + ); + for port in available_ports { + match midi_backend.port_name(&port) { + Ok(device_name) => message.push_str(&format!("\n{device_name}")), + Err(err) => message.push_str(&format!("\nERROR: {err:?}")), + } + } + + anyhow::bail!(message); + } + } + } + None => None, + }; + + let midi_output = match &config.midi_output { + Some(midi_output_name) => { + let midi_backend = MidiOutput::new(P::NAME) + .context("Could not initialize the MIDI output backend")?; + let available_ports = midi_backend.ports(); + + let found_port = if !midi_output_name.is_empty() { + available_ports.iter().find(|port| { + midi_backend.port_name(port).as_deref() == Ok(midi_output_name) + }) + } else { + None + }; + + match found_port { + Some(port) => Some(Mutex::new(MidirOutputDevice { + backend: midi_backend, + port: port.clone(), + })), + None => { + let mut message = format!( + "Unknown output MIDI device '{midi_output_name}'. Available devices \ + are:" + ); + for port in available_ports { + match midi_backend.port_name(&port) { + Ok(device_name) => message.push_str(&format!("\n{device_name}")), + Err(err) => message.push_str(&format!("\nERROR: {err:?}")), + } + } + + anyhow::bail!(message); + } + } + } + None => None, + }; + Ok(CpalMidir { config, audio_io_layout, input, output, + + midi_input, + midi_output, }) }