summaryrefslogtreecommitdiffstats
path: root/third_party/rust/midir/src/backend/webmidi
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/midir/src/backend/webmidi')
-rw-r--r--third_party/rust/midir/src/backend/webmidi/mod.rs250
1 files changed, 250 insertions, 0 deletions
diff --git a/third_party/rust/midir/src/backend/webmidi/mod.rs b/third_party/rust/midir/src/backend/webmidi/mod.rs
new file mode 100644
index 0000000000..b76f0b3617
--- /dev/null
+++ b/third_party/rust/midir/src/backend/webmidi/mod.rs
@@ -0,0 +1,250 @@
+//! Web MIDI Backend.
+//!
+//! Reference:
+//! * [W3C Editor's Draft](https://webaudio.github.io/web-midi-api/)
+//! * [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/API/MIDIAccess)
+
+extern crate js_sys;
+extern crate wasm_bindgen;
+extern crate web_sys;
+
+use self::js_sys::{Map, Promise, Uint8Array};
+use self::wasm_bindgen::prelude::*;
+use self::wasm_bindgen::JsCast;
+use self::web_sys::{MidiAccess, MidiOptions, MidiMessageEvent};
+
+use std::cell::RefCell;
+use std::sync::{Arc, Mutex};
+
+use ::errors::*;
+use ::Ignore;
+
+
+
+thread_local! {
+ static STATIC : RefCell<Static> = RefCell::new(Static::new());
+}
+
+struct Static {
+ pub access: Option<MidiAccess>,
+ pub request: Option<Promise>,
+ pub ever_requested: bool,
+
+ pub on_ok: Closure<dyn FnMut(JsValue)>,
+ pub on_err: Closure<dyn FnMut(JsValue)>,
+}
+
+impl Static {
+ pub fn new() -> Self {
+ let mut s = Self {
+ access: None,
+ request: None,
+ ever_requested: false,
+
+ on_ok: Closure::wrap(Box::new(|access| {
+ STATIC.with(|s|{
+ let mut s = s.borrow_mut();
+ let access : MidiAccess = access.dyn_into().unwrap();
+ s.request = None;
+ s.access = Some(access);
+ });
+ })),
+ on_err: Closure::wrap(Box::new(|_error| {
+ STATIC.with(|s|{
+ let mut s = s.borrow_mut();
+ s.request = None;
+ });
+ })),
+ };
+ // Some notes on sysex behavior:
+ // 1) Some devices (but not all!) may work without sysex
+ // 2) Chrome will only prompt the end user to grant permission if they requested sysex permissions for now...
+ // but that's changing soon for "security reasons" (reduced fingerprinting? poorly tested drivers?):
+ // https://www.chromestatus.com/feature/5138066234671104
+ //
+ // I've chosen to hardcode sysex=true here, since that'll be compatible with more devices, *and* should change
+ // less behavior when Chrome's changes land.
+ s.request_midi_access(true);
+ s
+ }
+
+ fn request_midi_access(&mut self, sysex: bool) {
+ self.ever_requested = true;
+ if self.access.is_some() { return; } // Already have access
+ if self.request.is_some() { return; } // Mid-request already
+ let window = if let Some(w) = web_sys::window() { w } else { return; };
+
+ let _request = match window.navigator().request_midi_access_with_options(MidiOptions::new().sysex(sysex)) {
+ Ok(p) => { self.request = Some(p.then2(&self.on_ok, &self.on_err)); },
+ Err(_) => { return; } // node.js? brower doesn't support webmidi? other?
+ };
+ }
+}
+
+#[derive(Clone, PartialEq)]
+pub struct MidiInputPort {
+ input: web_sys::MidiInput,
+}
+
+pub struct MidiInput {
+ ignore_flags: Ignore
+}
+
+impl MidiInput {
+ pub fn new(_client_name: &str) -> Result<Self, InitError> {
+ STATIC.with(|_|{});
+ Ok(MidiInput { ignore_flags: Ignore::None })
+ }
+
+ pub(crate) fn ports_internal(&self) -> Vec<::common::MidiInputPort> {
+ STATIC.with(|s|{
+ let mut v = Vec::new();
+ let s = s.borrow();
+ if let Some(access) = s.access.as_ref() {
+ let inputs : Map = access.inputs().unchecked_into();
+ inputs.for_each(&mut |value, _|{
+ v.push(::common::MidiInputPort {
+ imp: MidiInputPort { input: value.dyn_into().unwrap() }
+ });
+ });
+ }
+ v
+ })
+ }
+
+ pub fn ignore(&mut self, flags: Ignore) {
+ self.ignore_flags = flags;
+ }
+
+ pub fn port_count(&self) -> usize {
+ STATIC.with(|s| {
+ let s = s.borrow();
+ s.access.as_ref().map(|access| access.inputs().unchecked_into::<Map>().size() as usize).unwrap_or(0)
+ })
+ }
+
+ pub fn port_name(&self, port: &MidiInputPort) -> Result<String, PortInfoError> {
+ Ok(port.input.name().unwrap_or_else(|| port.input.id()))
+ }
+
+ pub fn connect<F, T: Send + 'static>(
+ self, port: &MidiInputPort, _port_name: &str, mut callback: F, data: T
+ ) -> Result<MidiInputConnection<T>, ConnectError<MidiInput>>
+ where F: FnMut(u64, &[u8], &mut T) + Send + 'static
+ {
+ let input = port.input.clone();
+ let _ = input.open(); // NOTE: asyncronous!
+
+ let ignore_flags = self.ignore_flags;
+ let user_data = Arc::new(Mutex::new(Some(data)));
+
+ let closure = {
+ let user_data = user_data.clone();
+
+ let closure = Closure::wrap(Box::new(move |event: MidiMessageEvent| {
+ let time = (event.time_stamp() * 1000.0) as u64; // ms -> us
+ let buffer = event.data().unwrap();
+
+ let status = buffer[0];
+ if !(status == 0xF0 && ignore_flags.contains(Ignore::Sysex) ||
+ status == 0xF1 && ignore_flags.contains(Ignore::Time) ||
+ status == 0xF8 && ignore_flags.contains(Ignore::Time) ||
+ status == 0xFE && ignore_flags.contains(Ignore::ActiveSense))
+ {
+ callback(time, &buffer[..], user_data.lock().unwrap().as_mut().unwrap());
+ }
+ }) as Box<dyn FnMut(MidiMessageEvent)>);
+
+ input.set_onmidimessage(Some(closure.as_ref().unchecked_ref()));
+
+ closure
+ };
+
+ Ok(MidiInputConnection { ignore_flags, input, user_data, closure })
+ }
+}
+
+pub struct MidiInputConnection<T> {
+ ignore_flags: Ignore,
+ input: web_sys::MidiInput,
+ user_data: Arc<Mutex<Option<T>>>,
+ #[allow(dead_code)] // Must be kept alive until we decide to unregister from input
+ closure: Closure<dyn FnMut(MidiMessageEvent)>,
+}
+
+impl<T> MidiInputConnection<T> {
+ pub fn close(self) -> (MidiInput, T) {
+ let Self { ignore_flags, input, user_data, .. } = self;
+
+ input.set_onmidimessage(None);
+ let mut user_data = user_data.lock().unwrap();
+
+ (
+ MidiInput { ignore_flags },
+ user_data.take().unwrap()
+ )
+ }
+}
+
+#[derive(Clone, PartialEq)]
+pub struct MidiOutputPort {
+ output: web_sys::MidiOutput,
+}
+
+pub struct MidiOutput {
+}
+
+impl MidiOutput {
+ pub fn new(_client_name: &str) -> Result<Self, InitError> {
+ STATIC.with(|_|{});
+ Ok(MidiOutput {})
+ }
+
+ pub(crate) fn ports_internal(&self) -> Vec<::common::MidiOutputPort> {
+ STATIC.with(|s|{
+ let mut v = Vec::new();
+ let s = s.borrow();
+ if let Some(access) = s.access.as_ref() {
+ access.outputs().unchecked_into::<Map>().for_each(&mut |value, _|{
+ v.push(::common::MidiOutputPort {
+ imp: MidiOutputPort { output: value.dyn_into().unwrap() }
+ });
+ });
+ }
+ v
+ })
+ }
+
+ pub fn port_count(&self) -> usize {
+ STATIC.with(|s|{
+ let s = s.borrow();
+ s.access.as_ref().map(|access| access.outputs().unchecked_into::<Map>().size() as usize).unwrap_or(0)
+ })
+ }
+
+ pub fn port_name(&self, port: &MidiOutputPort) -> Result<String, PortInfoError> {
+ Ok(port.output.name().unwrap_or_else(|| port.output.id()))
+ }
+
+ pub fn connect(self, port: &MidiOutputPort, _port_name: &str) -> Result<MidiOutputConnection, ConnectError<MidiOutput>> {
+ let _ = port.output.open(); // NOTE: asyncronous!
+ Ok(MidiOutputConnection{
+ output: port.output.clone()
+ })
+ }
+}
+
+pub struct MidiOutputConnection {
+ output: web_sys::MidiOutput,
+}
+
+impl MidiOutputConnection {
+ pub fn close(self) -> MidiOutput {
+ let _ = self.output.close(); // NOTE: asyncronous!
+ MidiOutput {}
+ }
+
+ pub fn send(&mut self, message: &[u8]) -> Result<(), SendError> {
+ self.output.send(unsafe { Uint8Array::view(message) }.as_ref()).map_err(|_| SendError::Other("JavaScript exception"))
+ }
+}