oplon 1.0.0

A no_std, integer-only OPL2/OPL3 (Yamaha YM3812/YMF262) FM synthesis core with a clean-room, compile-time-generated ROM set
Documentation
# 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`](https://codeberg.org/sbechet/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

```rust
use oplon::Opl2;

let mut opl = Opl2::new(44_100);          // output sample rate
opl.write_reg(0x20, 0x01);                // op0 mult = 1
opl.write_reg(0x40, 0x00);                // op0 total level = loud
opl.write_reg(0x60, 0xF0);                // op0 fast attack
opl.write_reg(0x80, 0x00);                // op0 sustain/release
opl.write_reg(0xA0, 0x98);                // F-number low
opl.write_reg(0xB0, 0x31);                // key-on + block 4 + F-number high
let (left, right) = 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

```rust
use oplon::{Opl2, ChannelPatch, OpPatch};

let mut opl = Opl2::new(48_000);
opl.set_patch(0, &ChannelPatch {
    modulator: OpPatch { mul: 1, tl: 63, attack: 15, release: 7, sustaining: true, ..Default::default() },
    carrier:   OpPatch { mul: 1, attack: 15, release: 7, sustaining: true, ..Default::default() },
    ..Default::default()
});
opl.set_frequency(0, 0x2AE, 4);   // F-number / block
opl.key_on(0);
let (l, r) = opl.render_frame();
```

## The `vgmplay` binary

An optional CLI that plays [VGM](https://vgmrips.net/) and **DRO** (DOSBox Raw OPL) register logs through the engine — the OPL analogue of `sidera`'s `sidplay`.

```sh
cargo run --release --features player --bin vgmplay -- tune.vgm         # live (cpal)
cargo run --release --features player --bin vgmplay -- tune.vgz --wav out.wav
cargo run --release --features player --bin vgmplay -- tune.vgm --regdump regs.csv
```

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.