Skip to main content

moont/
lib.rs

1// Copyright (C) 2021-2026 Geoff Hill <geoff@geoffhill.org>
2// Copyright (C) 2003-2026 Dean Beeler, Jerome Fisher, Sergey V. Mikayev
3//
4// This program is free software: you can redistribute it and/or modify it
5// under the terms of the GNU Lesser General Public License as published by
6// the Free Software Foundation, either version 2.1 of the License, or (at
7// your option) any later version. Read COPYING.LESSER.txt for details.
8
9//! Roland CM-32L synthesizer emulator.
10//!
11//! Renders 32kHz stereo PCM audio from MIDI input. Based on
12//! [Munt](https://github.com/munt/munt/), with no external dependencies.
13//!
14//! The CM-32L is in the [MT-32 family](https://en.wikipedia.org/wiki/Roland_MT-32)
15//! of synthesizers. This hardware predates
16//! [General MIDI](https://en.wikipedia.org/wiki/General_MIDI), but
17//! [many early 1990s PC games][vogons] directly support these synths.
18//!
19//! [vogons]: https://www.vogonswiki.com/index.php/List_of_MT-32-compatible_computer_games
20//!
21//! # Quick start
22//!
23//! ```no_run
24//! use moont::{CM32L, Frame, Rom, Synth};
25//!
26//! let control = std::fs::read("CM32L_CONTROL.ROM").unwrap();
27//! let pcm = std::fs::read("CM32L_PCM.ROM").unwrap();
28//! let rom = Rom::new(&control, &pcm).expect("invalid ROM");
29//! let mut synth = CM32L::new(rom);
30//!
31//! // Note On: channel 1, middle C, max velocity.
32//! synth.play_msg(0x7f3c91);
33//!
34//! // Render 1 second of audio at 32 kHz.
35//! let mut buf = vec![Frame(0, 0); 32000];
36//! synth.render(&mut buf);
37//!
38//! // Note Off.
39//! synth.play_msg(0x3c81);
40//! ```
41//!
42//! # Loading ROMs
43//!
44//! The CM-32L ROMs are not distributed with this crate. Load them at
45//! runtime with [`Rom::new`]:
46//!
47//! ```no_run
48//! # use moont::Rom;
49//! let control = std::fs::read("CM32L_CONTROL.ROM").unwrap();
50//! let pcm = std::fs::read("CM32L_PCM.ROM").unwrap();
51//! let rom = Rom::new(&control, &pcm).expect("invalid ROM");
52//! ```
53//!
54//! With the **`bundle-rom`** feature, ROMs are parsed at compile time and
55//! embedded in the binary, enabling `Rom::bundled()`:
56//!
57//! ```ignore
58//! let rom = Rom::bundled();
59//! ```
60//!
61//! # General MIDI translation
62//!
63//! The CM-32L uses its own instrument map. When the input is General MIDI,
64//! use [`GmSynth`] to translate program changes, drum notes, and bank
65//! selects into CM-32L equivalents:
66//!
67//! ```no_run
68//! use moont::{GmSynth, Rom, Synth};
69//!
70//! let control = std::fs::read("CM32L_CONTROL.ROM").unwrap();
71//! let pcm = std::fs::read("CM32L_PCM.ROM").unwrap();
72//! let mut synth = GmSynth::new(Rom::new(&control, &pcm).unwrap());
73//! synth.play_msg(0x7f3c91);
74//! ```
75//!
76//! # Type-safe MIDI messages
77//!
78//! The [`midi::Message`] type provides validated message construction as
79//! an alternative to raw `u32` values:
80//!
81//! ```
82//! use moont::midi::Message;
83//!
84//! let msg = Message::note_on(60, 100, 0).unwrap();
85//! let packed: u32 = msg.try_into().unwrap();
86//! ```
87//!
88//! # Features
89//!
90//! | Feature | Description |
91//! |---------|-------------|
92//! | **`bundle-rom`** | Embed pre-parsed ROMs in the binary (enables `Rom::bundled()`) |
93//! | **`tracing`** | Emit [`tracing`](https://docs.rs/tracing) spans and events |
94//!
95//! # Related crates
96//!
97//! | Crate | Description |
98//! |-------|-------------|
99//! | [`moont-render`](https://docs.rs/moont-render) | Render .mid files to .wav |
100//! | [`moont-live`](https://docs.rs/moont-live) | Real-time ALSA MIDI sink |
101//! | [`moont-web`](https://docs.rs/moont-web) | WebAssembly wrapper with Web Audio API |
102
103#[cfg(feature = "tracing")]
104macro_rules! trace {
105    ($($arg:tt)*) => { ::tracing::trace!($($arg)*) };
106}
107#[cfg(not(feature = "tracing"))]
108macro_rules! trace {
109    ($($arg:tt)*) => {};
110}
111
112#[cfg(feature = "tracing")]
113macro_rules! debug {
114    ($($arg:tt)*) => { ::tracing::debug!($($arg)*) };
115}
116#[cfg(not(feature = "tracing"))]
117macro_rules! debug {
118    ($($arg:tt)*) => {};
119}
120
121#[cfg(feature = "tracing")]
122macro_rules! info {
123    ($($arg:tt)*) => { ::tracing::info!($($arg)*) };
124}
125#[cfg(not(feature = "tracing"))]
126macro_rules! info {
127    ($($arg:tt)*) => {};
128}
129
130#[cfg(feature = "tracing")]
131macro_rules! warn {
132    ($($arg:tt)*) => { ::tracing::warn!($($arg)*) };
133}
134#[cfg(not(feature = "tracing"))]
135macro_rules! warn {
136    ($($arg:tt)*) => {};
137}
138
139#[cfg(feature = "tracing")]
140macro_rules! debug_span {
141    ($($arg:tt)*) => { ::tracing::debug_span!($($arg)*).entered() };
142}
143#[cfg(not(feature = "tracing"))]
144macro_rules! debug_span {
145    ($($arg:tt)*) => {
146        ()
147    };
148}
149
150mod dispatch;
151mod element;
152pub mod gm;
153mod lpf;
154pub mod midi;
155mod midiqueue;
156pub mod param;
157mod pcm;
158mod ramp;
159mod render;
160mod reverb;
161pub mod rom;
162pub mod smf;
163mod synth;
164mod sysex;
165mod tables;
166mod tva;
167mod tvf;
168mod tvp;
169
170#[cfg(feature = "bundle-rom")]
171mod bundle;
172
173pub use gm::GmSynth;
174pub use rom::Rom;
175
176/// Audio frames (stereo samples) per second.
177pub const SAMPLE_RATE: u32 = 32000;
178
179#[cfg(feature = "bundle-rom")]
180impl Rom {
181    /// Returns the bundled ROM (pre-parsed at compile time).
182    ///
183    /// Each call returns a new [`Rom`] containing references to the
184    /// embedded ROM data. This is a cheap operation (two pointers).
185    ///
186    /// Requires the **`bundle-rom`** feature and CM-32L ROM files at
187    /// `testdata/rom/` during compilation.
188    pub const fn bundled() -> Rom {
189        bundle::bundled_rom()
190    }
191}
192
193use element::{PartArena, PartialArena, PolyArena};
194use lpf::CoarseLpf;
195use midiqueue::MidiQueue;
196use reverb::Reverb;
197use sysex::MemState;
198
199/// Stereo PCM audio frame of two (left, right) 16-bit little-endian samples.
200#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
201#[repr(C)]
202pub struct Frame(pub i16, pub i16);
203
204/// Interface for MIDI synthesizers.
205///
206/// MIDI messages must be added in time-ascending order, otherwise they will
207/// be ignored during rendering.
208pub trait Synth {
209    /// Returns the number of frames rendered so far.
210    fn current_time(&self) -> u32;
211
212    /// Enqueues one MIDI message at a specific frame.
213    ///
214    /// A MIDI baud rate transfer delay is added to `time` before
215    /// enqueuing: 24 frames for 3-byte messages, 16 frames for
216    /// 2-byte messages (program change and channel pressure).
217    fn play_msg_at(&mut self, msg: u32, time: u32) -> bool;
218
219    /// Enqueues one MIDI message starting from the current frame.
220    ///
221    /// Equivalent to `play_msg_at(msg, current_time())`. The message
222    /// is processed after the MIDI baud rate transfer delay (16 or
223    /// 24 frames), not at the current frame itself.
224    fn play_msg(&mut self, msg: u32) -> bool {
225        self.play_msg_at(msg, self.current_time())
226    }
227
228    /// Enqueues one MIDI System Exclusive message at a specific frame.
229    ///
230    /// No transfer delay is added; the message is processed at
231    /// exactly the given frame.
232    fn play_sysex_at(&mut self, sysex: &[u8], time: u32) -> bool;
233
234    /// Enqueues one MIDI System Exclusive message at the current frame.
235    ///
236    /// Equivalent to `play_sysex_at(sysex, current_time())`. The
237    /// message is processed immediately at the current frame.
238    fn play_sysex(&mut self, sysex: &[u8]) -> bool {
239        self.play_sysex_at(sysex, self.current_time())
240    }
241
242    /// Renders audio samples into a slice of frames.
243    ///
244    /// The output slice is filled with stereo audio frames. Each [`Frame`]
245    /// is comprised of two 16-bit, native-endian, signed PCM audio samples.
246    ///
247    /// The internal clock is advanced by the number of frames rendered.
248    fn render(&mut self, out: &mut [Frame]);
249}
250
251/// CM-32L synthesizer state.
252#[derive(Debug)]
253pub struct CM32L {
254    time: u32,
255    rom: Rom,
256    mem: Box<MemState>,
257    midi_queue: MidiQueue,
258    parts: PartArena,
259    free_polys: PolyArena,
260    free_partials: PartialArena,
261    reverb: Reverb,
262    lpf_left: CoarseLpf,
263    lpf_right: CoarseLpf,
264    last_midi_timestamp: u32,
265    chantable: [[u8; 9]; 16],
266    aborting_part_ix: usize,
267    master_tune_pitch_delta: i32,
268}
269
270impl CM32L {
271    /// Creates a new synthesizer instance.
272    ///
273    /// Use the bundled ROM or load ROMs at runtime:
274    /// - `CM32L::new(Rom::bundled())` - uses bundled ROM
275    /// - `CM32L::new(Rom::new(ctrl, pcm)?)` - for dynamically loaded ROMs
276    pub fn new(rom: Rom) -> CM32L {
277        let reverb = Reverb::new();
278        let mem = MemState::new(&rom);
279        let master_volume = mem.master_volume;
280        let parts = PartArena::new(master_volume, &rom);
281        let free_partials = PartialArena::new(&rom.meta().reserve_settings);
282        let mut chantable = [[0xFF; 9]; 16];
283        dispatch::rebuild_chantable(&mut chantable, &mem.raw_system);
284        CM32L {
285            time: 0,
286            rom,
287            mem,
288            midi_queue: MidiQueue::new(),
289            parts,
290            free_polys: PolyArena::new(),
291            free_partials,
292            reverb,
293            lpf_left: CoarseLpf::new(),
294            lpf_right: CoarseLpf::new(),
295            last_midi_timestamp: 0,
296            chantable,
297            aborting_part_ix: 0,
298            master_tune_pitch_delta: 0,
299        }
300    }
301}