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}