1use std::{
6 collections::BTreeMap,
7 sync::{Arc, Mutex, PoisonError},
8 thread::spawn,
9 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
10};
11
12use midir::{MidiInput, MidiOutput};
13use tauri::{
14 plugin::{Builder, TauriPlugin},
15 Emitter, Manager, Runtime,
16};
17use tokio::time::sleep_until;
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#[cfg_attr(feature = "specta", specta::specta)]
62fn open_input<R: tauri::Runtime>(
63 app: tauri::AppHandle<R>,
64 state: tauri::State<State>,
65 id: String,
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 let epoch = std::time::SystemTime::now()
90 .duration_since(std::time::UNIX_EPOCH)
91 .expect("System time is before epoch")
92 .as_millis();
93
94 if let Err(err) = app.emit(
95 "plugin:midi:midi-message",
96 MIDIMessage(id.to_string(), epoch.to_string(), msg.to_vec()),
97 ) {
98 eprintln!("Failed to emit MIDI message: {err}");
99 }
100 }
101 },
102 (),
103 )
104 .map_err(|e| format!("Failed to open MIDI input to id '{id}': {e}"))?;
105
106 state.input_connections.insert(id, connection);
107
108 Ok(())
109}
110
111#[tauri::command(async)]
112#[cfg_attr(feature = "specta", specta::specta)]
113fn close_input(state: tauri::State<State>, id: String) {
114 let mut state = state.lock().unwrap_or_else(PoisonError::into_inner);
115
116 if let Some(connection) = state.input_connections.remove(&id) {
117 connection.close();
118 }
119}
120
121#[tauri::command(async)]
122#[cfg_attr(feature = "specta", specta::specta)]
123fn open_output(state: tauri::State<State>, id: String) -> Result<(), String> {
124 let mut state = state.lock().unwrap_or_else(PoisonError::into_inner);
125
126 if state.output_connections.contains_key(&id) {
127 return Ok(());
128 }
129
130 let midi_out = MidiOutput::new("").map_err(|e| format!("Failed to create MIDI output: {e}"))?;
131
132 let ports = midi_out.ports();
133 let port = ports
134 .iter()
135 .find(|p| p.id() == id)
136 .ok_or_else(|| format!("Failed to find port by id '{id}'"))?;
137
138 let connection = midi_out
139 .connect(&port, "")
140 .map_err(|e| format!("Failed to open MIDI output to id '{id}': {e}"))?;
141
142 state.output_connections.insert(id, connection);
143
144 Ok(())
145}
146
147#[tauri::command(async)]
148#[cfg_attr(feature = "specta", specta::specta)]
149fn close_output(state: tauri::State<State>, id: String) {
150 let mut state = state.lock().unwrap_or_else(PoisonError::into_inner);
151
152 if let Some(connection) = state.output_connections.remove(&id) {
153 connection.close();
154 }
155}
156
157#[tauri::command(async)]
158#[cfg_attr(feature = "specta", specta::specta)]
159fn output_send(
160 tstate: tauri::State<State>,
161 id: String,
162 msg: Vec<u8>,
163 timestamp: Option<String>,
164) -> Result<(), String> {
165 let timestamp = timestamp
166 .map(|s| {
167 s.parse::<u128>()
168 .map_err(|e| format!("Failed to parse timestamp: {e}"))
169 })
170 .transpose()?;
171
172 let mut state = tstate.lock().unwrap_or_else(PoisonError::into_inner);
173 let connection = state
174 .output_connections
175 .get_mut(&id)
176 .ok_or_else(|| format!("Failed to find output connection by name '{id}'"))?;
177
178 if let Some(timestamp) = timestamp {
180 drop(state);
181 let tstate = (*tstate).clone();
182 tauri::async_runtime::spawn(async move {
183 let current_epoch = SystemTime::now()
184 .duration_since(UNIX_EPOCH)
185 .expect("SystemTime is before epoch")
186 .as_millis();
187
188 let delay_ms = timestamp.saturating_sub(current_epoch);
189 let until = Instant::now() + Duration::from_millis(delay_ms as u64);
190 sleep_until(until.into()).await;
191
192 let mut state = tstate.lock().unwrap_or_else(PoisonError::into_inner);
193 if let Some(connection) = state.output_connections.get_mut(&id) {
194 connection.send(&msg).ok();
195 }
196 });
197 } else {
198 connection
199 .send(&msg)
200 .map_err(|err| format!("Failed to send MIDI message to port '{id}': {err}"))?;
201 }
202
203 Ok(())
204}
205
206#[derive(serde::Serialize, Clone, Debug)]
207#[cfg_attr(feature = "specta", derive(specta::Type, tauri_specta::Event))]
208struct StateChange {
209 inputs: Vec<(String, String)>,
210 outputs: Vec<(String, String)>,
211}
212
213#[derive(serde::Serialize, Clone)]
214#[cfg_attr(feature = "specta", derive(specta::Type, tauri_specta::Event))]
215struct MIDIMessage(String, String, Vec<u8>);
216
217pub fn init<R: Runtime>() -> TauriPlugin<R> {
251 let polyfill: String = RUNTIME_POLYFILL.into();
253
254 Builder::new(PLUGIN_NAME)
255 .invoke_handler(tauri::generate_handler![
256 open_input,
257 close_input,
258 open_output,
259 close_output,
260 output_send
261 ])
262 .js_init_script_on_all_frames(polyfill)
267 .setup(move |app, _| {
268 app.manage(State::default());
269
270 let app = app.clone();
271
272 spawn(move || loop {
273 let midi_in = match midir::MidiInput::new("tauri-plugin-midi blank input") {
278 Ok(midi_in) => midi_in,
279 Err(e) => {
280 eprintln!("Failed to create MIDI input: {e}");
281 std::thread::sleep(Duration::from_millis(1000));
282 continue;
283 }
284 };
285 let midi_out = match midir::MidiOutput::new("tauri-plugin-midi blank output") {
286 Ok(midi_out) => midi_out,
287 Err(e) => {
288 eprintln!("Failed to create MIDI output: {e}");
289 std::thread::sleep(Duration::from_millis(1000));
290 continue;
291 }
292 };
293
294 loop {
295 let state_change = StateChange {
296 inputs: get_inputs(&midi_in).unwrap_or_default(),
297 outputs: get_outputs(&midi_out).unwrap_or_default(),
298 };
299 if let Err(err) = app.emit("plugin:midi:state-change", state_change) {
300 eprintln!("Failed to emit MIDI state change: {err}");
301 }
302
303 std::thread::sleep(Duration::from_millis(1000));
304 }
305 });
306
307 Ok(())
308 })
309 .build()
310}
311
312#[cfg(all(test, feature = "specta"))]
313mod test {
314 use super::*;
315
316 #[test]
317 fn export_types() {
318 tauri_specta::Builder::<tauri::Wry>::new()
319 .plugin_name(PLUGIN_NAME)
320 .commands(tauri_specta::collect_commands![
321 open_input::<tauri::Wry>,
322 close_input,
323 open_output,
324 close_output,
325 output_send
326 ])
327 .events(tauri_specta::collect_events![StateChange, MIDIMessage])
328 .error_handling(tauri_specta::ErrorHandlingMode::Throw)
329 .export(
330 specta_typescript::Typescript::default(),
331 "./guest-js/bindings.ts",
332 )
333 .unwrap();
334 }
335}