1use 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
188pub fn init<R: Runtime>() -> TauriPlugin<R> {
221 let builder = builder::<R>();
222 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}