diff --git a/Cargo.toml b/Cargo.toml index e873cdf..cfa2296 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ bitflags = "0.3" memalloc = "0.1.0" jack-sys = { version = "0.1.0", optional = true } libc = { version = "0.2.21", optional = true } +winrt = { version = "0.4.0", features = ["windows-devices", "windows-storage"], optional = true} [target.'cfg(target_os = "linux")'.dependencies] alsa = "0.2" diff --git a/README.md b/README.md index 3f9e44b..d393d10 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ Cross-platform, realtime MIDI processing in Rust. - [x] ALSA (Linux) - [x] WinMM (Windows) - [x] CoreMIDI (macOS, iOS (untested)) -- [x] Jack (Linux, macOS), use the `jack` feature +- [x] WinRT (Windows 8+), enable the `winrt` feature +- [x] Jack (Linux, macOS), enable the `jack` feature A higher-level API for parsing and assembling MIDI messages might be added in the future. diff --git a/appveyor.yml b/appveyor.yml index 8f30274..59282d5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,8 +5,17 @@ branches: environment: matrix: - TARGET: x86_64-pc-windows-msvc + FEATURES: " " - TARGET: i686-pc-windows-msvc + FEATURES: " " - TARGET: i686-pc-windows-gnu + FEATURES: " " + - TARGET: x86_64-pc-windows-msvc + FEATURES: "winrt" + - TARGET: i686-pc-windows-gnu + FEATURES: "winrt" + - TARGET: x86_64-pc-windows-gnu + FEATURES: "winrt" install: - ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-nightly-${env:TARGET}.exe" - rust-nightly-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust" @@ -15,7 +24,9 @@ install: - rustc -V - cargo -V -build: false +build_script: + - cargo build --verbose --features "%FEATURES%" test_script: - - cargo build --verbose \ No newline at end of file + - cargo test --features "%FEATURES%" + - cargo build --features "%FEATURES%" --example test_list_ports diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 75c17fb..762fe90 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -3,8 +3,11 @@ // TODO: improve feature selection (make sure that there is always exactly one implementation, or enable dynamic backend selection) // TODO: allow to disable build dependency on ALSA -#[cfg(target_os="windows")] mod winmm; -#[cfg(target_os="windows")] pub use self::winmm::*; +#[cfg(all(target_os="windows", not(feature = "winrt")))] mod winmm; +#[cfg(all(target_os="windows", not(feature = "winrt")))] pub use self::winmm::*; + +#[cfg(all(target_os="windows", feature = "winrt"))] mod winrt; +#[cfg(all(target_os="windows", feature = "winrt"))] pub use self::winrt::*; #[cfg(all(target_os="macos", not(feature = "jack")))] mod coremidi; #[cfg(all(target_os="macos", not(feature = "jack")))] pub use self::coremidi::*; diff --git a/src/backend/winrt/mod.rs b/src/backend/winrt/mod.rs new file mode 100644 index 0000000..a68eaac --- /dev/null +++ b/src/backend/winrt/mod.rs @@ -0,0 +1,223 @@ +extern crate winrt; + +use std::sync::{Arc, Mutex}; + +use self::winrt::{RuntimeContext, ComPtr, HString, RtAsyncOperation, RtDefaultConstructible, IMemoryBufferByteAccess}; +use self::winrt::windows::foundation::*; +use self::winrt::windows::devices::enumeration::*; +use self::winrt::windows::devices::midi::*; +use self::winrt::windows::storage::streams::*; + +use ::errors::*; +use ::Ignore; + +pub struct MidiInput { + rt: RuntimeContext, + selector: HString, + ignore_flags: Ignore +} + +impl MidiInput { + pub fn new(_client_name: &str) -> Result { + let rt = RuntimeContext::init(); + let device_selector = MidiInPort::get_device_selector().map_err(|_| InitError)?; + Ok(MidiInput { rt: rt, selector: device_selector, ignore_flags: Ignore::None }) + } + + pub fn ignore(&mut self, flags: Ignore) { + self.ignore_flags = flags; + } + + pub fn port_count(&self) -> usize { + let device_collection = DeviceInformation::find_all_async_aqs_filter(&self.selector.make_reference()).unwrap().blocking_get().expect("find_all_async failed"); + unsafe { device_collection.get_size().expect("get_size failed") as usize } + } + + pub fn port_name(&self, port_number: usize) -> Result { + let device_collection = DeviceInformation::find_all_async_aqs_filter(&self.selector.make_reference()).unwrap().blocking_get().expect("find_all_async failed"); + let device_name; + unsafe { + let device_info = device_collection.get_at(port_number as u32).map_err(|_| PortInfoError::PortNumberOutOfRange)?; + device_name = device_info.get_name().map_err(|_| PortInfoError::CannotRetrievePortName)?; + } + Ok(device_name.to_string()) + } + + fn handle_input(args: &MidiMessageReceivedEventArgs, handler_data: &mut HandlerData) { + let ignore = handler_data.ignore_flags; + let data = &mut handler_data.user_data.as_mut().unwrap(); + let timestamp; + let byte_access; + let message_bytes; + unsafe { + let message = args.get_message().expect("get_message failed"); + timestamp = message.get_timestamp().expect("get_timestamp failed").Duration as u64 / 10; + let buffer = message.get_raw_data().expect("get_raw_data failed"); + let membuffer = Buffer::create_memory_buffer_over_ibuffer(&buffer).expect("create_memory_buffer_over_ibuffer failed"); + byte_access = membuffer.create_reference().expect("create_reference failed").query_interface::().unwrap(); + message_bytes = byte_access.get_buffer(); + } + + // The first byte in the message is the status + let status = message_bytes[0]; + + if !(status == 0xF0 && ignore.contains(Ignore::Sysex) || + status == 0xF1 && ignore.contains(Ignore::Time) || + status == 0xF8 && ignore.contains(Ignore::Time) || + status == 0xFE && ignore.contains(Ignore::ActiveSense)) + { + (handler_data.callback)(timestamp, message_bytes, data); + } + } + + pub fn connect( + self, port_number: usize, _port_name: &str, callback: F, data: T + ) -> Result, ConnectError> + where F: FnMut(u64, &[u8], &mut T) + Send + 'static { + + let device_collection = DeviceInformation::find_all_async_aqs_filter(&self.selector.make_reference()).unwrap().blocking_get().expect("find_all_async failed"); + unsafe { + let device_info = match device_collection.get_at(port_number as u32) { + Ok(info) => info, + Err(_) => return Err(ConnectError::new(ConnectErrorKind::PortNumberOutOfRange, self)) + }; + let device_id = match device_info.get_id() { + Ok(id) => id, + Err(_) => return Err(ConnectError::other("get_id failed", self)) + }; + let in_port = match MidiInPort::from_id_async(&device_id.make_reference()) { + Ok(port_async) => match port_async.blocking_get() { + Ok(port) => port, + Err(_) => return Err(ConnectError::other("getting result from MidiInPort::from_id_async failed", self)) + } + Err(_) => return Err(ConnectError::other("MidiInPort::from_id_async failed", self)) + }; + + let handler_data = Arc::new(Mutex::new(HandlerData { + ignore_flags: self.ignore_flags, + callback: Box::new(callback), + user_data: Some(data) + })); + let handler_data2 = handler_data.clone(); + + let handler = TypedEventHandler::new(move |_sender, args: *mut MidiMessageReceivedEventArgs| { + MidiInput::handle_input(&*args, &mut *handler_data2.lock().unwrap()); + Ok(()) + }); + let event_token = in_port.add_message_received(&handler).expect("add_message_received failed"); + + Ok(MidiInputConnection { rt: self.rt, port: in_port, event_token: event_token, handler_data: handler_data }) + } + } +} + +pub struct MidiInputConnection { + rt: RuntimeContext, + port: ComPtr, + event_token: EventRegistrationToken, + // TODO: get rid of Arc & Mutex? + // synchronization is required because the borrow checker does not + // know that the callback we're in here is never called concurrently + // (always in sequence) + handler_data: Arc>> +} + +impl MidiInputConnection { + pub fn close(self) -> (MidiInput, T) { + let _ = unsafe { self.port.remove_message_received(self.event_token) }; + let _ = unsafe { self.port.query_interface::().unwrap().close() }; + let device_selector = MidiInPort::get_device_selector().expect("get_device_selector failed"); // probably won't ever fail here, because it worked previously + let mut handler_data_locked = self.handler_data.lock().unwrap(); + (MidiInput { + rt: self.rt, + selector: device_selector, + ignore_flags: handler_data_locked.ignore_flags + }, handler_data_locked.user_data.take().unwrap()) + } +} + +/// This is all the data that is stored on the heap as long as a connection +/// is opened and passed to the callback handler. +/// +/// It is important that `user_data` is the last field to not influence +/// offsets after monomorphization. +struct HandlerData { + ignore_flags: Ignore, + callback: Box, + user_data: Option +} + +pub struct MidiOutput { + rt: RuntimeContext, + selector: HString // TODO: change to FastHString? +} + +impl MidiOutput { + pub fn new(_client_name: &str) -> Result { + let rt = RuntimeContext::init(); + let device_selector = MidiOutPort::get_device_selector().map_err(|_| InitError)?; + Ok(MidiOutput { rt: rt, selector: device_selector }) + } + + pub fn port_count(&self) -> usize { + let device_collection = DeviceInformation::find_all_async_aqs_filter(&self.selector.make_reference()).unwrap().blocking_get().expect("find_all_async failed"); + unsafe { device_collection.get_size().expect("get_size failed") as usize } + } + + pub fn port_name(&self, port_number: usize) -> Result { + let device_collection = DeviceInformation::find_all_async_aqs_filter(&self.selector.make_reference()).unwrap().blocking_get().expect("find_all_async failed"); + let device_name; + unsafe { + let device_info = device_collection.get_at(port_number as u32).map_err(|_| PortInfoError::PortNumberOutOfRange)?; + device_name = device_info.get_name().map_err(|_| PortInfoError::CannotRetrievePortName)?; + } + Ok(device_name.to_string()) + } + + pub fn connect(self, port_number: usize, _port_name: &str) -> Result> { + let device_collection = DeviceInformation::find_all_async_aqs_filter(&self.selector.make_reference()).unwrap().blocking_get().expect("find_all_async failed"); + unsafe { + let device_info = match device_collection.get_at(port_number as u32) { + Ok(info) => info, + Err(_) => return Err(ConnectError::new(ConnectErrorKind::PortNumberOutOfRange, self)) + }; + let device_id = match device_info.get_id() { + Ok(id) => id, + Err(_) => return Err(ConnectError::other("get_id failed", self)) + }; + let out_port = match MidiOutPort::from_id_async(&device_id.make_reference()) { + Ok(port_async) => match port_async.blocking_get() { + Ok(port) => port, + Err(_) => return Err(ConnectError::other("getting result from MidiOutPort::from_id_async failed", self)) + } + Err(_) => return Err(ConnectError::other("MidiOutPort::from_id_async failed", self)) + }; + Ok(MidiOutputConnection { rt: self.rt, port: out_port }) + } + } +} + +pub struct MidiOutputConnection { + rt: RuntimeContext, + port: ComPtr +} + +unsafe impl Send for MidiOutputConnection {} + +impl MidiOutputConnection { + pub fn close(self) -> MidiOutput { + let _ = unsafe { self.port.query_interface::().unwrap().close() }; + let device_selector = MidiOutPort::get_device_selector().expect("get_device_selector failed"); // probably won't ever fail here, because it worked previously + MidiOutput { rt: self.rt, selector: device_selector } + } + + pub fn send(&mut self, message: &[u8]) -> Result<(), SendError> { + let data_writer: ComPtr = DataWriter::new(); + unsafe { + data_writer.write_bytes(message).map_err(|_| SendError::Other("write_bytes failed"))?; + let buffer = data_writer.detach_buffer().map_err(|_| SendError::Other("detach_buffer failed"))?; + self.port.send_buffer(&buffer).map_err(|_| SendError::Other("send_buffer failed"))?; + } + Ok(()) + } +} \ No newline at end of file