mimium_midi/
lib.rs

1//! ## mimium MIDI Plugin
2//!
3//! MIDI plugin currently implements a functionality for binding midi note signal to a tuple of float value.
4//! Processing for raw MIDI events like midi plugin in VST cannot be realized for now.
5
6use atomic_float::AtomicF64;
7use midir::{MidiInput, MidiInputConnection, MidiInputPort};
8use mimium_lang::{
9    ast::{Expr, Literal},
10    function,
11    interner::{ToSymbol, TypeNodeId},
12    interpreter::Value,
13    log, numeric,
14    plugin::{SysPluginSignature, SystemPlugin, SystemPluginFnType, SystemPluginMacroType},
15    runtime::vm,
16    string_t,
17    types::{PType, RecordTypeField, Type},
18    unit,
19};
20use std::{
21    cell::OnceCell,
22    sync::{Arc, atomic::Ordering},
23};
24use wmidi::MidiMessage;
25
26type NoteCallBack = Arc<dyn Fn(f64, f64) + Send + Sync>;
27
28#[derive(Default)]
29struct NoteCallBacks(pub [Vec<NoteCallBack>; 16]);
30
31impl NoteCallBacks {
32    pub fn invoke_note_callback(&self, chan: u8, note: u8, vel: u8) {
33        if chan < 15 {
34            self.0[chan as usize]
35                .iter()
36                .for_each(|cb| cb(note as f64, vel as f64));
37        };
38    }
39}
40
41struct NoteCell {
42    channel: u8,
43    pitch: AtomicF64,
44    velocity: AtomicF64,
45}
46/// Main module for Midi Plugin.
47pub struct MidiPlugin {
48    input: Option<MidiInput>,
49    port: OnceCell<MidiInputPort>,
50    port_name: Option<String>,
51    note_callbacks: Option<NoteCallBacks>,
52    connection: Option<MidiInputConnection<NoteCallBacks>>,
53    // New fields for macro-based MIDI note handling
54    midi_note_cells: Vec<Arc<NoteCell>>,
55    midi_note_channels: Vec<u8>,
56}
57
58impl MidiPlugin {
59    const GET_MIDI_NOTE: &'static str = "__get_midi_note";
60
61    pub fn try_new() -> Option<Self> {
62        let input_res = MidiInput::new("mimium midi plugin");
63        match input_res {
64            Ok(input) => Some(Self {
65                input: Some(input),
66                port: OnceCell::new(),
67                port_name: None,
68                note_callbacks: Some(Default::default()),
69                connection: None,
70                midi_note_cells: Vec::new(),
71                midi_note_channels: Vec::new(),
72            }),
73            Err(_e) => None,
74        }
75    }
76    fn add_note_callback(&mut self, chan: u8, cb: NoteCallBack) {
77        match self.note_callbacks.as_mut() {
78            Some(v) if chan < 15 => {
79                v.0[chan as usize].push(cb);
80            }
81            _ => {}
82        }
83    }
84    /// This function is exposed to mimium as "set_midi_port(port:string)".
85    /// Until this function is called, MIDI plugin tries to the default device.
86    pub fn set_midi_port(&mut self, vm: &mut vm::Machine) -> vm::ReturnCode {
87        let idx = vm.get_stack(0);
88        let pname = vm.prog.strings[idx as usize].clone();
89
90        self.port_name = Some(pname);
91        0
92    }
93    /// Macro function for midi_note_mono! that generates unique IDs and returns runtime code
94    /// Arguments: channel:float[0-15], default_note:float[0-127], default_velocity:float[0-127]
95    /// Returns: Code that evaluates to a record {pitch:float, velocity:float}
96    pub fn midi_note_mono_macro(&mut self, v: &[(Value, TypeNodeId)]) -> Value {
97        let zero = Value::Code(
98            Expr::Literal(Literal::Float(0.0.to_string().to_symbol())).into_id_without_span(),
99        );
100        if v.len() != 3 {
101            log::error!(
102                "midi_note_mono! expects 3 arguments (channel, default_note, default_velocity)"
103            );
104            return zero;
105        }
106
107        let (ch, default_note, default_vel) = match (v[0].0.clone(), v[1].0.clone(), v[2].0.clone())
108        {
109            (Value::Number(ch), Value::Number(note), Value::Number(vel)) => (ch, note, vel),
110            _ => {
111                log::error!("midi_note_mono! arguments must be numbers");
112                return zero;
113            }
114        };
115        let (uid, cell) = if let Some((uid, cell)) = self
116            .midi_note_cells
117            .iter()
118            .enumerate()
119            .find(|(_, c)| c.channel == ch as u8)
120        {
121            (uid, cell.clone())
122        } else {
123            let cell = Arc::new(NoteCell {
124                channel: ch as u8,
125                pitch: AtomicF64::new(default_note),
126                velocity: AtomicF64::new(default_vel),
127            });
128            let uid = self.midi_note_cells.len();
129            self.midi_note_cells.push(cell.clone());
130            (uid, cell)
131        };
132
133        // Register the callback
134        let cell_c = cell.clone();
135        self.add_note_callback(
136            ch as u8,
137            Arc::new(move |note, vel| {
138                cell_c.pitch.store(note, Ordering::Relaxed);
139                cell_c.velocity.store(vel, Ordering::Relaxed);
140            }),
141        );
142
143        // Generate code that calls __get_midi_note(uid)
144        Value::Code(
145            Expr::Apply(
146                Expr::Var(Self::GET_MIDI_NOTE.to_symbol()).into_id_without_span(),
147                vec![
148                    Expr::Literal(Literal::Float(uid.to_string().to_symbol()))
149                        .into_id_without_span(),
150                ],
151            )
152            .into_id_without_span(),
153        )
154    }
155
156    /// Runtime function to get MIDI note values by UID
157    /// Arguments: uid:float (index into midi_note_cells)
158    /// Returns: record {pitch:float, velocity:float}
159    pub fn get_midi_note(&mut self, vm: &mut vm::Machine) -> vm::ReturnCode {
160        let uid = vm::Machine::get_as::<f64>(vm.get_stack(0)) as usize;
161
162        match self.midi_note_cells.get(uid) {
163            Some(cell) => {
164                let pitch = cell.pitch.load(Ordering::Relaxed);
165                let velocity = cell.velocity.load(Ordering::Relaxed);
166                // Return as a tuple for now (records are represented as tuples in the VM)
167                vm.set_stack(0, vm::Machine::to_value(pitch));
168                vm.set_stack(1, vm::Machine::to_value(velocity));
169                2
170            }
171            None => {
172                log::error!("Invalid MIDI note UID: {uid}");
173                vm.set_stack(0, vm::Machine::to_value(0.0));
174                vm.set_stack(1, vm::Machine::to_value(0.0));
175                2
176            }
177        }
178    }
179}
180
181impl Drop for MidiPlugin {
182    fn drop(&mut self) {
183        if let Some(c) = self.connection.take() {
184            c.close();
185        }
186    }
187}
188
189impl SystemPlugin for MidiPlugin {
190    fn after_main(&mut self, _machine: &mut vm::Machine) -> vm::ReturnCode {
191        if self.connection.is_some() {
192            return 0;
193        }
194        let input = self.input.as_ref().unwrap();
195        let ports = input.ports();
196
197        let port_opt = match (&self.port_name, ports.is_empty()) {
198            (Some(pname), false) => ports.iter().find(|port| {
199                let name = input.port_name(port).unwrap_or_default();
200                &name == pname
201            }),
202            (None, false) => {
203                log::info!("trying to connect default MIDI input device...");
204                ports.first()
205            }
206            (_, true) => None,
207        };
208        if let Some(p) = port_opt {
209            let name = input.port_name(p).unwrap_or_default();
210            log::info!("Midi Input: Connected to {name}");
211            let res = self.input.take().unwrap().connect(
212                p,
213                &name,
214                |_stamp, message, cbs: &mut NoteCallBacks| {
215                    let msg = MidiMessage::from_bytes(message);
216                    if let Ok(m) = msg {
217                        match m {
218                            MidiMessage::NoteOff(channel, note, _vel) => {
219                                cbs.invoke_note_callback(channel.index(), u8::from(note), 0);
220                            }
221                            MidiMessage::NoteOn(channel, note, vel) => {
222                                cbs.invoke_note_callback(
223                                    channel.index(),
224                                    u8::from(note),
225                                    vel.into(),
226                                );
227                            }
228                            _ => {}
229                        }
230                    }
231                },
232                self.note_callbacks.take().unwrap(),
233            );
234            match res {
235                Ok(c) => self.connection = Some(c),
236                Err(e) => {
237                    log::error!("{e}")
238                }
239            }
240            let _ = self.port.set(p.clone());
241        } else {
242            log::warn!("No MIDI devices found.")
243        }
244        0
245    }
246
247    fn gen_interfaces(&self) -> Vec<SysPluginSignature> {
248        // set_midi_port function
249        let ty = function!(vec![string_t!()], unit!());
250        let fun: SystemPluginFnType<Self> = Self::set_midi_port;
251        let setport = SysPluginSignature::new("set_midi_port", fun, ty);
252
253        // New midi_note_mono! macro (returns code that evaluates to record)
254        let midi_note_macro_f: SystemPluginMacroType<Self> = Self::midi_note_mono_macro;
255        let record_ty = Type::Record(vec![
256            RecordTypeField::new("pitch".to_symbol(), numeric!(), false),
257            RecordTypeField::new("velocity".to_symbol(), numeric!(), false),
258        ])
259        .into_id();
260        let midi_note_macro = SysPluginSignature::new_macro(
261            "midi_note_mono",
262            midi_note_macro_f,
263            function!(
264                vec![numeric!(), numeric!(), numeric!()],
265                Type::Code(record_ty).into_id()
266            ),
267        );
268
269        // Runtime function __get_midi_note (returns record)
270        let get_midi_note_f: SystemPluginFnType<Self> = Self::get_midi_note;
271        let get_midi_note = SysPluginSignature::new(
272            Self::GET_MIDI_NOTE,
273            get_midi_note_f,
274            function!(vec![numeric!()], record_ty),
275        );
276
277        vec![setport, midi_note_macro, get_midi_note]
278    }
279}