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 = 32000.0;
25
26struct Inner {
27    synth: CM32L,
28    smf_events: Vec<smf::Event>,
29    smf_index: usize,
30}
31
32impl Inner {
33    fn feed_smf(&mut self, deadline: u32) {
34        while self.smf_index < self.smf_events.len()
35            && self.smf_events[self.smf_index].time() <= deadline
36        {
37            match &self.smf_events[self.smf_index] {
38                smf::Event::Msg { time, msg } => {
39                    self.synth.play_msg_at(*msg, *time);
40                }
41                smf::Event::Sysex { time, data } => {
42                    self.synth.play_sysex_at(data, *time);
43                }
44            }
45            self.smf_index += 1;
46        }
47    }
48}
49
50/// CM-32L synthesizer with Web Audio output.
51#[wasm_bindgen]
52pub struct CM32LWeb {
53    inner: Rc<RefCell<Inner>>,
54    _ctx: AudioContext,
55    _processor: ScriptProcessorNode,
56    _closure: Closure<dyn FnMut(AudioProcessingEvent)>,
57}
58
59fn setup(synth: CM32L) -> Result<CM32LWeb, JsValue> {
60    let inner = Rc::new(RefCell::new(Inner {
61        synth,
62        smf_events: Vec::new(),
63        smf_index: 0,
64    }));
65
66    let opts = AudioContextOptions::new();
67    opts.set_sample_rate(SAMPLE_RATE);
68    let ctx = AudioContext::new_with_context_options(&opts)?;
69
70    let processor = ctx.create_script_processor_with_buffer_size_and_number_of_input_channels_and_number_of_output_channels(
71        BUFFER_SIZE, 0, 2,
72    )?;
73
74    let inner_ref = inner.clone();
75    let closure = Closure::wrap(Box::new(move |event: AudioProcessingEvent| {
76        let buf = event.output_buffer().unwrap();
77        let len = buf.length() as usize;
78        let mut inner = inner_ref.borrow_mut();
79        let current = inner.synth.current_time();
80        inner.feed_smf(current + len as u32);
81        let mut frames = vec![Frame(0, 0); len];
82        inner.synth.render(&mut frames);
83        drop(inner);
84
85        let mut left = vec![0.0f32; len];
86        let mut right = vec![0.0f32; len];
87        for (i, f) in frames.iter().enumerate() {
88            left[i] = f.0 as f32 / 32768.0;
89            right[i] = f.1 as f32 / 32768.0;
90        }
91        buf.copy_to_channel(&left, 0).unwrap();
92        buf.copy_to_channel(&right, 1).unwrap();
93    }) as Box<dyn FnMut(AudioProcessingEvent)>);
94
95    processor.set_onaudioprocess(Some(closure.as_ref().unchecked_ref()));
96    processor.connect_with_audio_node(&ctx.destination())?;
97
98    Ok(CM32LWeb {
99        inner,
100        _ctx: ctx,
101        _processor: processor,
102        _closure: closure,
103    })
104}
105
106#[wasm_bindgen]
107impl CM32LWeb {
108    /// Creates a new CM-32L synthesizer using the bundled ROM.
109    ///
110    /// Requires the `bundle-rom` feature to be enabled.
111    #[cfg(feature = "bundle-rom")]
112    #[wasm_bindgen(constructor)]
113    pub fn new() -> Result<CM32LWeb, JsValue> {
114        let synth = CM32L::new(moont::Rom::bundled());
115        setup(synth)
116    }
117
118    /// Creates a new CM-32L synthesizer from dynamically loaded ROM data.
119    pub fn from_rom(
120        control_rom: &[u8],
121        pcm_rom: &[u8],
122    ) -> Result<CM32LWeb, JsValue> {
123        let rom = moont::Rom::new(control_rom, pcm_rom)
124            .map_err(|e| JsValue::from_str(&format!("{e:?}")))?;
125        let synth = CM32L::new(rom);
126        setup(synth)
127    }
128
129    /// Plays a MIDI channel message from raw bytes (1-3 bytes).
130    ///
131    /// Returns `true` if the message was successfully enqueued.
132    pub fn play_midi(&self, data: &[u8]) -> bool {
133        if data.is_empty() || data.len() > 3 {
134            return false;
135        }
136        let mut msg: u32 = 0;
137        for (i, &b) in data.iter().enumerate() {
138            msg |= (b as u32) << (i * 8);
139        }
140        self.inner.borrow_mut().synth.play_msg(msg)
141    }
142
143    /// Plays a SysEx message from raw bytes.
144    ///
145    /// Returns `true` if the message was successfully enqueued.
146    pub fn play_sysex(&self, data: &[u8]) -> bool {
147        self.inner.borrow_mut().synth.play_sysex(data)
148    }
149
150    /// Loads an SMF (Standard MIDI File) for incremental playback.
151    ///
152    /// Events are fed to the synthesizer incrementally during audio
153    /// rendering. Returns the total duration in seconds, or throws
154    /// on parse error.
155    pub fn load_smf(&self, data: &[u8]) -> Result<f64, JsValue> {
156        let events =
157            smf::parse(data).map_err(|e| JsValue::from_str(&format!("{e}")))?;
158        let last = events.last().map(|e| e.time()).unwrap_or(0);
159        let total = last + 2 * SAMPLE_RATE as u32;
160        let mut inner = self.inner.borrow_mut();
161        inner.smf_events = events;
162        inner.smf_index = 0;
163        Ok(total as f64 / SAMPLE_RATE as f64)
164    }
165
166    /// Returns the current playback position in seconds.
167    pub fn current_time(&self) -> f64 {
168        self.inner.borrow().synth.current_time() as f64 / SAMPLE_RATE as f64
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    #[test]
175    fn test_midi_packing() {
176        let data: &[u8] = &[0x90, 0x3C, 0x7F];
177        let mut msg: u32 = 0;
178        for (i, &b) in data.iter().enumerate() {
179            msg |= (b as u32) << (i * 8);
180        }
181        assert_eq!(msg, 0x007F3C90);
182    }
183}