Skip to main content

oxide_browser/
midi.rs

1//! Host-side MIDI device enumeration and I/O for Oxide guest modules.
2//!
3//! Guests call the `api_midi_*` imports to enumerate MIDI input/output ports,
4//! open connections, send raw MIDI bytes, and poll for incoming messages.
5//!
6//! **macOS**: implemented via CoreMIDI (`coremidi` crate). Input callbacks run
7//! on a CoreMIDI background thread, split each incoming packet into individual
8//! MIDI messages, and push them onto a per-handle bounded [`VecDeque`]. The
9//! guest drains the queue by calling `api_midi_recv` each frame — no blocking,
10//! no async runtime needed. Each `api_midi_recv` returns exactly one MIDI
11//! message; if the queue fills up, the oldest message is dropped.
12//!
13//! **Other platforms**: all functions return 0 / error codes gracefully (no
14//! devices found). MIDI support for Linux and Windows will be added in a later
15//! phase.
16
17use std::sync::{Arc, Mutex};
18
19use anyhow::Result;
20use wasmtime::{Caller, Linker};
21
22use crate::capabilities::{read_guest_bytes, write_guest_bytes, HostState};
23
24// ── Platform implementation ───────────────────────────────────────────────────
25
26#[cfg(target_os = "macos")]
27mod platform {
28    use std::collections::{HashMap, VecDeque};
29    use std::sync::{Arc, Mutex};
30
31    use coremidi::{Client, Destination, InputPort, OutputPort, PacketList, Source};
32
33    /// Hard cap on queued incoming MIDI messages per input port. If the guest
34    /// falls behind, oldest messages are dropped first. 4096 is ~1 MB worst
35    /// case with 256-byte SysEx dumps; for typical 3-byte note events it's
36    /// ~12 KB — plenty for a ~1 min backlog at max MIDI rate (~30k msg/s).
37    const MAX_QUEUED_MESSAGES: usize = 4096;
38
39    /// Split a raw CoreMIDI packet (which may concatenate several MIDI messages
40    /// sharing a timestamp) into individual messages and push each onto `out`,
41    /// enforcing [`MAX_QUEUED_MESSAGES`] by dropping the oldest on overflow.
42    ///
43    /// Handles channel voice (0x80–0xEF), System Common (0xF1–0xF6),
44    /// System Real-Time (0xF8–0xFF), and SysEx (0xF0 … 0xF7). Bytes that
45    /// appear without a preceding status byte (running status, or junk) are
46    /// skipped.
47    fn enqueue_messages(data: &[u8], out: &mut VecDeque<Vec<u8>>) {
48        let mut i = 0;
49        while i < data.len() {
50            let status = data[i];
51            if status & 0x80 == 0 {
52                // Data byte with no status — skip. CoreMIDI normally delivers
53                // complete messages, so this only hits on malformed input.
54                i += 1;
55                continue;
56            }
57            let end = match status & 0xF0 {
58                0x80 | 0x90 | 0xA0 | 0xB0 | 0xE0 => i + 3,
59                0xC0 | 0xD0 => i + 2,
60                0xF0 => match status {
61                    0xF0 => {
62                        // SysEx: consume up to and including the next 0xF7.
63                        match data[i + 1..].iter().position(|&b| b == 0xF7) {
64                            Some(p) => i + 1 + p + 1,
65                            None => data.len(),
66                        }
67                    }
68                    0xF1 | 0xF3 => i + 2,
69                    0xF2 => i + 3,
70                    // 0xF4–0xF7 (undefined / SysEx end) and 0xF8–0xFF
71                    // (real-time) are single-byte messages.
72                    _ => i + 1,
73                },
74                _ => i + 1,
75            };
76            let end = end.min(data.len());
77            let msg: Vec<u8> = data[i..end].to_vec();
78            if out.len() >= MAX_QUEUED_MESSAGES {
79                out.pop_front();
80            }
81            out.push_back(msg);
82            i = end;
83        }
84    }
85
86    // ── Per-handle state ──────────────────────────────────────────────────
87
88    pub struct InputConn {
89        /// Kept alive — dropping closes the CoreMIDI port.
90        _port: InputPort,
91        pub queue: Arc<Mutex<VecDeque<Vec<u8>>>>,
92    }
93
94    pub struct OutputConn {
95        port: OutputPort,
96        dest_idx: usize,
97    }
98
99    // ── MidiState ─────────────────────────────────────────────────────────
100
101    pub struct MidiState {
102        client: Client,
103        next_handle: u32,
104        inputs: HashMap<u32, InputConn>,
105        outputs: HashMap<u32, OutputConn>,
106    }
107
108    impl MidiState {
109        pub fn new() -> Option<Self> {
110            let client = Client::new("oxide-browser").ok()?;
111            Some(Self {
112                client,
113                next_handle: 1,
114                inputs: HashMap::new(),
115                outputs: HashMap::new(),
116            })
117        }
118
119        fn alloc_handle(&mut self) -> u32 {
120            let h = self.next_handle;
121            self.next_handle = self.next_handle.wrapping_add(1).max(1);
122            h
123        }
124
125        pub fn input_count() -> u32 {
126            coremidi::Sources::count() as u32
127        }
128
129        pub fn output_count() -> u32 {
130            coremidi::Destinations::count() as u32
131        }
132
133        pub fn input_name(index: u32) -> Option<String> {
134            let src = Source::from_index(index as usize)?;
135            src.display_name()
136                .or_else(|| Some(format!("Input {}", index)))
137        }
138
139        pub fn output_name(index: u32) -> Option<String> {
140            let dst = Destination::from_index(index as usize)?;
141            dst.display_name()
142                .or_else(|| Some(format!("Output {}", index)))
143        }
144
145        pub fn open_input(&mut self, index: u32) -> u32 {
146            let source = match Source::from_index(index as usize) {
147                Some(s) => s,
148                None => return 0,
149            };
150            let queue: Arc<Mutex<VecDeque<Vec<u8>>>> = Arc::new(Mutex::new(VecDeque::new()));
151            let q = queue.clone();
152            let port_name = format!("oxide-in-{}", index);
153            let port = match self
154                .client
155                .input_port(&port_name, move |pkt_list: &PacketList| {
156                    let mut lock = q.lock().unwrap();
157                    for pkt in pkt_list.iter() {
158                        let bytes = pkt.data();
159                        if !bytes.is_empty() {
160                            enqueue_messages(bytes, &mut lock);
161                        }
162                    }
163                }) {
164                Ok(p) => p,
165                Err(_) => return 0,
166            };
167            if port.connect_source(&source).is_err() {
168                return 0;
169            }
170            let handle = self.alloc_handle();
171            self.inputs.insert(handle, InputConn { _port: port, queue });
172            handle
173        }
174
175        pub fn open_output(&mut self, index: u32) -> u32 {
176            let port_name = format!("oxide-out-{}", index);
177            let port = match self.client.output_port(&port_name) {
178                Ok(p) => p,
179                Err(_) => return 0,
180            };
181            let handle = self.alloc_handle();
182            self.outputs.insert(
183                handle,
184                OutputConn {
185                    port,
186                    dest_idx: index as usize,
187                },
188            );
189            handle
190        }
191
192        pub fn send(&mut self, handle: u32, data: &[u8]) -> bool {
193            let out = match self.outputs.get_mut(&handle) {
194                Some(o) => o,
195                None => return false,
196            };
197            let dest = match Destination::from_index(out.dest_idx) {
198                Some(d) => d,
199                None => return false,
200            };
201            // Build a single-packet PacketList from raw bytes.
202            let packets = coremidi::PacketBuffer::new(0, data);
203            out.port.send(&dest, &packets).is_ok()
204        }
205
206        /// Length in bytes of the front queued message without popping it.
207        /// Used by `api_midi_recv` to decide whether the guest's buffer is
208        /// large enough before dequeuing.
209        pub fn peek_len(&self, handle: u32) -> Option<usize> {
210            self.inputs
211                .get(&handle)?
212                .queue
213                .lock()
214                .unwrap()
215                .front()
216                .map(|m| m.len())
217        }
218
219        pub fn recv(&self, handle: u32) -> Option<Vec<u8>> {
220            self.inputs.get(&handle)?.queue.lock().unwrap().pop_front()
221        }
222
223        pub fn close(&mut self, handle: u32) {
224            self.inputs.remove(&handle);
225            self.outputs.remove(&handle);
226        }
227    }
228}
229
230// ── Stub for non-macOS ────────────────────────────────────────────────────────
231
232#[cfg(not(target_os = "macos"))]
233mod platform {
234    /// Placeholder — MIDI is not yet supported on this platform.
235    pub struct MidiState;
236
237    impl MidiState {
238        pub fn new() -> Option<Self> {
239            Some(Self)
240        }
241        pub fn input_count() -> u32 {
242            0
243        }
244        pub fn output_count() -> u32 {
245            0
246        }
247        pub fn input_name(_index: u32) -> Option<String> {
248            None
249        }
250        pub fn output_name(_index: u32) -> Option<String> {
251            None
252        }
253        pub fn open_input(&mut self, _index: u32) -> u32 {
254            0
255        }
256        pub fn open_output(&mut self, _index: u32) -> u32 {
257            0
258        }
259        pub fn send(&mut self, _handle: u32, _data: &[u8]) -> bool {
260            false
261        }
262        pub fn peek_len(&self, _handle: u32) -> Option<usize> {
263            None
264        }
265        pub fn recv(&self, _handle: u32) -> Option<Vec<u8>> {
266            None
267        }
268        pub fn close(&mut self, _handle: u32) {}
269    }
270}
271
272// ── Public re-export ──────────────────────────────────────────────────────────
273
274pub use platform::MidiState;
275
276// ── Lazy initialisation helper ────────────────────────────────────────────────
277
278fn ensure_midi(state: &Arc<Mutex<Option<MidiState>>>) {
279    let mut g = state.lock().unwrap();
280    if g.is_none() {
281        *g = MidiState::new();
282    }
283}
284
285// ── Host function registration ────────────────────────────────────────────────
286
287/// Register all `api_midi_*` host functions on the given linker.
288pub fn register_midi_functions(linker: &mut Linker<HostState>) -> Result<()> {
289    // ── midi_input_count ──────────────────────────────────────────────────
290    // api_midi_input_count() -> u32
291    linker.func_wrap(
292        "oxide",
293        "api_midi_input_count",
294        |_caller: Caller<'_, HostState>| -> u32 { MidiState::input_count() },
295    )?;
296
297    // ── midi_output_count ─────────────────────────────────────────────────
298    // api_midi_output_count() -> u32
299    linker.func_wrap(
300        "oxide",
301        "api_midi_output_count",
302        |_caller: Caller<'_, HostState>| -> u32 { MidiState::output_count() },
303    )?;
304
305    // ── midi_input_name ───────────────────────────────────────────────────
306    // api_midi_input_name(index: u32, out_ptr: u32, out_cap: u32) -> u32
307    // Writes the port name into guest memory. Returns bytes written, or 0
308    // if the index is out of range.
309    linker.func_wrap(
310        "oxide",
311        "api_midi_input_name",
312        |mut caller: Caller<'_, HostState>, index: u32, out_ptr: u32, out_cap: u32| -> u32 {
313            let name = match MidiState::input_name(index) {
314                Some(n) => n,
315                None => return 0,
316            };
317            let mem = match caller.data().memory {
318                Some(m) => m,
319                None => return 0,
320            };
321            let bytes = name.as_bytes();
322            let len = bytes.len().min(out_cap as usize);
323            if write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..len]).is_err() {
324                return 0;
325            }
326            len as u32
327        },
328    )?;
329
330    // ── midi_output_name ──────────────────────────────────────────────────
331    // api_midi_output_name(index: u32, out_ptr: u32, out_cap: u32) -> u32
332    linker.func_wrap(
333        "oxide",
334        "api_midi_output_name",
335        |mut caller: Caller<'_, HostState>, index: u32, out_ptr: u32, out_cap: u32| -> u32 {
336            let name = match MidiState::output_name(index) {
337                Some(n) => n,
338                None => return 0,
339            };
340            let mem = match caller.data().memory {
341                Some(m) => m,
342                None => return 0,
343            };
344            let bytes = name.as_bytes();
345            let len = bytes.len().min(out_cap as usize);
346            if write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..len]).is_err() {
347                return 0;
348            }
349            len as u32
350        },
351    )?;
352
353    // ── midi_open_input ───────────────────────────────────────────────────
354    // api_midi_open_input(index: u32) -> u32
355    // Returns a handle (> 0) or 0 on failure.
356    linker.func_wrap(
357        "oxide",
358        "api_midi_open_input",
359        |caller: Caller<'_, HostState>, index: u32| -> u32 {
360            let midi = caller.data().midi.clone();
361            ensure_midi(&midi);
362            let mut g = midi.lock().unwrap();
363            g.as_mut().map(|s| s.open_input(index)).unwrap_or(0)
364        },
365    )?;
366
367    // ── midi_open_output ──────────────────────────────────────────────────
368    // api_midi_open_output(index: u32) -> u32
369    linker.func_wrap(
370        "oxide",
371        "api_midi_open_output",
372        |caller: Caller<'_, HostState>, index: u32| -> u32 {
373            let midi = caller.data().midi.clone();
374            ensure_midi(&midi);
375            let mut g = midi.lock().unwrap();
376            g.as_mut().map(|s| s.open_output(index)).unwrap_or(0)
377        },
378    )?;
379
380    // ── midi_send ─────────────────────────────────────────────────────────
381    // api_midi_send(handle: u32, data_ptr: u32, data_len: u32) -> i32
382    // Returns 0 on success, -1 on failure.
383    linker.func_wrap(
384        "oxide",
385        "api_midi_send",
386        |caller: Caller<'_, HostState>, handle: u32, ptr: u32, len: u32| -> i32 {
387            let mem = match caller.data().memory {
388                Some(m) => m,
389                None => return -1,
390            };
391            let data = match read_guest_bytes(&mem, &caller, ptr, len) {
392                Ok(b) => b,
393                Err(_) => return -1,
394            };
395            let midi = caller.data().midi.clone();
396            let mut g = midi.lock().unwrap();
397            if g.as_mut().is_some_and(|s| s.send(handle, &data)) {
398                0
399            } else {
400                -1
401            }
402        },
403    )?;
404
405    // ── midi_recv ─────────────────────────────────────────────────────────
406    // api_midi_recv(handle: u32, out_ptr: u32, out_cap: u32) -> i32
407    // Dequeues one MIDI message and writes its bytes into guest memory.
408    // Returns bytes written on success, -1 if the queue is empty, or -2 if
409    // the guest buffer is too small (message stays in the queue so the guest
410    // can retry with a larger buffer).
411    linker.func_wrap(
412        "oxide",
413        "api_midi_recv",
414        |mut caller: Caller<'_, HostState>, handle: u32, out_ptr: u32, out_cap: u32| -> i32 {
415            let midi = caller.data().midi.clone();
416            let peek = {
417                let g = midi.lock().unwrap();
418                g.as_ref().and_then(|s| s.peek_len(handle))
419            };
420            let msg_len = match peek {
421                Some(n) => n,
422                None => return -1,
423            };
424            if msg_len > out_cap as usize {
425                return -2;
426            }
427            let msg = {
428                let g = midi.lock().unwrap();
429                match g.as_ref().and_then(|s| s.recv(handle)) {
430                    Some(m) => m,
431                    None => return -1,
432                }
433            };
434            let mem = match caller.data().memory {
435                Some(m) => m,
436                None => return -1,
437            };
438            if write_guest_bytes(&mem, &mut caller, out_ptr, &msg).is_err() {
439                return -1;
440            }
441            msg.len() as i32
442        },
443    )?;
444
445    // ── midi_close ────────────────────────────────────────────────────────
446    // api_midi_close(handle: u32)
447    // Frees host-side resources for the given handle.
448    linker.func_wrap(
449        "oxide",
450        "api_midi_close",
451        |caller: Caller<'_, HostState>, handle: u32| {
452            let midi = caller.data().midi.clone();
453            let mut g = midi.lock().unwrap();
454            if let Some(ref mut state) = *g {
455                state.close(handle);
456            }
457        },
458    )?;
459
460    Ok(())
461}