jevstrudel.git / src-tauri / src / midibridge.rs
1use std::collections::HashMap;
2use std::sync::Arc;
3use std::time::Duration;
4use midir::MidiOutput;
5
6use tokio::sync::{ mpsc, Mutex };
7use tokio::time::Instant;
8use serde::Deserialize;
9use std::thread::sleep;
10
11use crate::loggerbridge::Logger;
12pub struct MidiMessage {
13  pub message: Vec<u8>,
14  pub instant: Instant,
15  pub offset: u64,
16  pub requestedport: String,
17}
18
19pub struct AsyncInputTransmit {
20  pub inner: Mutex<mpsc::Sender<Vec<MidiMessage>>>,
21}
22
23pub fn init(
24  logger: Logger,
25  async_input_receiver: mpsc::Receiver<Vec<MidiMessage>>,
26  mut async_output_receiver: mpsc::Receiver<Vec<MidiMessage>>,
27  async_output_transmitter: mpsc::Sender<Vec<MidiMessage>>
28) {
29  tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await });
30  let message_queue: Arc<Mutex<Vec<MidiMessage>>> = Arc::new(Mutex::new(Vec::new()));
31  /* ...........................................................
32         Listen For incoming messages and add to queue
33  ............................................................*/
34  let message_queue_clone = Arc::clone(&message_queue);
35  tauri::async_runtime::spawn(async move {
36    loop {
37      if let Some(package) = async_output_receiver.recv().await {
38        let mut message_queue = message_queue_clone.lock().await;
39        let messages = package;
40        for message in messages {
41          (*message_queue).push(message);
42        }
43      }
44    }
45  });
46
47  let message_queue_clone = Arc::clone(&message_queue);
48  tauri::async_runtime::spawn(async move {
49    /* ...........................................................
50                        Open Midi Ports
51    ............................................................*/
52    let midiout = MidiOutput::new("strudel").unwrap();
53    let out_ports = midiout.ports();
54    let mut port_names = Vec::new();
55    //TODO: Send these print messages to the UI logger instead of the rust console so the user can see them
56    if out_ports.len() == 0 {
57      logger.log(
58        " No MIDI devices found. Connect a device or enable IAC Driver to enable midi.".to_string(),
59        "".to_string()
60      );
61      // logger(window, " No MIDI devices found. Connect a device or enable IAC Driver.".to_string(), None);
62      return;
63    }
64    // give the frontend couple seconds to load on start, or the log messages will get lost
65    sleep(Duration::from_secs(3));
66    logger.log(format!("Found {} midi devices!", out_ports.len()), "".to_string());
67
68    // the user could reference any port at anytime during runtime,
69    // so let's go ahead and open them all (same behavior as web app)
70    let mut output_connections = HashMap::new();
71    for i in 0..=out_ports.len().saturating_sub(1) {
72      let midiout = MidiOutput::new("strudel").unwrap();
73      let ports = midiout.ports();
74      let port = ports.get(i).unwrap();
75      let port_name = midiout.port_name(port).unwrap();
76      logger.log(port_name.clone(), "".to_string());
77      let out_con = midiout.connect(port, &port_name).unwrap();
78      port_names.insert(i, port_name.clone());
79      output_connections.insert(port_name, out_con);
80    }
81    /* ...........................................................
82                        Process queued messages 
83    ............................................................*/
84
85    loop {
86      let mut message_queue = message_queue_clone.lock().await;
87
88      //iterate over each message, play and remove messages when they are ready
89      message_queue.retain(|message| {
90        if message.instant.elapsed().as_millis() < message.offset.into() {
91          return true;
92        }
93        let mut out_con = output_connections.get_mut(&message.requestedport);
94
95        // WebMidi supports getting a connection by part of its name
96        // ex: 'bus 1' instead of 'IAC Driver bus 1' so let's emulate that behavior
97        if out_con.is_none() {
98          let key = port_names.iter().find(|port_name| {
99            return port_name.contains(&message.requestedport);
100          });
101          if key.is_some() {
102            out_con = output_connections.get_mut(key.unwrap());
103          }
104        }
105
106        if out_con.is_some() {
107          // process the message
108          if let Err(err) = (&mut out_con.unwrap()).send(&message.message) {
109            logger.log(format!("Midi message send error: {}", err), "error".to_string());
110          }
111        } else {
112          logger.log(format!("failed to find midi device: {}", message.requestedport), "error".to_string());
113        }
114        return false;
115      });
116
117      sleep(Duration::from_millis(1));
118    }
119  });
120}
121
122pub async fn async_process_model(
123  mut input_reciever: mpsc::Receiver<Vec<MidiMessage>>,
124  output_transmitter: mpsc::Sender<Vec<MidiMessage>>
125) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
126  while let Some(input) = input_reciever.recv().await {
127    let output = input;
128    output_transmitter.send(output).await?;
129  }
130  Ok(())
131}
132#[derive(Deserialize)]
133pub struct MessageFromJS {
134  message: Vec<u8>,
135  offset: u64,
136  requestedport: String,
137}

Called from JS

139#[tauri::command]
140pub async fn sendmidi(
141  messagesfromjs: Vec<MessageFromJS>,
142  state: tauri::State<'_, AsyncInputTransmit>
143) -> Result<(), String> {
144  let async_proc_input_tx = state.inner.lock().await;
145  let mut messages_to_process: Vec<MidiMessage> = Vec::new();
146
147  for m in messagesfromjs {
148    let message_to_process = MidiMessage {
149      instant: Instant::now(),
150      message: m.message,
151      offset: m.offset,
152      requestedport: m.requestedport,
153    };
154    messages_to_process.push(message_to_process);
155  }
156
157  async_proc_input_tx.send(messages_to_process).await.map_err(|e| e.to_string())
158}