devalang_wasm/engine/audio/
midi_native.rs

1// Native MIDI I/O helper (optional, enabled under CLI/native builds via 'midir')
2// Responsibilities:
3// - list available midi in/out ports
4// - open a chosen in port and forward incoming messages to EventRegistry
5// - provide a small API to send MIDI messages to out ports
6
7use crate::engine::events::EventRegistry;
8use crate::language::syntax::ast::Value;
9#[cfg(feature = "cli")]
10use midir::{MidiInput, MidiInputConnection, MidiOutput, MidiOutputConnection};
11use std::collections::HashMap;
12#[cfg(feature = "cli")]
13use std::sync::{Arc, Mutex};
14
15#[cfg(feature = "cli")]
16pub struct MidiManager {
17    // Keep connections alive
18    in_connections: Vec<MidiInputConnection<()>>,
19    out_connections: HashMap<String, MidiOutputConnection>,
20    registry: Arc<Mutex<EventRegistry>>,
21    start: Arc<std::time::Instant>,
22}
23
24#[cfg(feature = "cli")]
25impl MidiManager {
26    pub fn new(registry: Arc<Mutex<EventRegistry>>) -> Self {
27        MidiManager {
28            in_connections: Vec::new(),
29            out_connections: HashMap::new(),
30            registry,
31            start: Arc::new(std::time::Instant::now()),
32        }
33    }
34
35    pub fn list_input_ports() -> Vec<String> {
36        if let Ok(midi_in) = MidiInput::new("devalang-in") {
37            let ports = midi_in.ports();
38            ports
39                .iter()
40                .map(|p| {
41                    midi_in
42                        .port_name(p)
43                        .unwrap_or_else(|_| "unknown".to_string())
44                })
45                .collect()
46        } else {
47            Vec::new()
48        }
49    }
50
51    pub fn open_input_by_index(&mut self, index: usize, name: &str) -> Result<(), String> {
52        let midi_in = MidiInput::new("devalang-in").map_err(|e| format!("midi_in: {}", e))?;
53        // midi_in.ignore(Ignore::None);
54        let ports = midi_in.ports();
55        if index >= ports.len() {
56            return Err("port index out of range".to_string());
57        }
58        let port = ports[index].clone();
59        let registry = self.registry.clone();
60        let device_name = name.to_string();
61        let device_name_inner = device_name.clone();
62        let start = self.start.clone();
63        let conn_in = midi_in
64            .connect(
65                &port,
66                &device_name,
67                move |_stamp, message, _| {
68                    // Parse simple NoteOn/NoteOff (0x9x / 0x8x) messages
69                    if message[0] != 248 {
70                        let status = message[0];
71                        let cmd = status & 0xF0;
72                        let channel = (status & 0x0F) as u8;
73                        match cmd {
74                            0x90 => {
75                                let note = message[1] as u8;
76                                let vel = message[2] as u8;
77                                let mut data = HashMap::new();
78                                data.insert("note".to_string(), Value::Number(note as f32));
79                                data.insert("velocity".to_string(), Value::Number(vel as f32));
80                                data.insert("channel".to_string(), Value::Number(channel as f32));
81                                // Emit event name like mapping.in.<device>.noteOn (keyboard mode timestamp)
82                                let event_name = format!("mapping.in.{}.noteOn", device_name_inner);
83                                let elapsed = std::time::Instant::now().duration_since(*start);
84                                let ts = elapsed.as_secs_f32();
85                                if let Ok(mut reg) = registry.lock() {
86                                    reg.emit(event_name, data, ts);
87                                }
88                            }
89                            0x80 => {
90                                let note = message[1] as u8;
91                                let vel = message[2] as u8;
92                                let mut data = HashMap::new();
93                                data.insert("note".to_string(), Value::Number(note as f32));
94                                data.insert("velocity".to_string(), Value::Number(vel as f32));
95                                data.insert("channel".to_string(), Value::Number(channel as f32));
96                                let event_name =
97                                    format!("mapping.in.{}.noteOff", device_name_inner);
98                                let elapsed = std::time::Instant::now().duration_since(*start);
99                                let ts = elapsed.as_secs_f32();
100                                if let Ok(mut reg) = registry.lock() {
101                                    reg.emit(event_name, data, ts);
102                                }
103                            }
104                            _ => {}
105                        }
106                    }
107                },
108                (),
109            )
110            .map_err(|e| format!("connect error: {}", e))?;
111
112        self.in_connections.push(conn_in);
113        Ok(())
114    }
115
116    pub fn open_output_by_name(&mut self, name: &str, index: usize) -> Result<(), String> {
117        let midi_out = MidiOutput::new("devalang-out").map_err(|e| format!("midi_out: {}", e))?;
118        let ports = midi_out.ports();
119        if index >= ports.len() {
120            return Err("port index out of range".to_string());
121        }
122        let port = &ports[index];
123        let _port_name = midi_out
124            .port_name(port)
125            .unwrap_or_else(|_| "out".to_string());
126        let conn_out = midi_out
127            .connect(port, name)
128            .map_err(|e| format!("connect out: {}", e))?;
129        self.out_connections.insert(name.to_string(), conn_out);
130        Ok(())
131    }
132
133    pub fn send_note_on(
134        &mut self,
135        device_name: &str,
136        channel: u8,
137        note: u8,
138        vel: u8,
139    ) -> Result<(), String> {
140        if let Some(conn) = self.out_connections.get_mut(device_name) {
141            let status = 0x90 | (channel & 0x0F);
142            let _ = conn.send(&[status, note, vel]);
143            Ok(())
144        } else {
145            Err("output connection not found".to_string())
146        }
147    }
148}
149
150#[cfg(not(feature = "cli"))]
151pub struct MidiManager;
152
153#[cfg(not(feature = "cli"))]
154impl MidiManager {
155    pub fn new(_registry: Arc<Mutex<EventRegistry>>) -> Self {
156        MidiManager
157    }
158    pub fn list_input_ports() -> Vec<String> {
159        Vec::new()
160    }
161    pub fn open_input_by_index(&mut self, _index: usize, _name: &str) -> Result<(), String> {
162        Ok(())
163    }
164    pub fn open_output_by_name(&mut self, _name: &str, _index: usize) -> Result<(), String> {
165        Ok(())
166    }
167    pub fn send_note_on(
168        &mut self,
169        _device_name: &str,
170        _channel: u8,
171        _note: u8,
172        _vel: u8,
173    ) -> Result<(), String> {
174        Ok(())
175    }
176}