tauri_plugin_midi/
lib.rs

1//! A WebMIDI-compatible plugin for Tauri
2//!
3//! Refer to the [init](fn.init.html) function for more information on how to use this plugin or checkout [the example](https://github.com/specta-rs/tauri-plugin-midi/tree/main/example).
4
5use std::{
6    collections::BTreeMap,
7    sync::{Arc, Mutex, PoisonError},
8    thread::spawn,
9    time::Duration,
10};
11
12use midir::{MidiInput, MidiOutput};
13use tauri::{
14    plugin::{Builder, TauriPlugin},
15    Manager, Runtime,
16};
17use tauri_specta::Event;
18
19#[derive(Default)]
20struct MidiState {
21    input_connections: BTreeMap<String, midir::MidiInputConnection<()>>,
22    output_connections: BTreeMap<String, midir::MidiOutputConnection>,
23}
24
25type State = Arc<Mutex<MidiState>>;
26
27const PLUGIN_NAME: &str = "midi";
28const RUNTIME_POLYFILL: &str = include_str!("polyfill.js");
29
30fn get_inputs(midi_in: &midir::MidiInput) -> Result<Vec<(String, String)>, String> {
31    midi_in
32        .ports()
33        .iter()
34        .map(|p| {
35            Ok((
36                p.id(),
37                midi_in
38                    .port_name(p)
39                    .map_err(|e| format!("Failed to get port name: {e}"))?,
40            ))
41        })
42        .collect()
43}
44
45fn get_outputs(midi_out: &midir::MidiOutput) -> Result<Vec<(String, String)>, String> {
46    midi_out
47        .ports()
48        .iter()
49        .map(|p| {
50            Ok((
51                p.id(),
52                midi_out
53                    .port_name(p)
54                    .map_err(|e| format!("Failed to get port name: {e}"))?,
55            ))
56        })
57        .collect()
58}
59
60#[tauri::command(async)]
61#[specta::specta]
62fn open_input<R: tauri::Runtime>(
63    id: String,
64    state: tauri::State<State>,
65    app: tauri::AppHandle<R>,
66) -> Result<(), String> {
67    let mut state = state.lock().unwrap_or_else(PoisonError::into_inner);
68
69    if state.input_connections.contains_key(&id) {
70        return Ok(());
71    }
72
73    let mut midi_in = MidiInput::new("").unwrap();
74    midi_in.ignore(midir::Ignore::None);
75
76    let ports = midi_in.ports();
77    let port = ports
78        .iter()
79        .find(|p| p.id() == id)
80        .ok_or_else(|| format!("Failed to find port by id '{id}'"))?;
81
82    let connection = midi_in
83        .connect(
84            &port,
85            "",
86            {
87                let id = id.clone();
88                move |_, msg, _| {
89                    MIDIMessage(id.to_string(), msg.to_vec())
90                        .emit(&app)
91                        .unwrap();
92                }
93            },
94            (),
95        )
96        .map_err(|e| format!("Failed to open MIDI input to id '{id}': {e}"))?;
97
98    state.input_connections.insert(id, connection);
99
100    Ok(())
101}
102
103#[tauri::command(async)]
104#[specta::specta]
105fn close_input(id: String, state: tauri::State<State>) {
106    let mut state = state.lock().unwrap_or_else(PoisonError::into_inner);
107
108    if let Some(connection) = state.input_connections.remove(&id) {
109        connection.close();
110    }
111}
112
113#[tauri::command(async)]
114#[specta::specta]
115fn open_output(id: String, state: tauri::State<State>) -> Result<(), String> {
116    let mut state = state.lock().unwrap_or_else(PoisonError::into_inner);
117
118    if state.output_connections.contains_key(&id) {
119        return Ok(());
120    }
121
122    let midi_out = MidiOutput::new("").map_err(|e| format!("Failed to create MIDI output: {e}"))?;
123
124    let ports = midi_out.ports();
125    let port = ports
126        .iter()
127        .find(|p| p.id() == id)
128        .ok_or_else(|| format!("Failed to find port by id '{id}'"))?;
129
130    let connection = midi_out
131        .connect(&port, "")
132        .map_err(|e| format!("Failed to open MIDI output to id '{id}': {e}"))?;
133
134    state.output_connections.insert(id, connection);
135
136    Ok(())
137}
138
139#[tauri::command(async)]
140#[specta::specta]
141fn close_output(id: String, state: tauri::State<State>) {
142    let mut state = state.lock().unwrap_or_else(PoisonError::into_inner);
143
144    if let Some(connection) = state.output_connections.remove(&id) {
145        connection.close();
146    }
147}
148
149#[tauri::command(async)]
150#[specta::specta]
151fn output_send(id: String, msg: Vec<u8>, state: tauri::State<State>) -> Result<(), String> {
152    let mut state = state.lock().unwrap_or_else(PoisonError::into_inner);
153
154    let connection = state
155        .output_connections
156        .get_mut(&id)
157        .ok_or_else(|| format!("Failed to find output connection by name '{id}'"))?;
158
159    connection
160        .send(&msg)
161        .map_err(|err| format!("Failed to send MIDI message to port '{id}': {err}"))?;
162
163    Ok(())
164}
165
166#[derive(serde::Serialize, specta::Type, tauri_specta::Event, Clone, Debug)]
167struct StateChange {
168    inputs: Vec<(String, String)>,
169    outputs: Vec<(String, String)>,
170}
171
172#[derive(serde::Serialize, specta::Type, tauri_specta::Event, Clone)]
173struct MIDIMessage(String, Vec<u8>);
174
175fn builder<R: Runtime>() -> tauri_specta::Builder<R> {
176    tauri_specta::Builder::<R>::new()
177        .plugin_name(PLUGIN_NAME)
178        .commands(tauri_specta::collect_commands![
179            open_input::<tauri::Wry>,
180            close_input,
181            open_output,
182            close_output,
183            output_send
184        ])
185        .events(tauri_specta::collect_events![StateChange, MIDIMessage])
186}
187
188/// Initialise the plugin which will take care of polyfilling WebMIDI into any Tauri webview.
189///
190/// # Usage
191///
192/// Using this plugin is very simple. Just add it to your Tauri builder:
193///
194/// ```rust
195///  tauri::Builder::default()
196///        .plugin(tauri_plugin_midi::init()) // <- This bit here
197/// # ;
198///        // .... rest of your builder
199/// ```
200///
201/// Then give permissions to the plugin by adding the `midi:default` permissions to your application.
202///
203/// This can be done by modifying the `capabilities/default.json` file:
204/// ```json
205/// {
206///   "$schema": "../gen/schemas/desktop-schema.json",
207///   "identifier": "default",
208///   "description": "Capability for the main window",
209///   "windows": ["main"],
210///   "permissions": ["core:default", "midi:default"] // <- add `midi:default` into here
211/// }
212/// ```
213///
214/// and now you can use the regular [WebMIDI API](https://developer.mozilla.org/en-US/docs/Web/API/Web_MIDI_API) from within your webview.
215///
216/// ## Known issues
217///
218/// - This plugin doesn't work within iframes at the moment. It's being tracked as [#7](https://github.com/specta-rs/tauri-plugin-midi/issues/7)
219///
220pub fn init<R: Runtime>() -> TauriPlugin<R> {
221    let builder = builder::<R>();
222    // Tauri did a breaking change in 2.7.0 so we do this outside to ensure backwards compatibility
223    let polyfill: String = RUNTIME_POLYFILL.into();
224
225    Builder::new(PLUGIN_NAME)
226        .invoke_handler(builder.invoke_handler())
227        .js_init_script(polyfill)
228        .setup(move |app, _| {
229            app.manage(State::default());
230
231            builder.mount_events(app);
232
233            let app = app.clone();
234
235            #[cfg(target_os = "macos")]
236            coremidi_hotplug_notification::receive_device_updates(|| {})
237                .expect("Failed to register for MIDI device updates");
238
239            spawn(move || {
240                let midi_in = midir::MidiInput::new("tauri-plugin-midi blank input")
241                    .map_err(|e| format!("Failed to create MIDI input: {e}"))
242                    .unwrap();
243                let midi_out = midir::MidiOutput::new("tauri-plugin-midi blank output")
244                    .map_err(|e| format!("Failed to create MIDI output: {e}"))
245                    .unwrap();
246
247                loop {
248                    StateChange {
249                        inputs: get_inputs(&midi_in).unwrap_or_default(),
250                        outputs: get_outputs(&midi_out).unwrap_or_default(),
251                    }
252                    .emit(&app)
253                    .unwrap();
254
255                    std::thread::sleep(Duration::from_millis(1000));
256                }
257            });
258
259            Ok(())
260        })
261        .build()
262}
263
264#[cfg(test)]
265mod test {
266    use super::*;
267
268    #[test]
269    fn export_types() {
270        builder::<tauri::Wry>()
271            .error_handling(tauri_specta::ErrorHandlingMode::Throw)
272            .export(
273                specta_typescript::Typescript::default(),
274                "./guest-js/bindings.ts",
275            )
276            .unwrap();
277    }
278}