diff options
Diffstat (limited to 'dom/midi/midir_impl')
-rw-r--r-- | dom/midi/midir_impl/Cargo.toml | 14 | ||||
-rw-r--r-- | dom/midi/midir_impl/cbindgen.toml | 18 | ||||
-rw-r--r-- | dom/midi/midir_impl/moz.build | 17 | ||||
-rw-r--r-- | dom/midi/midir_impl/src/lib.rs | 418 |
4 files changed, 467 insertions, 0 deletions
diff --git a/dom/midi/midir_impl/Cargo.toml b/dom/midi/midir_impl/Cargo.toml new file mode 100644 index 0000000000..1e6d4030bf --- /dev/null +++ b/dom/midi/midir_impl/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "midir_impl" +version = "0.1.0" +authors = ["Gabriele Svelto"] +edition = "2018" +license = "MPL-2.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +midir = "0.7.0" +nsstring = { path = "../../../xpcom/rust/nsstring/" } +uuid = { version = "1.0", features = ["v4"] } +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } diff --git a/dom/midi/midir_impl/cbindgen.toml b/dom/midi/midir_impl/cbindgen.toml new file mode 100644 index 0000000000..1b80a85e45 --- /dev/null +++ b/dom/midi/midir_impl/cbindgen.toml @@ -0,0 +1,18 @@ +header = """/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */""" +autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */ +""" +include_version = true +braces = "SameLine" +line_length = 100 +tab_width = 2 +language = "C++" +include_guard = "midir_impl_ffi_generated_h" +includes = ["nsStringFwd.h", "nsTArrayForwardDeclare.h"] + +[defines] +"target_os = windows" = "XP_WIN" + +[export.rename] +"ThinVec" = "nsTArray" diff --git a/dom/midi/midir_impl/moz.build b/dom/midi/midir_impl/moz.build new file mode 100644 index 0000000000..f079022906 --- /dev/null +++ b/dom/midi/midir_impl/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +if CONFIG["COMPILE_ENVIRONMENT"]: + # This tells mach to run cbindgen and that this header-file should be created + CbindgenHeader( + "midir_impl_ffi_generated.h", + inputs=["/dom/midi/midir_impl"], + ) + + # This tells mach to copy that generated file to obj/dist/includes/mozilla/dom/midi + EXPORTS.mozilla.dom.midi += [ + "!midir_impl_ffi_generated.h", + ] diff --git a/dom/midi/midir_impl/src/lib.rs b/dom/midi/midir_impl/src/lib.rs new file mode 100644 index 0000000000..de47fbeb11 --- /dev/null +++ b/dom/midi/midir_impl/src/lib.rs @@ -0,0 +1,418 @@ +extern crate thin_vec; + +use midir::{ + InitError, MidiInput, MidiInputConnection, MidiInputPort, MidiOutput, MidiOutputConnection, + MidiOutputPort, +}; +use nsstring::{nsAString, nsString}; +use std::boxed::Box; +use std::ptr; +use thin_vec::ThinVec; +use uuid::Uuid; + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +extern crate midir; + +#[cfg(target_os = "windows")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct GeckoTimeStamp { + gtc: u64, + qpc: u64, + + is_null: u8, + has_qpc: u8, +} + +#[cfg(not(target_os = "windows"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct GeckoTimeStamp { + value: u64, +} + +enum MidiConnection { + Input(MidiInputConnection<CallbackData>), + Output(MidiOutputConnection), +} + +struct MidiConnectionWrapper { + id: String, + connection: MidiConnection, +} + +enum MidiPort { + Input(MidiInputPort), + Output(MidiOutputPort), +} + +struct MidiPortWrapper { + id: String, + name: String, + port: MidiPort, + open_count: u32, +} + +impl MidiPortWrapper { + fn input(self: &MidiPortWrapper) -> bool { + match self.port { + MidiPort::Input(_) => true, + MidiPort::Output(_) => false, + } + } +} + +pub struct MidirWrapper { + ports: Vec<MidiPortWrapper>, + connections: Vec<MidiConnectionWrapper>, +} + +struct CallbackData { + nsid: nsString, + open_timestamp: GeckoTimeStamp, +} + +type AddCallback = unsafe extern "C" fn(id: &nsString, name: &nsString, input: bool); +type RemoveCallback = AddCallback; + +impl MidirWrapper { + fn refresh( + self: &mut MidirWrapper, + add_callback: AddCallback, + remove_callback: Option<RemoveCallback>, + ) { + if let Ok(ports) = collect_ports() { + if let Some(remove_callback) = remove_callback { + self.remove_missing_ports(&ports, remove_callback); + } + + self.add_new_ports(ports, add_callback); + } + } + + fn remove_missing_ports( + self: &mut MidirWrapper, + ports: &Vec<MidiPortWrapper>, + remove_callback: RemoveCallback, + ) { + let old_ports = &mut self.ports; + let mut i = 0; + while i < old_ports.len() { + if !ports + .iter() + .any(|p| p.name == old_ports[i].name && p.input() == old_ports[i].input()) + { + let port = old_ports.remove(i); + let id = nsString::from(&port.id); + let name = nsString::from(&port.name); + unsafe { remove_callback(&id, &name, port.input()) }; + } else { + i += 1; + } + } + } + + fn add_new_ports( + self: &mut MidirWrapper, + ports: Vec<MidiPortWrapper>, + add_callback: AddCallback, + ) { + for port in ports { + if !self.is_port_present(&port) && !Self::is_microsoft_synth_output(&port) { + let id = nsString::from(&port.id); + let name = nsString::from(&port.name); + unsafe { add_callback(&id, &name, port.input()) }; + self.ports.push(port); + } + } + } + + fn is_port_present(self: &MidirWrapper, port: &MidiPortWrapper) -> bool { + self.ports + .iter() + .any(|p| p.name == port.name && p.input() == port.input()) + } + + // We explicitly disable Microsoft's soft synthesizer, see bug 1798097 + fn is_microsoft_synth_output(port: &MidiPortWrapper) -> bool { + (port.input() == false) && (port.name == "Microsoft GS Wavetable Synth") + } + + fn open_port( + self: &mut MidirWrapper, + nsid: &nsString, + timestamp: GeckoTimeStamp, + callback: unsafe extern "C" fn( + id: &nsString, + data: *const u8, + length: usize, + timestamp: &GeckoTimeStamp, + micros: u64, + ), + ) -> Result<(), ()> { + let id = nsid.to_string(); + let connections = &mut self.connections; + let port = self.ports.iter_mut().find(|e| e.id.eq(&id)); + if let Some(port) = port { + if port.open_count == 0 { + let connection = match &port.port { + MidiPort::Input(port) => { + let input = MidiInput::new("WebMIDI input").map_err(|_err| ())?; + let data = CallbackData { + nsid: nsid.clone(), + open_timestamp: timestamp, + }; + let connection = input + .connect( + port, + "Input connection", + move |stamp, message, data| unsafe { + callback( + &data.nsid, + message.as_ptr(), + message.len(), + &data.open_timestamp, + stamp, + ); + }, + data, + ) + .map_err(|_err| ())?; + MidiConnectionWrapper { + id: id.clone(), + connection: MidiConnection::Input(connection), + } + } + MidiPort::Output(port) => { + let output = MidiOutput::new("WebMIDI output").map_err(|_err| ())?; + let connection = output + .connect(port, "Output connection") + .map_err(|_err| ())?; + MidiConnectionWrapper { + connection: MidiConnection::Output(connection), + id: id.clone(), + } + } + }; + + connections.push(connection); + } + + port.open_count += 1; + return Ok(()); + } + + Err(()) + } + + fn close_port(self: &mut MidirWrapper, id: &str) { + let port = self.ports.iter_mut().find(|e| e.id.eq(&id)).unwrap(); + port.open_count -= 1; + + if port.open_count > 0 { + return; + } + + let connections = &mut self.connections; + let index = connections.iter().position(|e| e.id.eq(id)).unwrap(); + let connection_wrapper = connections.remove(index); + + match connection_wrapper.connection { + MidiConnection::Input(connection) => { + connection.close(); + } + MidiConnection::Output(connection) => { + connection.close(); + } + } + } + + fn send(self: &mut MidirWrapper, id: &str, data: &[u8]) -> Result<(), ()> { + let connections = &mut self.connections; + let index = connections.iter().position(|e| e.id.eq(id)).ok_or(())?; + let connection_wrapper = connections.get_mut(index).unwrap(); + + match &mut connection_wrapper.connection { + MidiConnection::Output(connection) => { + connection.send(data).map_err(|_err| ())?; + } + _ => { + panic!("Sending on an input port!"); + } + } + + Ok(()) + } +} + +fn collect_ports() -> Result<Vec<MidiPortWrapper>, InitError> { + let input = MidiInput::new("WebMIDI input")?; + let output = MidiOutput::new("WebMIDI output")?; + let mut ports = Vec::<MidiPortWrapper>::new(); + collect_input_ports(&input, &mut ports); + collect_output_ports(&output, &mut ports); + Ok(ports) +} + +impl MidirWrapper { + fn new() -> Result<MidirWrapper, InitError> { + let ports = Vec::new(); + let connections: Vec<MidiConnectionWrapper> = Vec::new(); + Ok(MidirWrapper { ports, connections }) + } +} + +/// Create the C++ wrapper that will be used to talk with midir. +/// +/// This function will be exposed to C++ +/// +/// # Safety +/// +/// This function deliberately leaks the wrapper because ownership is +/// transfered to the C++ code. Use [midir_impl_shutdown()] to free it. +#[no_mangle] +pub unsafe extern "C" fn midir_impl_init(callback: AddCallback) -> *mut MidirWrapper { + if let Ok(mut midir_impl) = MidirWrapper::new() { + midir_impl.refresh(callback, None); + + // Gecko invokes this initialization on a separate thread from all the + // other operations, so make it clear to Rust this needs to be Send. + fn assert_send<T: Send>(_: &T) {} + assert_send(&midir_impl); + + let midir_box = Box::new(midir_impl); + // Leak the object as it will be owned by the C++ code from now on + Box::leak(midir_box) as *mut _ + } else { + ptr::null_mut() + } +} + +/// Refresh the list of ports. +/// +/// This function will be exposed to C++ +/// +/// # Safety +/// +/// `wrapper` must be the pointer returned by [midir_impl_init()]. +#[no_mangle] +pub unsafe extern "C" fn midir_impl_refresh( + wrapper: *mut MidirWrapper, + add_callback: AddCallback, + remove_callback: RemoveCallback, +) { + (*wrapper).refresh(add_callback, Some(remove_callback)) +} + +/// Shutdown midir and free the C++ wrapper. +/// +/// This function will be exposed to C++ +/// +/// # Safety +/// +/// `wrapper` must be the pointer returned by [midir_impl_init()]. After this +/// has been called the wrapper object will be destoyed and cannot be accessed +/// anymore. +#[no_mangle] +pub unsafe extern "C" fn midir_impl_shutdown(wrapper: *mut MidirWrapper) { + // The MidirImpl object will be automatically destroyed when the contents + // of this box are automatically dropped at the end of the function + let _midir_box = Box::from_raw(wrapper); +} + +/// Open a MIDI port. +/// +/// This function will be exposed to C++ +/// +/// # Safety +/// +/// `wrapper` must be the pointer returned by [midir_impl_init()]. +#[no_mangle] +pub unsafe extern "C" fn midir_impl_open_port( + wrapper: *mut MidirWrapper, + nsid: *mut nsString, + timestamp: *mut GeckoTimeStamp, + callback: unsafe extern "C" fn( + id: &nsString, + data: *const u8, + length: usize, + timestamp: &GeckoTimeStamp, + micros: u64, + ), +) -> bool { + (*wrapper) + .open_port(nsid.as_ref().unwrap(), *timestamp, callback) + .is_ok() +} + +/// Close a MIDI port. +/// +/// This function will be exposed to C++ +/// +/// # Safety +/// +/// `wrapper` must be the pointer returned by [midir_impl_init()]. +#[no_mangle] +pub unsafe extern "C" fn midir_impl_close_port(wrapper: *mut MidirWrapper, id: *mut nsString) { + (*wrapper).close_port(&(*id).to_string()); +} + +/// Send a message over a MIDI output port. +/// +/// This function will be exposed to C++ +/// +/// # Safety +/// +/// `wrapper` must be the pointer returned by [midir_impl_init()]. +#[no_mangle] +pub unsafe extern "C" fn midir_impl_send( + wrapper: *mut MidirWrapper, + id: *const nsAString, + data: *const ThinVec<u8>, +) -> bool { + (*wrapper) + .send(&(*id).to_string(), (*data).as_slice()) + .is_ok() +} + +fn collect_input_ports(input: &MidiInput, wrappers: &mut Vec<MidiPortWrapper>) { + let ports = input.ports(); + for port in ports { + let id = Uuid::new_v4() + .as_hyphenated() + .encode_lower(&mut Uuid::encode_buffer()) + .to_owned(); + let name = input + .port_name(&port) + .unwrap_or_else(|_| "unknown input port".to_string()); + let port = MidiPortWrapper { + id, + name, + port: MidiPort::Input(port), + open_count: 0, + }; + wrappers.push(port); + } +} + +fn collect_output_ports(output: &MidiOutput, wrappers: &mut Vec<MidiPortWrapper>) { + let ports = output.ports(); + for port in ports { + let id = Uuid::new_v4() + .as_hyphenated() + .encode_lower(&mut Uuid::encode_buffer()) + .to_owned(); + let name = output + .port_name(&port) + .unwrap_or_else(|_| "unknown input port".to_string()); + let port = MidiPortWrapper { + id, + name, + port: MidiPort::Output(port), + open_count: 0, + }; + wrappers.push(port); + } +} |