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), 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, connections: Vec, } 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, ) { 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, 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, 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, InitError> { let input = MidiInput::new("WebMIDI input")?; let output = MidiOutput::new("WebMIDI output")?; let mut ports = Vec::::new(); collect_input_ports(&input, &mut ports); collect_output_ports(&output, &mut ports); Ok(ports) } impl MidirWrapper { fn new() -> Result { let ports = Vec::new(); let connections: Vec = 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) {} 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, ) -> bool { (*wrapper) .send(&(*id).to_string(), (*data).as_slice()) .is_ok() } fn collect_input_ports(input: &MidiInput, wrappers: &mut Vec) { 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) { 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); } }