Skip to main content

Opll

Struct Opll 

Source
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

Source

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.

Source

pub fn reset(&mut self)

Reset all channel/operator state. Patches are preserved.

Source

pub fn reset_patch(&mut self, chip_type: ChipType)

Load the patch ROM for a chip type.

Source

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
Source

pub fn read_reg(&self, reg: u8) -> u8

Read a register shadow byte (debugger / save-state helper).

Source

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

pub const fn chip_type(&self) -> ChipType

Returns the chip type.

Source§

impl Opll

Source

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.

Source

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.

Trait Implementations§

Source§

impl Clone for Opll

Source§

fn clone(&self) -> Opll

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more

Auto Trait Implementations§

§

impl Freeze for Opll

§

impl RefUnwindSafe for Opll

§

impl Send for Opll

§

impl Sync for Opll

§

impl Unpin for Opll

§

impl UnsafeUnpin for Opll

§

impl UnwindSafe for Opll

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.