pub struct Opll { /* private fields */ }Expand description
OPLL (YM2413 / VRC7) FM synthesizer instance.
One instance per VRC7-mapped cartridge. Caller drives the chip via
Opll::write_reg and pulls samples via Opll::calc at the
OPLL’s native 49,716 Hz sample rate.
§Example
// VRC7-mode chip for Lagrange Point
let mut opll = Opll::new(ChipType::Vrc7);
opll.write_reg(0x30, 0x01); // channel 0 instrument = patch 1
opll.write_reg(0x10, 0x80); // channel 0 fnum low
opll.write_reg(0x20, 0x15); // channel 0 fnum high + block + key-on
let sample: i16 = opll.calc();Implementations§
Source§impl Opll
impl Opll
Sourcepub fn new(chip_type: ChipType) -> Self
pub fn new(chip_type: ChipType) -> Self
Construct a new OPLL instance for the given chip type.
VRC7 mode loads the Konami custom patch set (the Nuke.YKT analysis values) — this is the table Lagrange Point uses.
Sourcepub fn reset_patch(&mut self, chip_type: ChipType)
pub fn reset_patch(&mut self, chip_type: ChipType)
Load the patch ROM for a chip type.
Sourcepub fn write_reg(&mut self, reg: u8, val: u8)
pub fn write_reg(&mut self, reg: u8, val: u8)
Write val to OPLL register reg (0x00..=0x3F). Larger
addresses are masked to 6 bits. Direct port of
OPLL_writeReg in emu2413.cpp:1223-1394.
This is the entry point VRC7 calls when the CPU writes to
$9030 (after latching the register address via $9010).
The decoder routes the write to the appropriate channel /
patch / control surface and schedules per-slot
commit_slot_update for the next OPLL tick.
VRC7-specific behaviour (chip_type == Vrc7):
$0E(rhythm mode) is ignored — VRC7 has no rhythm channels- Register addresses for channels 6, 7, 8 (
$16+,$26+,$36+) are ignored — VRC7 wires only 6 melodic channels
Sourcepub fn read_reg(&self, reg: u8) -> u8
pub fn read_reg(&self, reg: u8) -> u8
Read a register shadow byte (debugger / save-state helper).
Sourcepub fn calc(&mut self) -> i16
pub fn calc(&mut self) -> i16
Generate one mono sample at the OPLL’s native 49,716 Hz rate.
Drives the full per-clock pipeline: AM/PM LFO update → per-slot commit_slot_update / calc_envelope / calc_phase → per-channel 2-op FM output (modulator with self-feedback, carrier modulated by modulator’s output) → channel summation. For VRC7 (chip type 1), only the 6 melodic channels are summed; the rhythm channels in slots 12..18 are not used.
With no slot keyed on, calc produces silence — but the full
pipeline still runs every call (so AM/PM/EG advance), keeping the
per-clock cost constant regardless of channel activity.
Source§impl Opll
impl Opll
Sourcepub fn snapshot(&self) -> Vec<u8> ⓘ
pub fn snapshot(&self) -> Vec<u8> ⓘ
Serialize the complete live synthesizer state.
§Why this exists
Until v2.3.7 the VRC7 mapper’s save state carried only the shadow
register bytes and replayed nothing into the synthesizer, so after a
rewind, a netplay rollback, or a TAS restore the FM voice resumed from
whatever envelope and phase state it happened to hold — audible, and a
determinism gap in a project whose central claim is determinism. The
obvious format-free repair (replaying regs through
Opll::write_reg) is worse than the disease: it restarts every
keyed-on channel’s envelope at attack, so every rewind frame produces a
transient. Carrying the state verbatim is the only repair that restores
the sound that was actually playing.
§What is and is not carried
Everything mutated during synthesis: the register shadow, the EG/LFO
counters, the per-channel patch selection, all 18 operator slots
(phase accumulators, envelope state machines, feedback history), the
per-channel outputs and the mix. The user patch pair (patch_set[0..2],
writeable through registers $00-$07) is carried explicitly rather than
re-derived, so a restore cannot depend on refresh_user_patch_pointers
running in the right order.
Deliberately NOT carried, because they are constants of construction and
restoring them would be restoring a copy of the binary into itself:
waves and tll_rks (pure lookup tables built in Opll::new) and
patch_set[2..] (the chip’s patch ROM, fixed by chip_type). The chip
type itself IS carried, as a tag, purely so a mismatched restore is
rejected instead of silently reinterpreting slot patches against the
wrong instrument set.
The blob is exactly OPLL_SNAPSHOT_LEN bytes and self-describes its
version in byte 0.
Sourcepub fn restore(&mut self, data: &[u8]) -> Result<(), OpllStateError>
pub fn restore(&mut self, data: &[u8]) -> Result<(), OpllStateError>
Restore state previously produced by Opll::snapshot.
Trailing bytes past the schema are ignored, so a future version may append without breaking this reader — the same additive discipline the PPU and APU sections use.
§Errors
OpllStateError::Truncated if the blob is shorter than the schema,
OpllStateError::UnsupportedVersion if byte 0 is not
OPLL_SNAPSHOT_VERSION, OpllStateError::ChipTypeMismatch if the
blob describes a different chip, and
OpllStateError::InvalidEgState on a corrupt envelope-state tag.