oplon
Yamaha YM3812 / YMF262 (OPL2 / OPL3) FM synthesis core for Rust — no_std, integer-only, clean-room
oplon reproduces the sound of the AdLib / Sound Blaster OPL chip: up to 18 two-operator FM channels plus the rhythm/percussion mode, summed to stereo. It powers up as an OPL2 (9 channels, mono, waveforms 0-3) and becomes a full OPL3 — 18 channels, stereo panning, waveforms 0-7 — the moment OPL3 mode is enabled (port-1 register 0x05, or set_opl3_enabled), exactly like the real YMF262. So OPL2 code paths stay bit-for-bit unchanged while OPL3 streams get the extra features. It is designed to run on embedded targets, real-time audio threads, and any context where heap allocation and floating-point are unavailable or undesirable.
The operators run at the chip's native rate (OPL2 clock / 72, OPL3 clock / 288, ≈49716 Hz for the canonical clocks), and the output is 4-point-cubic-resampled to your sample rate — so the chip's own harmonic aliasing matches the silicon instead of folding at the wrong Nyquist.
It is the OPL counterpart of sidera (the SID engine), and ships the same way: a no_std core library, plus an optional CLI player behind a feature flag (sidplay there → vgmplay here).
Fidelity: verified against two independent OPL3 emulators used purely as black-box acoustic oracles — the DOSBox core (PyOPL/dbopl) and Nuked-OPL3 (the cycle-accurate reference). Driving both with the same register stream from real Doom (id Software) VGMs gives a median spectral cosine ≈ 0.99 and RMS-envelope correlation ≈ 0.998; the operator output is calibrated to the hardware ±4084 scale, so levels sit within a few tenths of a dB of Nuked. The 4-operator algorithms match the reference near-exactly for parallel/single-modulation connections (correlation ≈ 0.994–0.999); a deep serial FM chain (algorithm 0) is bit-faithful sample-for-sample at onset but, being a 3-stage non-linear chain, decorrelates from the reference over time under sub-LSB rounding differences while staying perceptually equivalent (matched envelope, brightness and level). The OPL2 path is exercised by an exhaustive waveform equality test and a render golden hash.
Properties
| Property | Value |
|---|---|
no_std |
✅ |
| Heap allocation | ❌ none |
| Floating-point | ❌ none |
unsafe |
❌ #![forbid(unsafe_code)] |
| Channels | up to 18 × 2-operator FM (two banks) + rhythm mode |
| Waveforms | all 8 OPL3 (sine, half, abs, quarter, even ×2, square, log-saw) |
| Envelope | 8-bit ADSR, cycle-accurate rate table + dither |
| LFOs | tremolo (AM) + vibrato (PM), documented shapes |
| Stereo panning | OPL3, register 0xC0 bits 4/5 |
| 4-operator mode | ✅ all 4 connection algorithms (reg 0x104) |
| ROM tables | generated at compile time, integer-only, ±1 LSB |
| Output | stereo i32 per frame (per-operator ±4084) |
Two interfaces
oplon::Opl2 exposes both layers; use whichever fits.
Register level — what a VGM / DRO / ported driver needs
use Opl2;
let mut opl = new; // output sample rate
opl.write_reg; // op0 mult = 1
opl.write_reg; // op0 total level = loud
opl.write_reg; // op0 fast attack
opl.write_reg; // op0 sustain/release
opl.write_reg; // F-number low
opl.write_reg; // key-on + block 4 + F-number high
let = opl.render_frame; // one stereo frame
For a non-standard master clock (VGM streams may declare one), use
Opl2::with_chip_clock(clock_hz, output_rate) — pitch scales with the clock.
Structured note/patch level
use ;
let mut opl = new;
opl.set_patch;
opl.set_frequency; // F-number / block
opl.key_on;
let = opl.render_frame;
The vgmplay binary
An optional CLI that plays VGM and DRO (DOSBox Raw OPL) register logs through the engine — the OPL analogue of sidera's sidplay.
Gzip-compressed .vgz files are decompressed transparently, and .dro files (DOSBox Raw OPL, both v1 and v2) are transcoded to VGM in memory so the same player path handles them. Scope is the OPL family: YM3812 (OPL2, VGM command 0x5A), YM3526 (OPL1, 0x5B), Y8950 (0x5C, melodic part) and the YMF262 (OPL3, both banks — port 0 0x5E and port 1 0x5F) — they share the register map. A YMF262 stream enables OPL3 mode (18 channels, stereo, waveforms 4-7, 4-operator linking) and is clocked at clock / 288. Other chips' commands are only skipped for timing.
--regdump writes the OPL register-write timeline as CSV (sample,reg,val) — the ground truth for comparing a reimplemented driver against the log.
Clean-room & attribution
MIT-licensed. The OPL emulators oplon is validated against are not: Schism Tracker's player/fmopl3.c (a ymf262 core) is GPL-2.0+, © Jarek Burczynski, and Nuked-OPL3 is LGPL. No code or data table is copied from either. The log-sin / exp ROMs are generated from their defining formulas, the envelope-increment table from the documented (4 + lo)·2^rate_hi rate law, the tremolo/vibrato LFOs from their documented shapes, and the MUL / KSL / key-scale values from the Yamaha YM3812/YMF262 datasheet. Those references are consulted only to know what the silicon does; the implementation — structure, code and tables — is original.
Licence
MIT © 2026 Sébastien Béchet.