Skip to main content

moont_web/
lib.rs

1// Copyright (C) 2021-2026 Geoff Hill <geoff@geoffhill.org>
2//
3// This program is free software: you can redistribute it and/or modify it
4// under the terms of the GNU Lesser General Public License as published by
5// the Free Software Foundation, either version 2.1 of the License, or (at
6// your option) any later version. Read COPYING.LESSER.txt for details.
7
8//! WebAssembly wrapper for the moont CM-32L synthesizer.
9//!
10//! Provides a [`CM32LWeb`] struct that hooks into the Web Audio API
11//! via `ScriptProcessorNode` for real-time audio output at 32 kHz.
12
13use std::cell::RefCell;
14use std::rc::Rc;
15
16use moont::{CM32L, Frame, Synth, smf};
17use wasm_bindgen::prelude::*;
18use web_sys::{
19    AudioContext, AudioContextOptions, AudioProcessingEvent,
20    ScriptProcessorNode,
21};
22
23const BUFFER_SIZE: u32 = 1024;
24const SAMPLE_RATE: f32 = moont::SAMPLE_RATE as f32;
25
26struct Inner {
27    synth: CM32L,
28    smf_events: Vec<smf::Event>,
29    smf_index: usize,
30    smf_start: u32,
31    smf_duration: u32,
32}
33
34impl Inner {
35    fn feed_smf(&mut self, deadline: u32) {
36        while self.smf_index < self.smf_events.len()
37            && self.smf_events[self.smf_index].time() <= deadline
38        {
39            match &self.smf_events[self.smf_index] {
40                smf::Event::Msg { time, msg } => {
41                    self.synth.play_msg_at(*msg, *time);
42                }
43                smf::Event::Sysex { time, data } => {
44                    self.synth.play_sysex_at(data, *time);
45                }
46            }
47            self.smf_index += 1;
48        }
49    }
50}
51
52/// CM-32L synthesizer with Web Audio output.
53#[wasm_bindgen]
54pub struct CM32LWeb {
55    inner: Rc<RefCell<Inner>>,
56    _ctx: AudioContext,
57    _processor: ScriptProcessorNode,
58    _closure: Closure<dyn FnMut(AudioProcessingEvent)>,
59}
60
61fn setup(synth: CM32L) -> Result<CM32LWeb, JsValue> {
62    let inner = Rc::new(RefCell::new(Inner {
63        synth,
64        smf_events: Vec::new(),
65        smf_index: 0,
66        smf_start: 0,
67        smf_duration: 0,
68    }));
69
70    let opts = AudioContextOptions::new();
71    opts.set_sample_rate(SAMPLE_RATE);
72    let ctx = AudioContext::new_with_context_options(&opts)?;
73
74    let processor = ctx.create_script_processor_with_buffer_size_and_number_of_input_channels_and_number_of_output_channels(
75        BUFFER_SIZE, 0, 2,
76    )?;
77
78    let inner_ref = inner.clone();
79    let closure = Closure::wrap(Box::new(move |event: AudioProcessingEvent| {
80        let buf = event.output_buffer().unwrap();
81        let len = buf.length() as usize;
82        let mut inner = inner_ref.borrow_mut();
83        let current = inner.synth.current_time();
84        inner.feed_smf(current + len as u32);
85        let mut frames = vec![Frame(0, 0); len];
86        inner.synth.render(&mut frames);
87        drop(inner);
88
89        let mut left = vec![0.0f32; len];
90        let mut right = vec![0.0f32; len];
91        for (i, f) in frames.iter().enumerate() {
92            left[i] = f.0 as f32 / 32768.0;
93            right[i] = f.1 as f32 / 32768.0;
94        }
95        buf.copy_to_channel(&left, 0).unwrap();
96        buf.copy_to_channel(&right, 1).unwrap();
97    }) as Box<dyn FnMut(AudioProcessingEvent)>);
98
99    processor.set_onaudioprocess(Some(closure.as_ref().unchecked_ref()));
100    processor.connect_with_audio_node(&ctx.destination())?;
101
102    Ok(CM32LWeb {
103        inner,
104        _ctx: ctx,
105        _processor: processor,
106        _closure: closure,
107    })
108}
109
110#[wasm_bindgen]
111impl CM32LWeb {
112    /// Creates a new CM-32L synthesizer using the bundled ROM.
113    ///
114    /// Requires the `bundle-rom` feature to be enabled.
115    #[cfg(feature = "bundle-rom")]
116    #[wasm_bindgen(constructor)]
117    pub fn new() -> Result<CM32LWeb, JsValue> {
118        let synth = CM32L::new(moont::Rom::bundled());
119        setup(synth)
120    }
121
122    /// Creates a new CM-32L synthesizer from dynamically loaded ROM data.
123    pub fn from_rom(
124        control_rom: &[u8],
125        pcm_rom: &[u8],
126    ) -> Result<CM32LWeb, JsValue> {
127        let rom = moont::Rom::new(control_rom, pcm_rom)
128            .map_err(|e| JsValue::from_str(&format!("{e:?}")))?;
129        let synth = CM32L::new(rom);
130        setup(synth)
131    }
132
133    /// Plays a MIDI channel message from raw bytes (1-3 bytes).
134    ///
135    /// Returns `true` if the message was successfully enqueued.
136    pub fn play_midi(&self, data: &[u8]) -> bool {
137        if data.is_empty() || data.len() > 3 {
138            return false;
139        }
140        let mut msg: u32 = 0;
141        for (i, &b) in data.iter().enumerate() {
142            msg |= (b as u32) << (i * 8);
143        }
144        self.inner.borrow_mut().synth.play_msg(msg)
145    }
146
147    /// Plays a SysEx message from raw bytes.
148    ///
149    /// Returns `true` if the message was successfully enqueued.
150    pub fn play_sysex(&self, data: &[u8]) -> bool {
151        self.inner.borrow_mut().synth.play_sysex(data)
152    }
153
154    /// Loads an SMF (Standard MIDI File) for incremental playback.
155    ///
156    /// Events are fed to the synthesizer incrementally during audio
157    /// rendering. All event times are offset by the current synth time
158    /// so playback starts immediately. Returns the total duration in
159    /// seconds, or throws on parse error.
160    pub fn load_smf(&self, data: &[u8]) -> Result<f64, JsValue> {
161        let events =
162            smf::parse(data).map_err(|e| JsValue::from_str(&format!("{e}")))?;
163        let last = events.last().map(|e| e.time()).unwrap_or(0);
164        let tail = 2 * SAMPLE_RATE as u32;
165        let mut inner = self.inner.borrow_mut();
166        let offset = inner.synth.current_time();
167        let shifted: Vec<smf::Event> = events
168            .into_iter()
169            .map(|e| match e {
170                smf::Event::Msg { time, msg } => smf::Event::Msg {
171                    time: time + offset,
172                    msg,
173                },
174                smf::Event::Sysex { time, data } => smf::Event::Sysex {
175                    time: time + offset,
176                    data,
177                },
178            })
179            .collect();
180        inner.smf_events = shifted;
181        inner.smf_index = 0;
182        inner.smf_start = offset;
183        inner.smf_duration = last + tail;
184        Ok(inner.smf_duration as f64 / SAMPLE_RATE as f64)
185    }
186
187    /// Stops SMF playback and clears loaded events.
188    pub fn stop_smf(&self) {
189        let mut inner = self.inner.borrow_mut();
190        inner.smf_events.clear();
191        inner.smf_index = 0;
192        inner.smf_duration = 0;
193    }
194
195    /// Returns elapsed playback time of the current SMF in seconds.
196    pub fn smf_elapsed(&self) -> f64 {
197        let inner = self.inner.borrow();
198        let elapsed =
199            inner.synth.current_time().saturating_sub(inner.smf_start);
200        elapsed as f64 / SAMPLE_RATE as f64
201    }
202
203    /// Returns the current playback position in seconds.
204    pub fn current_time(&self) -> f64 {
205        self.inner.borrow().synth.current_time() as f64 / SAMPLE_RATE as f64
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    #[test]
212    fn test_midi_packing() {
213        let data: &[u8] = &[0x90, 0x3C, 0x7F];
214        let mut msg: u32 = 0;
215        for (i, &b) in data.iter().enumerate() {
216            msg |= (b as u32) << (i * 8);
217        }
218        assert_eq!(msg, 0x007F3C90);
219    }
220}