Skip to main content

rustynes_apu/
opll.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//
3// Provenance: this is a Rust port of emu2413 (the Yamaha YM2413 / OPLL FM core) by Mitsutaka Okazaki, MIT-licensed. See docs/originality-and-provenance.md (Section 1)
4// and NOTICE for the complete, audited derivation record.
5//! Yamaha YM2413 (OPLL) FM synthesizer — pure-Rust port of
6//! [`emu2413 v1.5.9`](https://github.com/digital-sound-antiques/emu2413)
7//! (MIT, Mitsutaka Okazaki) for the VRC7 mapper.
8//!
9//! # Scope
10//!
11//! The full FM pipeline is implemented: the constants, patch ROM tables
12//! (YM2413 / VRC7 / YMF281B), exp/sin lookup tables, the register-write
13//! decoder ([`Opll::write_reg`]), the phase generator (PG), envelope
14//! generator (EG), per-operator arithmetic, the 2-op channel update loop,
15//! and the AM/PM LFO. [`Opll::calc`] runs the whole pipeline once per
16//! chip clock and returns the mixed sample. Each DSP stage is covered by
17//! a unit test against emu2413 reference outputs.
18//!
19//! # Algorithmic reference
20//!
21//! - `emu2413 v1.5.9` (<https://github.com/digital-sound-antiques/emu2413>,
22//!   MIT, Mitsutaka Okazaki) — the canonical upstream MIT C source
23//! - nesdev wiki `VRC7_audio.md` — register surface + chip-level behaviour
24//! - nesdev wiki `User_Ben_Boldt_YM2413_Patches.md` — patch ROM analysis
25//!
26//! # License posture
27//!
28//! emu2413 is MIT-licensed at upstream; this is a pure-Rust port of that C
29//! source, distributed under the same MIT license. We preserve the upstream
30//! MIT notice in `NOTICE` at the repo root (see ADR-0006).
31//!
32//! # Determinism
33//!
34//! The OPLL is fully deterministic: identical input register-write
35//! sequences produce bit-identical sample streams. The output is
36//! `i16` in the `[-4095, 4095]` range (15-bit signed magnitude per
37//! the chip's DAC).
38
39// A handful of patch-ROM / LUT entries and chip-state fields ported
40// verbatim from emu2413 are not read on the VRC7 path (e.g. rhythm-mode
41// state the VRC7 wiring never reaches). Allowing dead_code keeps the
42// faithful 1:1 port intact without per-field cfg gating while preserving
43// the `-D warnings` quality gate.
44#![allow(dead_code)]
45// The PG/EG ports mirror C semantics (unsigned/signed wrap, narrowing
46// casts) byte-for-byte against emu2413.cpp. The arithmetic is bounded
47// by the chip's documented register widths; clippy's pedantic cast
48// lints flag intentional truncations that match the upstream behavior
49// and the reference test outputs.  Allowed at module level rather
50// than salting every cast site.
51#![allow(
52    clippy::cast_possible_truncation,
53    clippy::cast_possible_wrap,
54    clippy::cast_sign_loss,
55    clippy::cast_precision_loss
56)]
57
58extern crate alloc;
59
60use alloc::vec;
61use alloc::vec::Vec;
62
63// ---------------------------------------------------------------------------
64// Constants — match emu2413.cpp lines 108-134
65// ---------------------------------------------------------------------------
66
67/// Phase increment counter width.
68const DP_BITS: u32 = 19;
69/// Full DP counter range = `1 << DP_BITS`.
70const DP_WIDTH: u32 = 1u32 << DP_BITS;
71/// Phase generator output bits (1024-length sine table = 2^10).
72const PG_BITS: u32 = 10;
73/// Phase generator table width.
74const PG_WIDTH: usize = 1 << PG_BITS;
75/// Number of address bits between DP and PG counters.
76const DP_BASE_BITS: u32 = DP_BITS - PG_BITS;
77
78/// Envelope output bits.
79const EG_BITS: u32 = 7;
80/// Envelope mute level.
81const EG_MUTE: u32 = (1 << EG_BITS) - 1;
82/// Envelope max level (mute - 4).
83const EG_MAX: u32 = EG_MUTE - 4;
84
85/// Total-level bits.
86const TL_BITS: u32 = 6;
87
88/// Damper rate (before key-on; key-scale affects this).
89const DAMPER_RATE: u8 = 12;
90
91/// Convert TL to EG units (left-shift by 1).
92#[inline]
93const fn tl_to_eg(d: u32) -> u32 {
94    d << 1
95}
96
97// ---------------------------------------------------------------------------
98// Patch ROM tables — match emu2413.cpp lines 42-104
99//
100// Each patch is 8 bytes; 16 instrument patches + 3 rhythm patches per chip
101// type. VRC7 only uses the 16 instrument patches.
102// ---------------------------------------------------------------------------
103
104/// Chip type — selects the patch ROM table.
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub enum ChipType {
107    /// YM2413 — original OPLL with 15 melodic patches + percussion.
108    Ym2413,
109    /// VRC7 — Konami mapper 85's custom OPLL variant.
110    Vrc7,
111    /// YMF281B — derivative used in some arcade hardware.
112    Ymf281b,
113}
114
115impl ChipType {
116    /// Returns the 19×8 patch dump for this chip type.
117    #[inline]
118    const fn patch_dump(self) -> &'static [u8; 19 * 8] {
119        match self {
120            Self::Ym2413 => &DEFAULT_INST_YM2413,
121            Self::Vrc7 => &DEFAULT_INST_VRC7,
122            Self::Ymf281b => &DEFAULT_INST_YMF281B,
123        }
124    }
125}
126
127/// YM2413 patch dump (16 melodic + 3 rhythm). emu2413 row 0.
128const DEFAULT_INST_YM2413: [u8; 19 * 8] = [
129    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0: User
130    0x71, 0x61, 0x1e, 0x17, 0xd0, 0x78, 0x00, 0x17, // 1: Violin
131    0x13, 0x41, 0x1a, 0x0d, 0xd8, 0xf7, 0x23, 0x13, // 2: Guitar
132    0x13, 0x01, 0x99, 0x00, 0xf2, 0xc4, 0x21, 0x23, // 3: Piano
133    0x11, 0x61, 0x0e, 0x07, 0x8d, 0x64, 0x70, 0x27, // 4: Flute
134    0x32, 0x21, 0x1e, 0x06, 0xe1, 0x76, 0x01, 0x28, // 5: Clarinet
135    0x31, 0x22, 0x16, 0x05, 0xe0, 0x71, 0x00, 0x18, // 6: Oboe
136    0x21, 0x61, 0x1d, 0x07, 0x82, 0x81, 0x11, 0x07, // 7: Trumpet
137    0x33, 0x21, 0x2d, 0x13, 0xb0, 0x70, 0x00, 0x07, // 8: Organ
138    0x61, 0x61, 0x1b, 0x06, 0x64, 0x65, 0x10, 0x17, // 9: Horn
139    0x41, 0x61, 0x0b, 0x18, 0x85, 0xf0, 0x81, 0x07, // A: Synthesizer
140    0x33, 0x01, 0x83, 0x11, 0xea, 0xef, 0x10, 0x04, // B: Harpsichord
141    0x17, 0xc1, 0x24, 0x07, 0xf8, 0xf8, 0x22, 0x12, // C: Vibraphone
142    0x61, 0x50, 0x0c, 0x05, 0xd2, 0xf5, 0x40, 0x42, // D: Synthsizer Bass
143    0x01, 0x01, 0x55, 0x03, 0xe9, 0x90, 0x03, 0x02, // E: Acoustic Bass
144    0x41, 0x41, 0x89, 0x03, 0xf1, 0xe4, 0xc0, 0x13, // F: Electric Guitar
145    0x01, 0x01, 0x18, 0x0f, 0xdf, 0xf8, 0x6a, 0x6d, // R: Bass Drum
146    0x01, 0x01, 0x00, 0x00, 0xc8, 0xd8, 0xa7, 0x68, // R: High-Hat(M) / Snare Drum(C)
147    0x05, 0x01, 0x00, 0x00, 0xf8, 0xaa, 0x59, 0x55, // R: Tom-tom(M) / Top Cymbal(C)
148];
149
150/// VRC7 patch dump from Nuke.YKT analysis (16 melodic + 3 rhythm, but
151/// VRC7 doesn't use rhythm). This is THE table for Lagrange Point.
152const DEFAULT_INST_VRC7: [u8; 19 * 8] = [
153    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0: User
154    0x03, 0x21, 0x05, 0x06, 0xe8, 0x81, 0x42, 0x27, // 1
155    0x13, 0x41, 0x14, 0x0d, 0xd8, 0xf6, 0x23, 0x12, // 2
156    0x11, 0x11, 0x08, 0x08, 0xfa, 0xb2, 0x20, 0x12, // 3
157    0x31, 0x61, 0x0c, 0x07, 0xa8, 0x64, 0x61, 0x27, // 4
158    0x32, 0x21, 0x1e, 0x06, 0xe1, 0x76, 0x01, 0x28, // 5
159    0x02, 0x01, 0x06, 0x00, 0xa3, 0xe2, 0xf4, 0xf4, // 6
160    0x21, 0x61, 0x1d, 0x07, 0x82, 0x81, 0x11, 0x07, // 7
161    0x23, 0x21, 0x22, 0x17, 0xa2, 0x72, 0x01, 0x17, // 8
162    0x35, 0x11, 0x25, 0x00, 0x40, 0x73, 0x72, 0x01, // 9
163    0xb5, 0x01, 0x0f, 0x0F, 0xa8, 0xa5, 0x51, 0x02, // A
164    0x17, 0xc1, 0x24, 0x07, 0xf8, 0xf8, 0x22, 0x12, // B
165    0x71, 0x23, 0x11, 0x06, 0x65, 0x74, 0x18, 0x16, // C
166    0x01, 0x02, 0xd3, 0x05, 0xc9, 0x95, 0x03, 0x02, // D
167    0x61, 0x63, 0x0c, 0x00, 0x94, 0xC0, 0x33, 0xf6, // E
168    0x21, 0x72, 0x0d, 0x00, 0xc1, 0xd5, 0x56, 0x06, // F
169    0x01, 0x01, 0x18, 0x0f, 0xdf, 0xf8, 0x6a, 0x6d, // R: Bass Drum (unused on VRC7)
170    0x01, 0x01, 0x00, 0x00, 0xc8, 0xd8, 0xa7, 0x68, // R: HH/SD
171    0x05, 0x01, 0x00, 0x00, 0xf8, 0xaa, 0x59, 0x55, // R: Tom/Cymbal
172];
173
174/// YMF281B patch dump (kept for completeness; not used by VRC7).
175const DEFAULT_INST_YMF281B: [u8; 19 * 8] = [
176    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x21, 0x1a, 0x07, 0xf0, 0x6f, 0x00, 0x16,
177    0x40, 0x10, 0x45, 0x00, 0xf6, 0x83, 0x73, 0x63, 0x13, 0x01, 0x99, 0x00, 0xf2, 0xc3, 0x21, 0x23,
178    0x01, 0x61, 0x0b, 0x0f, 0xf9, 0x64, 0x70, 0x17, 0x32, 0x21, 0x1e, 0x06, 0xe1, 0x76, 0x01, 0x28,
179    0x60, 0x01, 0x82, 0x0e, 0xf9, 0x61, 0x20, 0x27, 0x21, 0x61, 0x1c, 0x07, 0x84, 0x81, 0x11, 0x07,
180    0x37, 0x32, 0xc9, 0x01, 0x66, 0x64, 0x40, 0x28, 0x01, 0x21, 0x07, 0x03, 0xa5, 0x71, 0x51, 0x07,
181    0x06, 0x01, 0x5e, 0x07, 0xf3, 0xf3, 0xf6, 0x13, 0x00, 0x00, 0x18, 0x06, 0xf5, 0xf3, 0x20, 0x23,
182    0x17, 0xc1, 0x24, 0x07, 0xf8, 0xf8, 0x22, 0x12, 0x35, 0x64, 0x00, 0x00, 0xff, 0xf3, 0x77, 0xf5,
183    0x11, 0x31, 0x00, 0x07, 0xdd, 0xf3, 0xff, 0xfb, 0x3a, 0x21, 0x00, 0x07, 0x80, 0x84, 0x0f, 0xf5,
184    0x01, 0x01, 0x18, 0x0f, 0xdf, 0xf8, 0x6a, 0x6d, 0x01, 0x01, 0x00, 0x00, 0xc8, 0xd8, 0xa7, 0x68,
185    0x05, 0x01, 0x00, 0x00, 0xf8, 0xaa, 0x59, 0x55,
186];
187
188// ---------------------------------------------------------------------------
189// exp_table[256] — match emu2413.cpp lines 137-154
190//
191// exp_table[x] = round((exp2((double)x / 256.0) - 1) * 1024)
192// Used by the operator output: log-domain volume → linear amplitude.
193// ---------------------------------------------------------------------------
194
195const EXP_TABLE: [u16; 256] = [
196    0, 3, 6, 8, 11, 14, 17, 20, 22, 25, 28, 31, 34, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69,
197    72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108, 111, 114, 117, 120, 123, 126, 130, 133,
198    136, 139, 142, 145, 148, 152, 155, 158, 161, 164, 168, 171, 174, 177, 181, 184, 187, 190, 194,
199    197, 200, 204, 207, 210, 214, 217, 220, 224, 227, 231, 234, 237, 241, 244, 248, 251, 255, 258,
200    262, 265, 268, 272, 276, 279, 283, 286, 290, 293, 297, 300, 304, 308, 311, 315, 318, 322, 326,
201    329, 333, 337, 340, 344, 348, 352, 355, 359, 363, 367, 370, 374, 378, 382, 385, 389, 393, 397,
202    401, 405, 409, 412, 416, 420, 424, 428, 432, 436, 440, 444, 448, 452, 456, 460, 464, 468, 472,
203    476, 480, 484, 488, 492, 496, 501, 505, 509, 513, 517, 521, 526, 530, 534, 538, 542, 547, 551,
204    555, 560, 564, 568, 572, 577, 581, 585, 590, 594, 599, 603, 607, 612, 616, 621, 625, 630, 634,
205    639, 643, 648, 652, 657, 661, 666, 670, 675, 680, 684, 689, 693, 698, 703, 708, 712, 717, 722,
206    726, 731, 736, 741, 745, 750, 755, 760, 765, 770, 774, 779, 784, 789, 794, 799, 804, 809, 814,
207    819, 824, 829, 834, 839, 844, 849, 854, 859, 864, 869, 874, 880, 885, 890, 895, 900, 906, 911,
208    916, 921, 927, 932, 937, 942, 948, 953, 959, 964, 969, 975, 980, 986, 991, 996, 1002, 1007,
209    1013, 1018,
210];
211
212// ---------------------------------------------------------------------------
213// fullsin_table[256] — match emu2413.cpp lines 156-173
214//
215// fullsin_table[x] = round(-log2(sin((x + 0.5) * PI / (PG_WIDTH / 4) / 2)) * 256)
216// Quarter-wave log-domain sine. PG generation mirrors across quadrants.
217// First 256 entries explicit; remainder zero per the C declaration's
218// implicit zero-init.
219// ---------------------------------------------------------------------------
220
221const FULLSIN_TABLE_QUARTER: [u16; 256] = [
222    2137, 1731, 1543, 1419, 1326, 1252, 1190, 1137, 1091, 1050, 1013, 979, 949, 920, 894, 869, 846,
223    825, 804, 785, 767, 749, 732, 717, 701, 687, 672, 659, 646, 633, 621, 609, 598, 587, 576, 566,
224    556, 546, 536, 527, 518, 509, 501, 492, 484, 476, 468, 461, 453, 446, 439, 432, 425, 418, 411,
225    405, 399, 392, 386, 380, 375, 369, 363, 358, 352, 347, 341, 336, 331, 326, 321, 316, 311, 307,
226    302, 297, 293, 289, 284, 280, 276, 271, 267, 263, 259, 255, 251, 248, 244, 240, 236, 233, 229,
227    226, 222, 219, 215, 212, 209, 205, 202, 199, 196, 193, 190, 187, 184, 181, 178, 175, 172, 169,
228    167, 164, 161, 159, 156, 153, 151, 148, 146, 143, 141, 138, 136, 134, 131, 129, 127, 125, 122,
229    120, 118, 116, 114, 112, 110, 108, 106, 104, 102, 100, 98, 96, 94, 92, 91, 89, 87, 85, 83, 82,
230    80, 78, 77, 75, 74, 72, 70, 69, 67, 66, 64, 63, 62, 60, 59, 57, 56, 55, 53, 52, 51, 49, 48, 47,
231    46, 45, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 23,
232    22, 21, 20, 20, 19, 18, 17, 17, 16, 15, 15, 14, 13, 13, 12, 12, 11, 10, 10, 9, 9, 8, 8, 7, 7,
233    7, 6, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
234];
235
236// ---------------------------------------------------------------------------
237// pm_table[8][8] — pitch-modulation LFO table (emu2413.cpp lines 181-190)
238// ---------------------------------------------------------------------------
239
240const PM_TABLE: [[i8; 8]; 8] = [
241    [0, 0, 0, 0, 0, 0, 0, 0],    // fnum = 000xxxxxx
242    [0, 0, 1, 0, 0, 0, -1, 0],   // fnum = 001xxxxxx
243    [0, 1, 2, 1, 0, -1, -2, -1], // fnum = 010xxxxxx
244    [0, 1, 3, 1, 0, -1, -3, -1], // fnum = 011xxxxxx
245    [0, 2, 4, 2, 0, -2, -4, -2], // fnum = 100xxxxxx
246    [0, 2, 5, 2, 0, -2, -5, -2], // fnum = 101xxxxxx
247    [0, 3, 6, 3, 0, -3, -6, -3], // fnum = 110xxxxxx
248    [0, 3, 7, 3, 0, -3, -7, -3], // fnum = 111xxxxxx
249];
250
251// ---------------------------------------------------------------------------
252// am_table[210] — amplitude-modulation LFO table (emu2413.cpp lines 195-209)
253//
254// Verified against real YM2413 hardware. Each element repeats 64 cycles.
255// ---------------------------------------------------------------------------
256
257const AM_TABLE: [u8; 210] = [
258    0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, //
259    2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, //
260    4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, //
261    6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, //
262    8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, //
263    10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, //
264    12, 12, 12, 12, 12, 12, 12, 12, //
265    13, 13, 13, //
266    12, 12, 12, 12, 12, 12, 12, 12, //
267    11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 10, 10, 10, 10, 10, 10, //
268    9, 9, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 8, //
269    7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, //
270    5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, //
271    3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, //
272    1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
273];
274
275// ---------------------------------------------------------------------------
276// EG step tables (emu2413.cpp lines 213-218) — based on andete's research
277// ---------------------------------------------------------------------------
278
279const EG_STEP_TABLES: [[u8; 8]; 4] = [
280    [0, 1, 0, 1, 0, 1, 0, 1],
281    [0, 1, 0, 1, 1, 1, 0, 1],
282    [0, 1, 1, 1, 0, 1, 1, 1],
283    [0, 1, 1, 1, 1, 1, 1, 1],
284];
285
286// ---------------------------------------------------------------------------
287// Multiplier table (emu2413.cpp line 222-223). Doubled fixed-point.
288// ---------------------------------------------------------------------------
289
290const ML_TABLE: [u32; 16] = [
291    1,
292    2,
293    2 * 2,
294    3 * 2,
295    4 * 2,
296    5 * 2,
297    6 * 2,
298    7 * 2,
299    8 * 2,
300    9 * 2,
301    10 * 2,
302    10 * 2,
303    12 * 2,
304    12 * 2,
305    15 * 2,
306    15 * 2,
307];
308
309// ---------------------------------------------------------------------------
310// Patch parameters (13 fields per patch). Matches OPLL_PATCH in emu2413.h.
311// ---------------------------------------------------------------------------
312
313/// One OPLL patch — the 13-field instrument definition.
314#[derive(Clone, Copy, Debug, Default)]
315pub struct Patch {
316    /// Total level (carrier volume; 0-63 in dB units).
317    pub tl: u8,
318    /// Feedback level (modulator self-feedback).
319    pub fb: u8,
320    /// Envelope-generator sustain enable.
321    pub eg: u8,
322    /// Multiplier (frequency ratio).
323    pub ml: u8,
324    /// Attack rate.
325    pub ar: u8,
326    /// Decay rate.
327    pub dr: u8,
328    /// Sustain level.
329    pub sl: u8,
330    /// Release rate.
331    pub rr: u8,
332    /// Key-rate scaling.
333    pub kr: u8,
334    /// Key-level scaling.
335    pub kl: u8,
336    /// AM enable.
337    pub am: u8,
338    /// PM enable.
339    pub pm: u8,
340    /// Wave select (0=full sine, 1=half sine).
341    pub ws: u8,
342}
343
344impl Patch {
345    /// Decode 8 bytes of patch ROM dump into a Patch. Matches
346    /// `OPLL_dumpToPatch` in emu2413.cpp lines 366-395.
347    pub fn from_dump_modulator(dump: &[u8; 8]) -> Self {
348        Self {
349            am: (dump[0] >> 7) & 1,
350            pm: (dump[0] >> 6) & 1,
351            eg: (dump[0] >> 5) & 1,
352            kr: (dump[0] >> 4) & 1,
353            ml: dump[0] & 0x0f,
354            kl: (dump[2] >> 6) & 0x03,
355            tl: dump[2] & 0x3f,
356            ar: (dump[4] >> 4) & 0x0f,
357            dr: dump[4] & 0x0f,
358            sl: (dump[6] >> 4) & 0x0f,
359            rr: dump[6] & 0x0f,
360            fb: dump[3] & 0x07,
361            ws: (dump[3] >> 3) & 0x01,
362        }
363    }
364
365    /// Decode 8 bytes of patch ROM dump into a carrier Patch.
366    pub fn from_dump_carrier(dump: &[u8; 8]) -> Self {
367        Self {
368            am: (dump[1] >> 7) & 1,
369            pm: (dump[1] >> 6) & 1,
370            eg: (dump[1] >> 5) & 1,
371            kr: (dump[1] >> 4) & 1,
372            ml: dump[1] & 0x0f,
373            kl: (dump[3] >> 6) & 0x03,
374            tl: 0, // carrier TL comes from $3x register, not patch
375            ar: (dump[5] >> 4) & 0x0f,
376            dr: dump[5] & 0x0f,
377            sl: (dump[7] >> 4) & 0x0f,
378            rr: dump[7] & 0x0f,
379            fb: 0,
380            ws: (dump[3] >> 4) & 0x01,
381        }
382    }
383}
384
385// ---------------------------------------------------------------------------
386// Envelope generator state machine (emu2413.cpp line 220)
387// ---------------------------------------------------------------------------
388
389#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
390enum EgState {
391    Attack,
392    Decay,
393    Sustain,
394    Release,
395    Damp,
396    #[default]
397    Unknown,
398}
399
400// ---------------------------------------------------------------------------
401// Slot — one operator (modulator or carrier). 18 in YM2413, 12 used in VRC7.
402// ---------------------------------------------------------------------------
403
404#[derive(Clone, Copy, Debug, Default)]
405struct Slot {
406    number: u8,
407    /// Bit 0 (M): 0=modulator, 1=carrier. Bit 1 (S): rhythm-only flag.
408    type_flags: u8,
409    patch: Patch,
410
411    /// Latest and previous output for self-feedback.
412    output: [i32; 2],
413
414    // Phase generator
415    wave_table_idx: u8,
416    pg_phase: u32,
417    pg_out: u32,
418    pg_keep: u8,
419    blk_fnum: u16,
420    fnum: u16,
421    blk: u8,
422
423    // Envelope generator
424    eg_state: EgState,
425    volume: i32,
426    key_flag: u8,
427    sus_flag: u8,
428    tll: u16,
429    rks: u8,
430    eg_rate_h: u8,
431    eg_rate_l: u8,
432    eg_shift: u32,
433    eg_out: u32,
434
435    update_requests: u32,
436}
437
438impl Slot {
439    /// Initialize a slot to the post-`reset_slot` state (emu2413.cpp:561-582).
440    ///
441    /// `number` is the slot index (0..18). Even indices are modulators
442    /// (`type & 1 == 0`); odd are carriers (`type & 1 == 1`). The slot
443    /// enters the `Release` envelope state with maximum attenuation
444    /// (`eg_out = EG_MUTE`), ready to be keyed on.
445    fn reset_to_release(&mut self, number: u8) {
446        self.number = number;
447        self.type_flags = number % 2;
448        self.pg_keep = 0;
449        self.wave_table_idx = 0;
450        self.pg_phase = 0;
451        self.output = [0, 0];
452        self.eg_state = EgState::Release;
453        self.eg_shift = 0;
454        self.rks = 0;
455        self.tll = 0;
456        self.key_flag = 0;
457        self.sus_flag = 0;
458        self.blk_fnum = 0;
459        self.blk = 0;
460        self.fnum = 0;
461        self.volume = 0;
462        self.pg_out = 0;
463        self.eg_out = EG_MUTE;
464        self.eg_rate_h = 0;
465        self.eg_rate_l = 0;
466        self.update_requests = 0;
467        self.patch = Patch::default();
468    }
469
470    /// Advance the phase generator by one OPLL clock and refresh `pg_out`.
471    ///
472    /// Direct port of emu2413.cpp lines 765-773.  The phase increment is
473    /// `((fnum_low9 * 2 + pm) * ml_table[ML]) << blk >> 2`, where `pm`
474    /// is the pitch-modulation offset from [`PM_TABLE`] when the patch's
475    /// PM bit is set.  The DP counter wraps modulo `DP_WIDTH`; `pg_out`
476    /// is the upper [`PG_BITS`] of the counter (the index into the
477    /// 1024-entry sine table).
478    ///
479    /// `pm_phase` is the chip's global PM LFO phase (`Opll::pm_phase`).
480    /// `reset` is true on key-on for rhythm slots with `pg_keep == 0`
481    /// (the carrier of a damped channel re-zeros its phase).
482    fn calc_phase(&mut self, pm_phase: i32, reset: bool) {
483        let pm = if self.patch.pm != 0 {
484            // pm_table[fnum>>6 & 7][pm_phase>>10 & 7] in C; values are i8.
485            let fnum_row = (self.fnum as usize >> 6) & 7;
486            let phase_col = ((pm_phase >> 10) & 7) as usize;
487            i32::from(PM_TABLE[fnum_row][phase_col])
488        } else {
489            0
490        };
491
492        if reset {
493            self.pg_phase = 0;
494        }
495
496        // fnum_low9 = fnum & 0x1FF; phase increment expression matches C.
497        let fnum_low9 = i32::from(self.fnum & 0x1FF);
498        let ml = ML_TABLE[self.patch.ml as usize] as i32;
499        let increment_pre_shift = (fnum_low9 * 2 + pm) * ml;
500        // `<< blk >> 2` in C — wrap with i64 to safely shift then truncate.
501        let shifted = (i64::from(increment_pre_shift) << self.blk) >> 2;
502
503        // Cast back to u32 with wrapping_add, then mask to DP range.
504        self.pg_phase = self.pg_phase.wrapping_add(shifted as u32) & (DP_WIDTH - 1);
505        self.pg_out = self.pg_phase >> DP_BASE_BITS;
506    }
507
508    /// Attack-state EG step lookup. Direct port of emu2413.cpp:775-795.
509    fn lookup_attack_step(&self, counter: u32) -> u8 {
510        match self.eg_rate_h {
511            12 => {
512                let index = ((counter & 0xc) >> 1) as usize;
513                4 - EG_STEP_TABLES[self.eg_rate_l as usize][index]
514            }
515            13 => {
516                let index = ((counter & 0xc) >> 1) as usize;
517                3 - EG_STEP_TABLES[self.eg_rate_l as usize][index]
518            }
519            14 => {
520                let index = ((counter & 0xc) >> 1) as usize;
521                2 - EG_STEP_TABLES[self.eg_rate_l as usize][index]
522            }
523            0 | 15 => 0,
524            _ => {
525                let index = (counter >> self.eg_shift) as usize;
526                if EG_STEP_TABLES[self.eg_rate_l as usize][index & 7] != 0 {
527                    4
528                } else {
529                    0
530                }
531            }
532        }
533    }
534
535    /// Decay-state EG step lookup. Direct port of emu2413.cpp:797-815.
536    fn lookup_decay_step(&self, counter: u32) -> u8 {
537        match self.eg_rate_h {
538            0 => 0,
539            13 => {
540                let index = (((counter & 0xc) >> 1) | (counter & 1)) as usize;
541                EG_STEP_TABLES[self.eg_rate_l as usize][index]
542            }
543            14 => {
544                let index = ((counter & 0xc) >> 1) as usize;
545                EG_STEP_TABLES[self.eg_rate_l as usize][index] + 1
546            }
547            15 => 2,
548            _ => {
549                let index = (counter >> self.eg_shift) as usize;
550                EG_STEP_TABLES[self.eg_rate_l as usize][index & 7]
551            }
552        }
553    }
554
555    /// Begin envelope from the `Damp` → `Attack`/`Decay` transition
556    /// (emu2413.cpp:817-825).  If the effective attack rate saturates
557    /// at 15, the operator skips Attack and enters Decay at zero
558    /// attenuation (instant attack).
559    fn start_envelope(&mut self) {
560        // min(15, AR + (rks >> 2)) — saturated effective rate.
561        let effective_ar = (self.patch.ar + (self.rks >> 2)).min(15);
562        if effective_ar == 15 {
563            self.eg_state = EgState::Decay;
564            self.eg_out = 0;
565        } else {
566            self.eg_state = EgState::Attack;
567        }
568        self.update_requests |= UPDATE_EG;
569    }
570
571    /// Run one envelope-generator tick (emu2413.cpp:827-887).
572    ///
573    /// Returns [`EnvelopeStep::ResetBuddyPhase`] when this carrier slot
574    /// just transitioned out of Damp on key-on AND the caller should
575    /// also reset the modulator's `pg_phase`.  The caller (Opll) is
576    /// responsible for applying the buddy reset — Rust's borrow checker
577    /// rules out a buddy `&mut` while we hold `&mut self`.
578    ///
579    /// `buddy_pg_keep` is the buddy slot's `pg_keep` flag (false for
580    /// non-rhythm channels).  `eg_counter` is the chip's global EG
581    /// counter (`Opll::eg_counter`).  `test` is bit 1 of register `$0F`
582    /// (forces `eg_out` to 0 each tick).
583    fn calc_envelope(&mut self, buddy_pg_keep: bool, eg_counter: u32, test: u8) -> EnvelopeStep {
584        let mask = (1u32 << self.eg_shift).wrapping_sub(1);
585        let mut buddy_reset = EnvelopeStep::Continue;
586
587        if self.eg_state == EgState::Attack {
588            if self.eg_out > 0 && self.eg_rate_h > 0 && (eg_counter & mask & !3) == 0 {
589                let s = self.lookup_attack_step(eg_counter);
590                if s > 0 {
591                    let cur = self.eg_out as i32;
592                    let next = (cur - (cur >> s) - 1).max(0);
593                    self.eg_out = next as u32;
594                }
595            }
596        } else if self.eg_rate_h > 0 && (eg_counter & mask) == 0 {
597            self.eg_out =
598                (self.eg_out + u32::from(self.lookup_decay_step(eg_counter))).min(EG_MUTE);
599        }
600
601        match self.eg_state {
602            EgState::Damp => {
603                if self.eg_out >= EG_MAX && (eg_counter & mask) == 0 {
604                    self.start_envelope();
605                    // For carriers (type bit 0 set), the carrier's
606                    // pg_phase resets to 0 unless pg_keep is set; the
607                    // modulator buddy also resets (caller applies).
608                    if self.type_flags & 1 != 0 {
609                        if self.pg_keep == 0 {
610                            self.pg_phase = 0;
611                        }
612                        if !buddy_pg_keep {
613                            buddy_reset = EnvelopeStep::ResetBuddyPhase;
614                        }
615                    }
616                }
617            }
618            EgState::Attack => {
619                if self.eg_out == 0 {
620                    self.eg_state = EgState::Decay;
621                    self.update_requests |= UPDATE_EG;
622                }
623            }
624            EgState::Decay => {
625                // Decay → Sustain transition is checked every cycle
626                // (NOT synchronized with the envelope counter — per
627                // upstream comment at emu2413.cpp:871).
628                if (self.eg_out >> 3) == u32::from(self.patch.sl) {
629                    self.eg_state = EgState::Sustain;
630                    self.update_requests |= UPDATE_EG;
631                }
632            }
633            EgState::Sustain | EgState::Release | EgState::Unknown => {}
634        }
635
636        if test != 0 {
637            self.eg_out = 0;
638        }
639
640        buddy_reset
641    }
642}
643
644/// Result of a single [`Slot::calc_envelope`] tick — signals the caller
645/// when to reset the buddy slot's `pg_phase` (the Damp → Attack carrier
646/// transition resets both the carrier's and modulator's phase counters).
647#[derive(Clone, Copy, Debug, PartialEq, Eq)]
648enum EnvelopeStep {
649    /// No buddy-state mutation required.
650    Continue,
651    /// Caller must zero the buddy slot's `pg_phase`.
652    ResetBuddyPhase,
653}
654
655// ---------------------------------------------------------------------------
656// Update-request flags — emu2413.cpp:504-510. Set by setters that change
657// patch/fnum/volume, consumed by `commit_slot_update` (next sprint).
658// ---------------------------------------------------------------------------
659
660const UPDATE_WS: u32 = 1;
661const UPDATE_TLL: u32 = 2;
662const UPDATE_RKS: u32 = 4;
663const UPDATE_EG: u32 = 8;
664const UPDATE_ALL: u32 = 255;
665
666// ---------------------------------------------------------------------------
667// kl_table — key-level scaling base values (emu2413.cpp:226-228).
668// All values are pre-doubled (`dB2(x) = x * 2`) so the raw cell value is
669// the 1/2-dB attenuation magnitude.
670// ---------------------------------------------------------------------------
671
672const KL_TABLE: [f32; 16] = [
673    0.0, 18.0, 24.0, 27.75, 30.0, 32.25, 33.75, 35.25, 36.0, 37.5, 38.25, 39.0, 39.75, 40.5, 41.25,
674    42.0,
675];
676
677// ---------------------------------------------------------------------------
678// Runtime-built lookup tables. Sized for the full OPLL register space.
679// ---------------------------------------------------------------------------
680
681/// Full 1024-entry log-domain sine table (extended from
682/// [`FULLSIN_TABLE_QUARTER`] per emu2413.cpp:356-372).
683///
684/// First quarter [0..256): explicit (input data).
685/// Second quarter [256..512): mirror of first (descending).
686/// Second half [512..1024): first half with sign bit (`0x8000`) set.
687#[derive(Clone)]
688struct WaveTables {
689    fullsin: [u16; PG_WIDTH],
690    /// First half = `fullsin[0..512]`; second half = `0xfff` (mute).
691    halfsin: [u16; PG_WIDTH],
692}
693
694impl WaveTables {
695    fn new() -> Self {
696        let mut fullsin = [0u16; PG_WIDTH];
697        let qw = PG_WIDTH / 4;
698        // First quarter: copy from the quarter-wave LUT.
699        fullsin[..qw].copy_from_slice(&FULLSIN_TABLE_QUARTER);
700        // Second quarter: mirror (descending) from the first.
701        for x in 0..qw {
702            fullsin[qw + x] = fullsin[qw - x - 1];
703        }
704        // Second half: set the sign bit on each first-half entry.
705        for x in 0..(PG_WIDTH / 2) {
706            fullsin[PG_WIDTH / 2 + x] = 0x8000 | fullsin[x];
707        }
708
709        let mut halfsin = [0u16; PG_WIDTH];
710        halfsin[..(PG_WIDTH / 2)].copy_from_slice(&fullsin[..(PG_WIDTH / 2)]);
711        for slot in &mut halfsin[(PG_WIDTH / 2)..] {
712            *slot = 0xfff;
713        }
714        Self { fullsin, halfsin }
715    }
716
717    #[inline]
718    fn sample(&self, idx: u8, phase: u32) -> u16 {
719        let i = (phase as usize) & (PG_WIDTH - 1);
720        match idx {
721            0 => self.fullsin[i],
722            _ => self.halfsin[i],
723        }
724    }
725}
726
727/// Total-Level Lookup. `tll[block_fnum_idx][TL or volume][KL] → EG units`.
728///
729/// `block_fnum_idx = (block << 4) | (fnum_high_4)` — 7 bits indexing
730/// 128 rows. TL ranges 0..64, KL ranges 0..4.
731#[derive(Clone)]
732struct TllRksTables {
733    /// Flat storage of `[block_fnum: 128][TL: 64][KL: 4]` u32s
734    /// (32,768 entries × 4 bytes = 128 KiB).  Indexed via
735    /// [`TllRksTables::tll_at`].
736    tll_flat: alloc::boxed::Box<[u32]>,
737    /// `rks[(block << 1) | fnum_top_bit][KR]`.
738    rks: [[u8; 2]; 16],
739}
740
741impl TllRksTables {
742    /// Read TLL[block_fnum][TL][KL]. Bounds are: `block_fnum < 128`,
743    /// `tl < 64`, `kl < 4`.
744    #[inline]
745    fn tll_at(&self, block_fnum: usize, tl: usize, kl: usize) -> u32 {
746        self.tll_flat[block_fnum * 64 * 4 + tl * 4 + kl]
747    }
748}
749
750impl TllRksTables {
751    fn new() -> Self {
752        // Allocate the 128 KiB TLL table directly on the heap via
753        // `vec!` to avoid the 128 KiB stack intermediate that
754        // `Box::new([...])` would otherwise require.
755        let mut tll_flat: alloc::vec::Vec<u32> = alloc::vec![0u32; 128 * 64 * 4];
756
757        // emu2413.cpp:374-396 — buildTllTable
758        for (fnum, &kl_val) in KL_TABLE.iter().enumerate() {
759            for block in 0..8usize {
760                let idx = (block << 4) | fnum;
761                for tl in 0..64usize {
762                    for kl in 0..4usize {
763                        let pos = idx * 64 * 4 + tl * 4 + kl;
764                        if kl == 0 {
765                            tll_flat[pos] = tl_to_eg(tl as u32);
766                        } else {
767                            // tmp = (int32_t)(kl_table[fnum] - dB2(3.0) * (7 - block))
768                            let tmp = (kl_val - 6.0 * (7 - block) as f32) as i32;
769                            if tmp <= 0 {
770                                tll_flat[pos] = tl_to_eg(tl as u32);
771                            } else {
772                                let shifted = tmp >> (3 - kl as u32);
773                                // EG_STEP = 0.375 → division by 0.375 = multiplication by 8/3
774                                let scaled = (shifted as f32 / 0.375) as u32;
775                                tll_flat[pos] = scaled + tl_to_eg(tl as u32);
776                            }
777                        }
778                    }
779                }
780            }
781        }
782        let tll_flat = tll_flat.into_boxed_slice();
783
784        // emu2413.cpp:398-405 — buildRksTable
785        let mut rks = [[0u8; 2]; 16];
786        for fnum8 in 0..2usize {
787            for block in 0..8usize {
788                let idx = (block << 1) | fnum8;
789                rks[idx][1] = ((block << 1) + fnum8) as u8;
790                rks[idx][0] = (block >> 1) as u8;
791            }
792        }
793
794        Self { tll_flat, rks }
795    }
796}
797
798// ---------------------------------------------------------------------------
799// Operator output stage — emu2413.cpp:911-925
800// ---------------------------------------------------------------------------
801
802/// Decode a 16-bit log-domain magnitude into a 13-bit linear sample
803/// (-4095..=4095). Direct port of emu2413.cpp:911-916.
804///
805/// Layout of `i`:
806/// - bit 15: sign
807/// - bits 14-8: exponent (right-shift amount)
808/// - bits 7-0: mantissa index into [`EXP_TABLE`]
809#[inline]
810fn lookup_exp_table(i: u32) -> i16 {
811    // From andete's expression.  The C code on x86 implicitly masks the
812    // shift to bits [5:0] (the hardware's `shr` masking behavior). We
813    // mirror that here: shifts >= 32 saturate to "fully attenuated"
814    // which is what emu2413 produces on x86 / ARM (the only platforms
815    // it runs on).  Without the mask, debug builds in Rust panic on
816    // `shr-overflow`.
817    let t = i32::from(EXP_TABLE[((i & 0xff) ^ 0xff) as usize]) + 1024;
818    let shift = ((i & 0x7f00) >> 8) & 31;
819    let res = t >> shift;
820    let signed = if (i & 0x8000) != 0 { !res } else { res };
821    (signed << 1) as i16
822}
823
824/// Convert a wave-table log-magnitude `h` to a linear sample, applying
825/// the slot's envelope + total-level + AM offset. Direct port of
826/// emu2413.cpp:918-925.
827#[inline]
828fn to_linear(h: u16, slot: &Slot, am: u8) -> i16 {
829    if slot.eg_out > EG_MAX {
830        return 0;
831    }
832    let att = (slot.eg_out + u32::from(slot.tll) + u32::from(am)).min(EG_MUTE) << 4;
833    lookup_exp_table(u32::from(h) + att)
834}
835
836// ---------------------------------------------------------------------------
837// Opll — the chip instance.
838// ---------------------------------------------------------------------------
839
840/// OPLL (YM2413 / VRC7) FM synthesizer instance.
841///
842/// One instance per VRC7-mapped cartridge. Caller drives the chip via
843/// [`Opll::write_reg`] and pulls samples via [`Opll::calc`] at the
844/// OPLL's native 49,716 Hz sample rate.
845///
846/// # Example
847///
848/// ```ignore
849/// // VRC7-mode chip for Lagrange Point
850/// let mut opll = Opll::new(ChipType::Vrc7);
851/// opll.write_reg(0x30, 0x01); // channel 0 instrument = patch 1
852/// opll.write_reg(0x10, 0x80); // channel 0 fnum low
853/// opll.write_reg(0x20, 0x15); // channel 0 fnum high + block + key-on
854/// let sample: i16 = opll.calc();
855/// ```
856#[derive(Clone)]
857pub struct Opll {
858    chip_type: ChipType,
859
860    /// Current register address (set by writes to `$9010` on VRC7).
861    adr: u8,
862
863    /// All 64 OPLL registers (shadow).
864    reg: [u8; 0x40],
865
866    /// Test flag (register $0F bit 4).
867    test_flag: u8,
868
869    /// Bit mask of key-on slots (1 bit per slot).
870    slot_key_status: u32,
871
872    /// EG global counter (drives envelope timing).
873    eg_counter: u32,
874
875    /// PM (pitch modulation) LFO phase.
876    pm_phase: u32,
877    /// AM (amplitude modulation) LFO phase.
878    am_phase: i32,
879    /// Current AM LFO output value (0..13).
880    lfo_am: u8,
881
882    /// Per-channel patch number (0-15; 0=user patch).
883    patch_number: [i32; 9],
884
885    /// 18 slots (9 channels × 2 ops). VRC7 only uses indices [0..12).
886    slot: [Slot; 18],
887
888    /// Loaded patch set: 19 slots (16 melodic + 3 rhythm) × 2 ops each.
889    /// Index 0 is the user patch (writeable via $00-$07).
890    patch_set: Vec<Patch>,
891
892    /// Per-channel output sample (after operator + envelope).
893    ch_out: [i16; 14],
894
895    /// Mixed mono output.
896    mix_out: i16,
897
898    /// Full 1024-entry sine + half-sine wave tables (built at
899    /// construction).
900    waves: WaveTables,
901
902    /// TLL + RKS tables (built at construction).  TLL is heap-allocated
903    /// (~128 KiB) since it indexes `[128][64][4]` of `u32`.
904    tll_rks: TllRksTables,
905}
906
907impl Opll {
908    /// Construct a new OPLL instance for the given chip type.
909    ///
910    /// VRC7 mode loads the Konami custom patch set (the Nuke.YKT
911    /// analysis values) — this is the table Lagrange Point uses.
912    pub fn new(chip_type: ChipType) -> Self {
913        let mut opll = Self {
914            chip_type,
915            adr: 0,
916            reg: [0; 0x40],
917            test_flag: 0,
918            slot_key_status: 0,
919            eg_counter: 0,
920            pm_phase: 0,
921            am_phase: 0,
922            lfo_am: 0,
923            patch_number: [0; 9],
924            slot: [Slot::default(); 18],
925            patch_set: vec![Patch::default(); 19 * 2],
926            ch_out: [0; 14],
927            mix_out: 0,
928            waves: WaveTables::new(),
929            tll_rks: TllRksTables::new(),
930        };
931        opll.reset_patch(chip_type);
932        opll.reset();
933        opll
934    }
935
936    /// Reset all channel/operator state. Patches are preserved.
937    pub fn reset(&mut self) {
938        self.adr = 0;
939        self.reg = [0; 0x40];
940        self.test_flag = 0;
941        self.slot_key_status = 0;
942        self.eg_counter = 0;
943        self.pm_phase = 0;
944        self.am_phase = 0;
945        self.lfo_am = 0;
946        self.patch_number = [0; 9];
947        // Per emu2413.cpp:561-582 each slot enters Release with eg_out
948        // at EG_MUTE (max attenuation) — ready for the next key-on.
949        for (i, s) in self.slot.iter_mut().enumerate() {
950            s.reset_to_release(i as u8);
951        }
952        self.ch_out = [0; 14];
953        self.mix_out = 0;
954    }
955
956    /// Load the patch ROM for a chip type.
957    pub fn reset_patch(&mut self, chip_type: ChipType) {
958        let dump = chip_type.patch_dump();
959        for i in 0..19 {
960            let chunk: &[u8; 8] = (&dump[i * 8..i * 8 + 8]).try_into().unwrap();
961            self.patch_set[i * 2] = Patch::from_dump_modulator(chunk);
962            self.patch_set[i * 2 + 1] = Patch::from_dump_carrier(chunk);
963        }
964    }
965
966    /// Write `val` to OPLL register `reg` (0x00..=0x3F). Larger
967    /// addresses are masked to 6 bits. Direct port of
968    /// `OPLL_writeReg` in emu2413.cpp:1223-1394.
969    ///
970    /// This is the entry point VRC7 calls when the CPU writes to
971    /// `$9030` (after latching the register address via `$9010`).
972    /// The decoder routes the write to the appropriate channel /
973    /// patch / control surface and schedules per-slot
974    /// `commit_slot_update` for the next OPLL tick.
975    ///
976    /// VRC7-specific behaviour (`chip_type == Vrc7`):
977    /// - `$0E` (rhythm mode) is ignored — VRC7 has no rhythm channels
978    /// - Register addresses for channels 6, 7, 8 (`$16+`, `$26+`,
979    ///   `$36+`) are ignored — VRC7 wires only 6 melodic channels
980    #[allow(clippy::too_many_lines)]
981    pub fn write_reg(&mut self, reg: u8, val: u8) {
982        if reg >= 0x40 {
983            return;
984        }
985
986        // Mirror registers (emu2413.cpp:1230-1232): `$19-$1F` → `$10-$16`,
987        // `$29-$2F` → `$20-$26`, `$39-$3F` → `$30-$36`.
988        let reg = if (0x19..=0x1F).contains(&reg)
989            || (0x29..=0x2F).contains(&reg)
990            || (0x39..=0x3F).contains(&reg)
991        {
992            reg - 9
993        } else {
994            reg
995        };
996        self.reg[reg as usize] = val;
997
998        let is_vrc7 = self.chip_type == ChipType::Vrc7;
999
1000        match reg {
1001            // ---- $00-$07: user patch (patch[0] = modulator, patch[1] = carrier) ----
1002            0x00 => {
1003                self.patch_set[0].am = (val >> 7) & 1;
1004                self.patch_set[0].pm = (val >> 6) & 1;
1005                self.patch_set[0].eg = (val >> 5) & 1;
1006                self.patch_set[0].kr = (val >> 4) & 1;
1007                self.patch_set[0].ml = val & 0x0f;
1008                for ch in 0..9 {
1009                    if self.patch_number[ch] == 0 {
1010                        self.slot[ch * 2].update_requests |= UPDATE_RKS | UPDATE_EG;
1011                    }
1012                }
1013                self.refresh_user_patch_pointers();
1014            }
1015            0x01 => {
1016                self.patch_set[1].am = (val >> 7) & 1;
1017                self.patch_set[1].pm = (val >> 6) & 1;
1018                self.patch_set[1].eg = (val >> 5) & 1;
1019                self.patch_set[1].kr = (val >> 4) & 1;
1020                self.patch_set[1].ml = val & 0x0f;
1021                for ch in 0..9 {
1022                    if self.patch_number[ch] == 0 {
1023                        self.slot[ch * 2 + 1].update_requests |= UPDATE_RKS | UPDATE_EG;
1024                    }
1025                }
1026                self.refresh_user_patch_pointers();
1027            }
1028            0x02 => {
1029                self.patch_set[0].kl = (val >> 6) & 3;
1030                self.patch_set[0].tl = val & 0x3f;
1031                for ch in 0..9 {
1032                    if self.patch_number[ch] == 0 {
1033                        self.slot[ch * 2].update_requests |= UPDATE_TLL;
1034                    }
1035                }
1036                self.refresh_user_patch_pointers();
1037            }
1038            0x03 => {
1039                self.patch_set[1].kl = (val >> 6) & 3;
1040                self.patch_set[1].ws = (val >> 4) & 1;
1041                self.patch_set[0].ws = (val >> 3) & 1;
1042                self.patch_set[0].fb = val & 7;
1043                for ch in 0..9 {
1044                    if self.patch_number[ch] == 0 {
1045                        self.slot[ch * 2].update_requests |= UPDATE_WS;
1046                        self.slot[ch * 2 + 1].update_requests |= UPDATE_WS | UPDATE_TLL;
1047                    }
1048                }
1049                self.refresh_user_patch_pointers();
1050            }
1051            0x04 => {
1052                self.patch_set[0].ar = (val >> 4) & 0x0f;
1053                self.patch_set[0].dr = val & 0x0f;
1054                for ch in 0..9 {
1055                    if self.patch_number[ch] == 0 {
1056                        self.slot[ch * 2].update_requests |= UPDATE_EG;
1057                    }
1058                }
1059                self.refresh_user_patch_pointers();
1060            }
1061            0x05 => {
1062                self.patch_set[1].ar = (val >> 4) & 0x0f;
1063                self.patch_set[1].dr = val & 0x0f;
1064                for ch in 0..9 {
1065                    if self.patch_number[ch] == 0 {
1066                        self.slot[ch * 2 + 1].update_requests |= UPDATE_EG;
1067                    }
1068                }
1069                self.refresh_user_patch_pointers();
1070            }
1071            0x06 => {
1072                self.patch_set[0].sl = (val >> 4) & 0x0f;
1073                self.patch_set[0].rr = val & 0x0f;
1074                for ch in 0..9 {
1075                    if self.patch_number[ch] == 0 {
1076                        self.slot[ch * 2].update_requests |= UPDATE_EG;
1077                    }
1078                }
1079                self.refresh_user_patch_pointers();
1080            }
1081            0x07 => {
1082                self.patch_set[1].sl = (val >> 4) & 0x0f;
1083                self.patch_set[1].rr = val & 0x0f;
1084                for ch in 0..9 {
1085                    if self.patch_number[ch] == 0 {
1086                        self.slot[ch * 2 + 1].update_requests |= UPDATE_EG;
1087                    }
1088                }
1089                self.refresh_user_patch_pointers();
1090            }
1091
1092            // ---- $0E: rhythm mode (VRC7 ignores; YM2413 not yet implemented) ----
1093            0x0E => {
1094                // VRC7 has no rhythm channels; ignore.
1095                // Full YM2413 rhythm-mode handling would go here.
1096            }
1097
1098            // ---- $0F: test flag ----
1099            0x0F => {
1100                self.test_flag = val;
1101            }
1102
1103            // ---- $10-$18: per-channel fnum low byte (VRC7 caps at $15) ----
1104            0x10..=0x18 => {
1105                let ch = (reg - 0x10) as usize;
1106                if is_vrc7 && reg >= 0x16 {
1107                    return;
1108                }
1109                let fnum_high_bit = u16::from(self.reg[0x20 + ch] & 1);
1110                let fnum = u16::from(val) | (fnum_high_bit << 8);
1111                self.set_fnumber_internal(ch, fnum);
1112            }
1113
1114            // ---- $20-$28: per-channel fnum-high + block + key-on + sustain ----
1115            0x20..=0x28 => {
1116                let ch = (reg - 0x20) as usize;
1117                if is_vrc7 && reg >= 0x26 {
1118                    return;
1119                }
1120                let fnum = (u16::from(val & 1) << 8) | u16::from(self.reg[0x10 + ch]);
1121                self.set_fnumber_internal(ch, fnum);
1122                let blk = (val >> 1) & 7;
1123                self.set_block_internal(ch, blk);
1124                let sus = (val >> 5) & 1;
1125                self.set_sus_flag_internal(ch, sus);
1126                self.update_key_status();
1127            }
1128
1129            // ---- $30-$38: per-channel volume + instrument select ----
1130            0x30..=0x38 => {
1131                let ch = (reg - 0x30) as usize;
1132                if is_vrc7 && reg >= 0x36 {
1133                    return;
1134                }
1135                let inst = (val >> 4) & 0x0f;
1136                self.set_patch_internal(ch, usize::from(inst));
1137                let vol = i32::from((val & 0x0f) << 2);
1138                self.set_volume_internal(ch, vol);
1139            }
1140
1141            _ => {}
1142        }
1143    }
1144
1145    /// Re-point the channels currently using the user patch (number 0)
1146    /// at the freshly-rewritten patch_set[0] / patch_set[1] entries.
1147    /// Each slot caches a copy of its `Patch` for hot-path access, so
1148    /// patch-modifying writes to `$00-$07` must propagate the new
1149    /// fields into the slots.
1150    fn refresh_user_patch_pointers(&mut self) {
1151        for ch in 0..9 {
1152            if self.patch_number[ch] == 0 {
1153                self.slot[ch * 2].patch = self.patch_set[0];
1154                self.slot[ch * 2 + 1].patch = self.patch_set[1];
1155            }
1156        }
1157    }
1158
1159    /// Production set_patch: assigns instrument `num` to channel `ch`
1160    /// and requests slot recomputation. Mirrors `set_patch` in
1161    /// emu2413.cpp:645-651.
1162    fn set_patch_internal(&mut self, ch: usize, num: usize) {
1163        if ch >= 9 || num * 2 + 1 >= self.patch_set.len() {
1164            return;
1165        }
1166        self.patch_number[ch] = num as i32;
1167        self.slot[ch * 2].patch = self.patch_set[num * 2];
1168        self.slot[ch * 2 + 1].patch = self.patch_set[num * 2 + 1];
1169        self.slot[ch * 2].update_requests |= UPDATE_ALL;
1170        self.slot[ch * 2 + 1].update_requests |= UPDATE_ALL;
1171    }
1172
1173    fn set_fnumber_internal(&mut self, ch: usize, fnum: u16) {
1174        if ch >= 9 {
1175            return;
1176        }
1177        let f = fnum & 0x1ff;
1178        for slot_idx in [ch * 2, ch * 2 + 1] {
1179            self.slot[slot_idx].fnum = f;
1180            self.slot[slot_idx].blk_fnum = (self.slot[slot_idx].blk_fnum & 0xe00) | f;
1181            self.slot[slot_idx].update_requests |= UPDATE_EG | UPDATE_RKS | UPDATE_TLL;
1182        }
1183    }
1184
1185    fn set_block_internal(&mut self, ch: usize, blk: u8) {
1186        if ch >= 9 {
1187            return;
1188        }
1189        let b = blk & 7;
1190        for slot_idx in [ch * 2, ch * 2 + 1] {
1191            self.slot[slot_idx].blk = b;
1192            self.slot[slot_idx].blk_fnum =
1193                (u16::from(b) << 9) | (self.slot[slot_idx].blk_fnum & 0x1ff);
1194            self.slot[slot_idx].update_requests |= UPDATE_EG | UPDATE_RKS | UPDATE_TLL;
1195        }
1196    }
1197
1198    fn set_sus_flag_internal(&mut self, ch: usize, sus: u8) {
1199        if ch >= 9 {
1200            return;
1201        }
1202        self.slot[ch * 2 + 1].sus_flag = sus;
1203        self.slot[ch * 2 + 1].update_requests |= UPDATE_EG;
1204        // For the rhythm "single slot mode" carriers we'd also set
1205        // the modulator's sus_flag; not relevant to VRC7's 6 melodic
1206        // channels (none have type & 1 == 1 for the modulator).
1207    }
1208
1209    fn set_volume_internal(&mut self, ch: usize, volume: i32) {
1210        if ch >= 9 {
1211            return;
1212        }
1213        self.slot[ch * 2 + 1].volume = volume;
1214        self.slot[ch * 2 + 1].update_requests |= UPDATE_TLL;
1215    }
1216
1217    /// Update the key-on/off state across all 9 channels based on
1218    /// the current `$20-$28` bit 4 values. Mirrors
1219    /// `update_key_status` in emu2413.cpp:600-643.
1220    ///
1221    /// VRC7-only path (no rhythm). For each channel: bit 4 of `$2x`
1222    /// is the key-on flag; this routine compares against the prior
1223    /// `slot_key_status` snapshot and issues `slot_on` / `slot_off`
1224    /// only on transitions.
1225    fn update_key_status(&mut self) {
1226        let mut new_status: u32 = 0;
1227        let ch_count = if self.chip_type == ChipType::Vrc7 {
1228            6
1229        } else {
1230            9
1231        };
1232        for ch in 0..ch_count {
1233            if self.reg[0x20 + ch] & 0x10 != 0 {
1234                new_status |= 3u32 << (ch * 2);
1235            }
1236        }
1237        let changed = self.slot_key_status ^ new_status;
1238        if changed != 0 {
1239            for i in 0..18 {
1240                if (changed >> i) & 1 != 0 {
1241                    if (new_status >> i) & 1 != 0 {
1242                        self.slot_on(i);
1243                    } else {
1244                        self.slot_off(i);
1245                    }
1246                }
1247            }
1248        }
1249        self.slot_key_status = new_status;
1250    }
1251
1252    /// Slot key-on. Sets `key_flag`, enters Damp state, requests EG
1253    /// recompute. Per `slotOn` in emu2413.cpp:584-589.
1254    fn slot_on(&mut self, slot_idx: usize) {
1255        if slot_idx >= 18 {
1256            return;
1257        }
1258        self.slot[slot_idx].key_flag = 1;
1259        self.slot[slot_idx].eg_state = EgState::Damp;
1260        self.slot[slot_idx].update_requests |= UPDATE_EG;
1261    }
1262
1263    /// Slot key-off. For carriers (type & 1 == 1), enters Release;
1264    /// for modulators, just clears key_flag. Per `slotOff` in
1265    /// emu2413.cpp:591-598.
1266    fn slot_off(&mut self, slot_idx: usize) {
1267        if slot_idx >= 18 {
1268            return;
1269        }
1270        self.slot[slot_idx].key_flag = 0;
1271        if self.slot[slot_idx].type_flags & 1 != 0 {
1272            self.slot[slot_idx].eg_state = EgState::Release;
1273            self.slot[slot_idx].update_requests |= UPDATE_EG;
1274        }
1275    }
1276
1277    /// Read a register shadow byte (debugger / save-state helper).
1278    #[must_use]
1279    pub fn read_reg(&self, reg: u8) -> u8 {
1280        self.reg[(reg & 0x3F) as usize]
1281    }
1282
1283    /// Generate one mono sample at the OPLL's native 49,716 Hz rate.
1284    ///
1285    /// Drives the full per-clock pipeline: AM/PM LFO update → per-slot
1286    /// commit_slot_update / calc_envelope / calc_phase → per-channel
1287    /// 2-op FM output (modulator with self-feedback, carrier modulated
1288    /// by modulator's output) → channel summation. For VRC7 (chip type
1289    /// 1), only the 6 melodic channels are summed; the rhythm channels
1290    /// in slots 12..18 are not used.
1291    ///
1292    /// With no slot keyed on, `calc` produces silence — but the full
1293    /// pipeline still runs every call (so AM/PM/EG advance), keeping the
1294    /// per-clock cost constant regardless of channel activity.
1295    pub fn calc(&mut self) -> i16 {
1296        self.update_output();
1297        self.mix_output();
1298        self.mix_out
1299    }
1300
1301    /// Returns the chip type.
1302    #[must_use]
1303    pub const fn chip_type(&self) -> ChipType {
1304        self.chip_type
1305    }
1306
1307    /// AM + PM LFO update (emu2413.cpp:730-739).
1308    fn update_ampm(&mut self) {
1309        if self.test_flag & 2 != 0 {
1310            self.pm_phase = 0;
1311            self.am_phase = 0;
1312        } else {
1313            let pm_inc: u32 = if self.test_flag & 8 != 0 { 1024 } else { 1 };
1314            let am_inc: i32 = if self.test_flag & 8 != 0 { 64 } else { 1 };
1315            self.pm_phase = self.pm_phase.wrapping_add(pm_inc);
1316            self.am_phase = self.am_phase.wrapping_add(am_inc);
1317        }
1318        let idx = ((self.am_phase >> 6) as usize) % AM_TABLE.len();
1319        self.lfo_am = AM_TABLE[idx];
1320    }
1321
1322    /// Get the effective rate for `get_parameter_rate` (emu2413.cpp:476-502).
1323    fn get_parameter_rate(slot: &Slot) -> u8 {
1324        if (slot.type_flags & 1) == 0 && slot.key_flag == 0 {
1325            return 0;
1326        }
1327        match slot.eg_state {
1328            EgState::Attack => slot.patch.ar,
1329            EgState::Decay => slot.patch.dr,
1330            EgState::Sustain => {
1331                if slot.patch.eg != 0 {
1332                    0
1333                } else {
1334                    slot.patch.rr
1335                }
1336            }
1337            EgState::Release => {
1338                if slot.sus_flag != 0 {
1339                    5
1340                } else if slot.patch.eg != 0 {
1341                    slot.patch.rr
1342                } else {
1343                    7
1344                }
1345            }
1346            EgState::Damp => DAMPER_RATE,
1347            EgState::Unknown => 0,
1348        }
1349    }
1350
1351    /// Commit pending update requests for slot `i` (emu2413.cpp:514-559).
1352    /// Translates patch/fnum/volume changes into the cached rate / level
1353    /// state the per-clock DSP reads.
1354    fn commit_slot_update(&mut self, i: usize) {
1355        let requests = self.slot[i].update_requests;
1356        if requests == 0 {
1357            return;
1358        }
1359
1360        // Snapshot slot fields we need for table lookups (avoids overlapping borrows).
1361        let blk_fnum = self.slot[i].blk_fnum;
1362        let patch_ws = self.slot[i].patch.ws;
1363        let patch_tl = usize::from(self.slot[i].patch.tl);
1364        let patch_kl = usize::from(self.slot[i].patch.kl);
1365        let patch_kr = usize::from(self.slot[i].patch.kr);
1366        let volume = self.slot[i].volume as usize;
1367        let is_carrier = self.slot[i].type_flags & 1 != 0;
1368
1369        if requests & UPDATE_WS != 0 {
1370            self.slot[i].wave_table_idx = patch_ws;
1371        }
1372        if requests & UPDATE_TLL != 0 {
1373            let row = (blk_fnum >> 5) as usize;
1374            self.slot[i].tll = if is_carrier {
1375                self.tll_rks.tll_at(row, volume.min(63), patch_kl) as u16
1376            } else {
1377                self.tll_rks.tll_at(row, patch_tl, patch_kl) as u16
1378            };
1379        }
1380        if requests & UPDATE_RKS != 0 {
1381            let row = (blk_fnum >> 8) as usize;
1382            self.slot[i].rks = self.tll_rks.rks[row][patch_kr];
1383        }
1384
1385        if requests & (UPDATE_RKS | UPDATE_EG) != 0 {
1386            let p_rate = Self::get_parameter_rate(&self.slot[i]);
1387            if p_rate == 0 {
1388                self.slot[i].eg_shift = 0;
1389                self.slot[i].eg_rate_h = 0;
1390                self.slot[i].eg_rate_l = 0;
1391                self.slot[i].update_requests = 0;
1392                return;
1393            }
1394            let rks_h2 = self.slot[i].rks >> 2;
1395            self.slot[i].eg_rate_h = (p_rate + rks_h2).min(15);
1396            self.slot[i].eg_rate_l = self.slot[i].rks & 3;
1397            let eg_state = self.slot[i].eg_state;
1398            let eg_rate_h = self.slot[i].eg_rate_h;
1399            self.slot[i].eg_shift = if eg_state == EgState::Attack {
1400                if eg_rate_h > 0 && eg_rate_h < 12 {
1401                    u32::from(13 - eg_rate_h)
1402                } else {
1403                    0
1404                }
1405            } else if eg_rate_h < 13 {
1406                u32::from(13 - eg_rate_h)
1407            } else {
1408                0
1409            };
1410        }
1411
1412        self.slot[i].update_requests = 0;
1413    }
1414
1415    /// Run one OPLL clock: advance EG counter, then for each of 18
1416    /// slots: commit pending updates → run envelope → run phase
1417    /// (emu2413.cpp:889-908).
1418    fn update_slots(&mut self) {
1419        self.eg_counter = self.eg_counter.wrapping_add(1);
1420        for i in 0..18 {
1421            if self.slot[i].update_requests != 0 {
1422                self.commit_slot_update(i);
1423            }
1424            // Buddy lookup for the rare Damp→Attack carrier transition.
1425            // For VRC7 melodic channels neither slot has pg_keep set so
1426            // this defaults to false; the rhythm path is non-VRC7.
1427            let buddy_pg_keep = if i & 1 == 0 {
1428                // modulator (even) — buddy = carrier at i+1
1429                self.slot.get(i + 1).is_some_and(|s| s.pg_keep != 0)
1430            } else {
1431                // carrier (odd) — buddy = modulator at i-1
1432                self.slot[i - 1].pg_keep != 0
1433            };
1434            let test = self.test_flag & 1;
1435            let step = self.slot[i].calc_envelope(buddy_pg_keep, self.eg_counter, test);
1436            if step == EnvelopeStep::ResetBuddyPhase {
1437                // Carrier just transitioned Damp→Attack; reset modulator.
1438                let buddy_idx = i ^ 1;
1439                self.slot[buddy_idx].pg_phase = 0;
1440            }
1441            let pm_phase_i32 = self.pm_phase as i32;
1442            let pg_reset = self.test_flag & 4 != 0;
1443            self.slot[i].calc_phase(pm_phase_i32, pg_reset);
1444        }
1445    }
1446
1447    /// Generate one modulator-slot sample. The modulator's own previous
1448    /// output is fed back into its phase index per emu2413.cpp:938-948.
1449    fn calc_slot_mod(&mut self, ch: usize) -> i16 {
1450        let mod_idx = ch * 2;
1451        let fb = self.slot[mod_idx].patch.fb;
1452        let am = if self.slot[mod_idx].patch.am != 0 {
1453            self.lfo_am
1454        } else {
1455            0
1456        };
1457
1458        let fm = if fb > 0 {
1459            // (output[0] + output[1]) >> (9 - FB)
1460            let sum = self.slot[mod_idx].output[0] + self.slot[mod_idx].output[1];
1461            (sum >> (9 - fb)) as i16
1462        } else {
1463            0
1464        };
1465
1466        let pg_out = self.slot[mod_idx].pg_out;
1467        let wave_idx = self.slot[mod_idx].wave_table_idx;
1468        let phase = pg_out.wrapping_add(fm as u32) & (PG_WIDTH as u32 - 1);
1469        let h = self.waves.sample(wave_idx, phase);
1470
1471        let out = to_linear(h, &self.slot[mod_idx], am);
1472        self.slot[mod_idx].output[1] = self.slot[mod_idx].output[0];
1473        self.slot[mod_idx].output[0] = i32::from(out);
1474        out
1475    }
1476
1477    /// Generate one carrier-slot sample. The carrier takes the
1478    /// modulator's output as FM input per emu2413.cpp:927-936.
1479    fn calc_slot_car(&mut self, ch: usize, fm: i16) -> i16 {
1480        let car_idx = ch * 2 + 1;
1481        let am = if self.slot[car_idx].patch.am != 0 {
1482            self.lfo_am
1483        } else {
1484            0
1485        };
1486
1487        // Phase index = pg_out + 2 * (fm >> 1), mask to PG_WIDTH.
1488        let pg_out = self.slot[car_idx].pg_out;
1489        let wave_idx = self.slot[car_idx].wave_table_idx;
1490        let phase_offset = (2 * i32::from(fm >> 1)) as u32;
1491        let phase = pg_out.wrapping_add(phase_offset) & (PG_WIDTH as u32 - 1);
1492        let h = self.waves.sample(wave_idx, phase);
1493
1494        let out = to_linear(h, &self.slot[car_idx], am);
1495        self.slot[car_idx].output[1] = self.slot[car_idx].output[0];
1496        self.slot[car_idx].output[0] = i32::from(out);
1497        out
1498    }
1499
1500    /// Drive one chip-clock of output: LFO + slots + 6 channel outputs
1501    /// (VRC7 path of emu2413.cpp:996-1058; rhythm path elided).
1502    fn update_output(&mut self) {
1503        self.update_ampm();
1504        self.update_slots();
1505        // VRC7 melodic channels (6 channels, slots 0..12).
1506        for ch in 0..6 {
1507            let fm = self.calc_slot_mod(ch);
1508            let car_out = self.calc_slot_car(ch, fm);
1509            // _MO(x) = -(x) >> 1
1510            self.ch_out[ch] = ((-i32::from(car_out)) >> 1) as i16;
1511        }
1512    }
1513
1514    /// Sum the 6 VRC7 channel outputs into `mix_out`
1515    /// (emu2413.cpp:1060-1077, chip_type != 0 path).
1516    fn mix_output(&mut self) {
1517        let mut sum: i32 = 0;
1518        for ch in 0..6 {
1519            sum += i32::from(self.ch_out[ch]);
1520        }
1521        self.mix_out = sum.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16;
1522    }
1523
1524    // ---- Test / register-write helpers (used by Sprint 1.2 register
1525    // decoder + by unit tests in this sprint) ----
1526
1527    /// Assign the patch at `num` to channel `ch` and request
1528    /// recomputation of all derived slot state. Mirrors `set_patch`
1529    /// in emu2413.cpp:645-651.
1530    #[cfg(test)]
1531    fn set_patch(&mut self, ch: usize, num: usize) {
1532        self.patch_number[ch] = num as i32;
1533        self.slot[ch * 2].patch = self.patch_set[num * 2];
1534        self.slot[ch * 2 + 1].patch = self.patch_set[num * 2 + 1];
1535        self.slot[ch * 2].update_requests |= UPDATE_ALL;
1536        self.slot[ch * 2 + 1].update_requests |= UPDATE_ALL;
1537    }
1538
1539    /// Set channel `ch`'s 9-bit fnum on both modulator and carrier.
1540    /// Mirrors `set_fnumber` in emu2413.cpp:673-683.
1541    #[cfg(test)]
1542    fn set_fnumber(&mut self, ch: usize, fnum: u16) {
1543        let car = ch * 2 + 1;
1544        let mod_ = ch * 2;
1545        self.slot[car].fnum = fnum & 0x1ff;
1546        self.slot[car].blk_fnum = (self.slot[car].blk_fnum & 0xe00) | (fnum & 0x1ff);
1547        self.slot[mod_].fnum = fnum & 0x1ff;
1548        self.slot[mod_].blk_fnum = (self.slot[mod_].blk_fnum & 0xe00) | (fnum & 0x1ff);
1549        self.slot[car].update_requests |= UPDATE_EG | UPDATE_RKS | UPDATE_TLL;
1550        self.slot[mod_].update_requests |= UPDATE_EG | UPDATE_RKS | UPDATE_TLL;
1551    }
1552
1553    /// Set channel `ch`'s 3-bit block on both modulator and carrier.
1554    /// Mirrors `set_block` in emu2413.cpp:685-695.
1555    #[cfg(test)]
1556    fn set_block(&mut self, ch: usize, blk: u8) {
1557        let car = ch * 2 + 1;
1558        let mod_ = ch * 2;
1559        let blk_low3 = blk & 7;
1560        self.slot[car].blk = blk_low3;
1561        self.slot[car].blk_fnum = (u16::from(blk_low3) << 9) | (self.slot[car].blk_fnum & 0x1ff);
1562        self.slot[mod_].blk = blk_low3;
1563        self.slot[mod_].blk_fnum = (u16::from(blk_low3) << 9) | (self.slot[mod_].blk_fnum & 0x1ff);
1564        self.slot[car].update_requests |= UPDATE_EG | UPDATE_RKS | UPDATE_TLL;
1565        self.slot[mod_].update_requests |= UPDATE_EG | UPDATE_RKS | UPDATE_TLL;
1566    }
1567
1568    /// Set channel `ch`'s carrier volume (6-bit, post-`<< 2` from
1569    /// register data). Mirrors `set_volume` in emu2413.cpp:663-666.
1570    #[cfg(test)]
1571    fn set_volume(&mut self, ch: usize, volume: i32) {
1572        let car = ch * 2 + 1;
1573        self.slot[car].volume = volume;
1574        self.slot[car].update_requests |= UPDATE_TLL;
1575    }
1576
1577    /// Key on channel `ch` (start envelope from Damp). Mirrors
1578    /// `slotOn` per `update_key_status` (emu2413.cpp:584-589).
1579    #[cfg(test)]
1580    fn key_on(&mut self, ch: usize) {
1581        for slot_idx in [ch * 2, ch * 2 + 1] {
1582            self.slot[slot_idx].key_flag = 1;
1583            self.slot[slot_idx].eg_state = EgState::Damp;
1584            self.slot[slot_idx].update_requests |= UPDATE_EG;
1585        }
1586    }
1587}
1588
1589// ---------------------------------------------------------------------------
1590// Save-state surface (v2.3.7 — closes the `docs/accuracy-ledger.md` OPLL row)
1591// ---------------------------------------------------------------------------
1592
1593/// Errors returned by [`Opll::restore`].
1594///
1595/// Deliberately its own type rather than `ApuSnapshotError`: the OPLL blob does
1596/// not ride in the APU section of a save state. It rides in the **mapper**
1597/// section of whichever board carries the chip (VRC7 today), which is versioned
1598/// independently of `APU_SNAPSHOT_VERSION`. Sharing an error type would imply a
1599/// coupling between two schemas that must be free to move apart.
1600#[derive(Debug, thiserror::Error)]
1601#[non_exhaustive]
1602pub enum OpllStateError {
1603    /// Blob is shorter than the schema declares.
1604    #[error("OPLL snapshot truncated at offset {0}")]
1605    Truncated(usize),
1606    /// The blob's version byte is not understood by this build.
1607    #[error("OPLL snapshot unsupported version {0}")]
1608    UnsupportedVersion(u8),
1609    /// The blob was written by a differently-configured chip (YM2413 state
1610    /// restored into a VRC7 instance, or the reverse). The patch ROM differs
1611    /// between them, so the slot patches would be reinterpreted against the
1612    /// wrong instrument set.
1613    #[error("OPLL snapshot chip type {got} does not match this instance ({want})")]
1614    ChipTypeMismatch {
1615        /// The tag read from the blob.
1616        got: u8,
1617        /// The tag this instance was constructed with.
1618        want: u8,
1619    },
1620    /// An envelope-generator state tag outside the six defined variants.
1621    #[error("OPLL snapshot has invalid envelope-generator state tag {0}")]
1622    InvalidEgState(u8),
1623}
1624
1625/// Schema version of the blob [`Opll::snapshot`] emits.
1626pub const OPLL_SNAPSHOT_VERSION: u8 = 1;
1627
1628/// Number of slots (operators) carried. 18 in the YM2413; the VRC7 uses the
1629/// first 12, but all 18 are serialized so the same blob describes either chip.
1630const SNAPSHOT_SLOTS: usize = 18;
1631
1632/// Serialized size of one [`Slot`], in bytes. Asserted against the writer by
1633/// `slot_serialized_size_matches_the_declared_constant` so the two cannot drift.
1634const SLOT_BYTES: usize = 62;
1635
1636/// Largest `eg_shift` the envelope generator can legitimately produce.
1637///
1638/// `commit_slot_update` assigns `13 - eg_rate_h` with `eg_rate_h <= 13`, so the
1639/// value is always in `0..=13`. It matters on restore because `calc_envelope`
1640/// evaluates `1u32 << eg_shift`, which panics at 32 and above.
1641const EG_SHIFT_MAX: u32 = 13;
1642
1643/// Highest instrument number a channel can select: the `$3x` high nibble is four
1644/// bits, and index 0 is the user patch.
1645const MAX_PATCH_NUMBER: i32 = 15;
1646
1647/// Clamp a restored operator output into the range synthesis can actually
1648/// produce. See the call site for why the field is wider than its contents.
1649const fn clamp_i16(v: i32) -> i32 {
1650    if v < i16::MIN as i32 {
1651        i16::MIN as i32
1652    } else if v > i16::MAX as i32 {
1653        i16::MAX as i32
1654    } else {
1655        v
1656    }
1657}
1658
1659/// Serialized size of one [`Patch`], in bytes (13 one-byte parameters).
1660const PATCH_BYTES: usize = 13;
1661
1662/// Total serialized size of an OPLL snapshot, in bytes.
1663///
1664/// version(1) + chip_type(1) + adr(1) + reg(64) + test_flag(1)
1665///   + slot_key_status(4) + eg_counter(4) + pm_phase(4) + am_phase(4)
1666///   + lfo_am(1) + patch_number(9x4) + user patch pair(2x13)
1667///   + slot(18x62) + ch_out(14x2) + mix_out(2)
1668pub const OPLL_SNAPSHOT_LEN: usize = 1
1669    + 1
1670    + 1
1671    + 0x40
1672    + 1
1673    + 4
1674    + 4
1675    + 4
1676    + 4
1677    + 1
1678    + 9 * 4
1679    + 2 * PATCH_BYTES
1680    + SNAPSHOT_SLOTS * SLOT_BYTES
1681    + 14 * 2
1682    + 2;
1683
1684impl EgState {
1685    /// Stable on-disk tag. Explicit rather than `as u8` so reordering the enum
1686    /// cannot silently reinterpret existing save states.
1687    const fn to_tag(self) -> u8 {
1688        match self {
1689            Self::Attack => 0,
1690            Self::Decay => 1,
1691            Self::Sustain => 2,
1692            Self::Release => 3,
1693            Self::Damp => 4,
1694            Self::Unknown => 5,
1695        }
1696    }
1697
1698    const fn from_tag(tag: u8) -> Result<Self, OpllStateError> {
1699        match tag {
1700            0 => Ok(Self::Attack),
1701            1 => Ok(Self::Decay),
1702            2 => Ok(Self::Sustain),
1703            3 => Ok(Self::Release),
1704            4 => Ok(Self::Damp),
1705            5 => Ok(Self::Unknown),
1706            other => Err(OpllStateError::InvalidEgState(other)),
1707        }
1708    }
1709}
1710
1711impl ChipType {
1712    /// Stable on-disk tag, for the same reason as [`EgState::to_tag`].
1713    const fn to_tag(self) -> u8 {
1714        match self {
1715            Self::Ym2413 => 0,
1716            Self::Vrc7 => 1,
1717            Self::Ymf281b => 2,
1718        }
1719    }
1720}
1721
1722/// Append-only byte writer. Local to this module because the OPLL schema is
1723/// versioned with the mapper section, not with the APU section (see
1724/// [`OpllStateError`]).
1725struct OpllW(Vec<u8>);
1726
1727impl OpllW {
1728    fn u8(&mut self, v: u8) {
1729        self.0.push(v);
1730    }
1731    fn u16(&mut self, v: u16) {
1732        self.0.extend_from_slice(&v.to_le_bytes());
1733    }
1734    fn u32(&mut self, v: u32) {
1735        self.0.extend_from_slice(&v.to_le_bytes());
1736    }
1737    fn i16(&mut self, v: i16) {
1738        self.0.extend_from_slice(&v.to_le_bytes());
1739    }
1740    fn i32(&mut self, v: i32) {
1741        self.0.extend_from_slice(&v.to_le_bytes());
1742    }
1743    fn patch(&mut self, p: &Patch) {
1744        // Field order is load-bearing: `OpllR::patch` reads it back verbatim.
1745        for b in [
1746            p.tl, p.fb, p.eg, p.ml, p.ar, p.dr, p.sl, p.rr, p.kr, p.kl, p.am, p.pm, p.ws,
1747        ] {
1748            self.0.push(b);
1749        }
1750    }
1751}
1752
1753/// Bounds-checked byte reader. Every read is length-checked before it happens,
1754/// so a truncated or hand-edited blob returns [`OpllStateError::Truncated`]
1755/// rather than panicking — this parses untrusted save-state bytes.
1756struct OpllR<'a> {
1757    src: &'a [u8],
1758    pos: usize,
1759}
1760
1761impl OpllR<'_> {
1762    fn need(&self, n: usize) -> Result<(), OpllStateError> {
1763        if self.src.len() - self.pos < n {
1764            return Err(OpllStateError::Truncated(self.pos));
1765        }
1766        Ok(())
1767    }
1768    fn u8(&mut self) -> Result<u8, OpllStateError> {
1769        self.need(1)?;
1770        let v = self.src[self.pos];
1771        self.pos += 1;
1772        Ok(v)
1773    }
1774    fn u16(&mut self) -> Result<u16, OpllStateError> {
1775        self.need(2)?;
1776        let v = u16::from_le_bytes([self.src[self.pos], self.src[self.pos + 1]]);
1777        self.pos += 2;
1778        Ok(v)
1779    }
1780    fn u32(&mut self) -> Result<u32, OpllStateError> {
1781        self.need(4)?;
1782        let mut b = [0u8; 4];
1783        b.copy_from_slice(&self.src[self.pos..self.pos + 4]);
1784        self.pos += 4;
1785        Ok(u32::from_le_bytes(b))
1786    }
1787    fn i16(&mut self) -> Result<i16, OpllStateError> {
1788        Ok(self.u16()? as i16)
1789    }
1790    fn i32(&mut self) -> Result<i32, OpllStateError> {
1791        Ok(self.u32()? as i32)
1792    }
1793    fn patch(&mut self) -> Result<Patch, OpllStateError> {
1794        self.need(PATCH_BYTES)?;
1795        // Destructured positionally rather than field-by-field so the order
1796        // here is visibly the same list `OpllW::patch` writes; a reordering on
1797        // one side is then a visible diff on the other, not a silent
1798        // reinterpretation of thirteen interchangeable `u8`s.
1799        let [tl, fb, eg, ml, ar, dr, sl, rr, kr, kl, am, pm, ws]: [u8; PATCH_BYTES] = self.src
1800            [self.pos..self.pos + PATCH_BYTES]
1801            .try_into()
1802            .expect("slice length checked by `need` above");
1803        self.pos += PATCH_BYTES;
1804        // MASK to each parameter's hardware width. These are register fields of
1805        // fixed bit width, so a wider value does not describe a chip state that
1806        // exists -- masking IS the parse, not a repair after it.
1807        //
1808        // Scope, precisely, because an earlier version of this comment claimed
1809        // "every register field is masked" and that was an OVERCLAIM (review
1810        // caught it): what is masked is everything that reaches a SUBSCRIPT --
1811        // the 13 patch parameters here, and `blk_fnum` / `fnum` / `blk` /
1812        // `number` / `wave_table_idx` / `pg_keep` in the slot reader. The
1813        // remaining restored fields are deliberately left alone because none of
1814        // them can index anything: `eg_rate_h`/`eg_rate_l` are consumed by a
1815        // `match` and recomputed as `(p_rate + rks_h2).min(15)`; `lfo_am` is
1816        // overwritten every `update_ampm` from `AM_TABLE[idx % len]`; `rks`,
1817        // `tll`, `type_flags`, `key_flag`, `sus_flag` and `test_flag` are only
1818        // ever compared, shifted or added; and `lookup_exp_table` masks its own
1819        // index to 8 bits. `opll_restore_survives_a_hostile_blob` sweeps
1820        // pseudo-random payloads to keep that true rather than assumed.
1821        //
1822        // Load-bearing, not defensive tidiness: `commit_slot_update` indexes the
1823        // TLL table as `[block_fnum][tl][kl]`, dimensions `[128][64][4]`. An
1824        // unmasked `tl` of 255 computes an index of ~524k into a 32,768-entry
1825        // table and PANICS. A save state is a file on disk -- untrusted input --
1826        // so a hand-edited one must not be able to crash the emulator. Pinned by
1827        // `opll_restore_survives_a_hostile_blob`, which panicked before this.
1828        Ok(Patch {
1829            tl: tl & 0x3F,
1830            fb: fb & 0x07,
1831            eg: eg & 0x01,
1832            ml: ml & 0x0F,
1833            ar: ar & 0x0F,
1834            dr: dr & 0x0F,
1835            sl: sl & 0x0F,
1836            rr: rr & 0x0F,
1837            kr: kr & 0x01,
1838            kl: kl & 0x03,
1839            am: am & 0x01,
1840            pm: pm & 0x01,
1841            ws: ws & 0x01,
1842        })
1843    }
1844}
1845
1846impl Opll {
1847    /// Serialize the complete live synthesizer state.
1848    ///
1849    /// # Why this exists
1850    ///
1851    /// Until v2.3.7 the VRC7 mapper's save state carried only the *shadow*
1852    /// register bytes and replayed nothing into the synthesizer, so after a
1853    /// rewind, a netplay rollback, or a TAS restore the FM voice resumed from
1854    /// whatever envelope and phase state it happened to hold — audible, and a
1855    /// determinism gap in a project whose central claim is determinism. The
1856    /// obvious format-free repair (replaying `regs` through
1857    /// [`Opll::write_reg`]) is worse than the disease: it restarts every
1858    /// keyed-on channel's envelope at attack, so every rewind frame produces a
1859    /// transient. Carrying the state verbatim is the only repair that restores
1860    /// the sound that was actually playing.
1861    ///
1862    /// # What is and is not carried
1863    ///
1864    /// Everything mutated during synthesis: the register shadow, the EG/LFO
1865    /// counters, the per-channel patch selection, all 18 operator slots
1866    /// (phase accumulators, envelope state machines, feedback history), the
1867    /// per-channel outputs and the mix. The user patch pair (`patch_set[0..2]`,
1868    /// writeable through registers `$00-$07`) is carried explicitly rather than
1869    /// re-derived, so a restore cannot depend on `refresh_user_patch_pointers`
1870    /// running in the right order.
1871    ///
1872    /// Deliberately NOT carried, because they are constants of construction and
1873    /// restoring them would be restoring a copy of the binary into itself:
1874    /// `waves` and `tll_rks` (pure lookup tables built in [`Opll::new`]) and
1875    /// `patch_set[2..]` (the chip's patch ROM, fixed by `chip_type`). The chip
1876    /// type itself IS carried, as a tag, purely so a mismatched restore is
1877    /// rejected instead of silently reinterpreting slot patches against the
1878    /// wrong instrument set.
1879    ///
1880    /// The blob is exactly [`OPLL_SNAPSHOT_LEN`] bytes and self-describes its
1881    /// version in byte 0.
1882    #[must_use]
1883    pub fn snapshot(&self) -> Vec<u8> {
1884        let mut w = OpllW(Vec::with_capacity(OPLL_SNAPSHOT_LEN));
1885        w.u8(OPLL_SNAPSHOT_VERSION);
1886        w.u8(self.chip_type.to_tag());
1887        w.u8(self.adr);
1888        w.0.extend_from_slice(&self.reg);
1889        w.u8(self.test_flag);
1890        w.u32(self.slot_key_status);
1891        w.u32(self.eg_counter);
1892        w.u32(self.pm_phase);
1893        w.i32(self.am_phase);
1894        w.u8(self.lfo_am);
1895        for n in self.patch_number {
1896            w.i32(n);
1897        }
1898        // The user patch (modulator + carrier), written through $00-$07.
1899        for p in self.patch_set.iter().take(2) {
1900            w.patch(p);
1901        }
1902        for s in &self.slot {
1903            w.u8(s.number);
1904            w.u8(s.type_flags);
1905            w.patch(&s.patch);
1906            w.i32(s.output[0]);
1907            w.i32(s.output[1]);
1908            w.u8(s.wave_table_idx);
1909            w.u32(s.pg_phase);
1910            w.u32(s.pg_out);
1911            w.u8(s.pg_keep);
1912            w.u16(s.blk_fnum);
1913            w.u16(s.fnum);
1914            w.u8(s.blk);
1915            w.u8(s.eg_state.to_tag());
1916            w.i32(s.volume);
1917            w.u8(s.key_flag);
1918            w.u8(s.sus_flag);
1919            w.u16(s.tll);
1920            w.u8(s.rks);
1921            w.u8(s.eg_rate_h);
1922            w.u8(s.eg_rate_l);
1923            w.u32(s.eg_shift);
1924            w.u32(s.eg_out);
1925            w.u32(s.update_requests);
1926        }
1927        for v in self.ch_out {
1928            w.i16(v);
1929        }
1930        w.i16(self.mix_out);
1931        debug_assert_eq!(w.0.len(), OPLL_SNAPSHOT_LEN, "OPLL snapshot length drift");
1932        w.0
1933    }
1934
1935    /// Restore state previously produced by [`Opll::snapshot`].
1936    ///
1937    /// Trailing bytes past the schema are ignored, so a future version may
1938    /// append without breaking this reader — the same additive discipline the
1939    /// PPU and APU sections use.
1940    ///
1941    /// # Errors
1942    ///
1943    /// [`OpllStateError::Truncated`] if the blob is shorter than the schema,
1944    /// [`OpllStateError::UnsupportedVersion`] if byte 0 is not
1945    /// [`OPLL_SNAPSHOT_VERSION`], [`OpllStateError::ChipTypeMismatch`] if the
1946    /// blob describes a different chip, and
1947    /// [`OpllStateError::InvalidEgState`] on a corrupt envelope-state tag.
1948    pub fn restore(&mut self, data: &[u8]) -> Result<(), OpllStateError> {
1949        let mut r = OpllR { src: data, pos: 0 };
1950        let version = r.u8()?;
1951        if version != OPLL_SNAPSHOT_VERSION {
1952            return Err(OpllStateError::UnsupportedVersion(version));
1953        }
1954        let chip_tag = r.u8()?;
1955        if chip_tag != self.chip_type.to_tag() {
1956            return Err(OpllStateError::ChipTypeMismatch {
1957                got: chip_tag,
1958                want: self.chip_type.to_tag(),
1959            });
1960        }
1961        // Read the whole blob into locals BEFORE mutating `self`: a truncated
1962        // or corrupt tail must leave the synthesizer on its previous state
1963        // rather than half-overwritten, since the caller (a mapper's
1964        // `load_state`) reports the error and keeps running.
1965        let adr = r.u8()?;
1966        r.need(0x40)?;
1967        let mut reg = [0u8; 0x40];
1968        reg.copy_from_slice(&r.src[r.pos..r.pos + 0x40]);
1969        r.pos += 0x40;
1970        let test_flag = r.u8()? & 0x01;
1971        let slot_key_status = r.u32()?;
1972        let eg_counter = r.u32()?;
1973        let pm_phase = r.u32()?;
1974        let am_phase = r.i32()?;
1975        let lfo_am = r.u8()?;
1976        let mut patch_number = [0i32; 9];
1977        for n in &mut patch_number {
1978            // CLAMPED to the instrument range even though it is not currently a
1979            // subscript -- it is only ever compared to zero, and `set_patch`
1980            // bounds-checks its own argument before touching `patch_set`.
1981            //
1982            // Clamped anyway, for consistency with every other field here: the
1983            // legal domain is 0..=15 (a 4-bit `$3x` high nibble), so a wider
1984            // value describes a chip state that cannot exist, and letting one
1985            // through would leave the ONE field whose safety rests on "nothing
1986            // indexes it today" rather than on its own width. Reviewers flagged
1987            // it three times; that is a fair signal that the invariant was too
1988            // subtle to be load-bearing.
1989            *n = r.i32()?.clamp(0, MAX_PATCH_NUMBER);
1990        }
1991        let user_patch = [r.patch()?, r.patch()?];
1992        let mut slots = [Slot::default(); SNAPSHOT_SLOTS];
1993        for s in &mut slots {
1994            // Masked for the same reason as the patch fields: `blk_fnum` feeds
1995            // the TLL/RKS row index (`>> 5` into 128 rows, `>> 8` into 16), so an
1996            // unmasked u16 indexes far past both tables. A legal `blk_fnum` is
1997            // `(blk3 << 9) | fnum9`, i.e. at most 0x0FFF.
1998            s.number = r.u8()? % SNAPSHOT_SLOTS as u8;
1999            s.type_flags = r.u8()? & 0x03;
2000            s.patch = r.patch()?;
2001            // CLAMPED to i16: `calc_slot_mod` / `calc_slot_car` only ever store
2002            // `i32::from(out)` with `out: i16`, and the feedback path evaluates
2003            // `output[0] + output[1]`, which overflows on two arbitrary i32s.
2004            // The field is `i32` for headroom in that sum, not because the
2005            // values are ever wider than i16.
2006            s.output = [clamp_i16(r.i32()?), clamp_i16(r.i32()?)];
2007            s.wave_table_idx = r.u8()? & 0x01;
2008            s.pg_phase = r.u32()?;
2009            s.pg_out = r.u32()?;
2010            s.pg_keep = r.u8()? & 0x01;
2011            s.blk_fnum = r.u16()? & 0x0FFF;
2012            s.fnum = r.u16()? & 0x01FF;
2013            s.blk = r.u8()? & 0x07;
2014            s.eg_state = EgState::from_tag(r.u8()?)?;
2015            s.volume = r.i32()?;
2016            s.key_flag = r.u8()? & 0x01;
2017            s.sus_flag = r.u8()? & 0x01;
2018            s.tll = r.u16()?;
2019            s.rks = r.u8()? & 0x0F;
2020            s.eg_rate_h = r.u8()? & 0x0F;
2021            // `eg_rate_l` INDEXES `EG_STEP_TABLES`, whose outer dimension is
2022            // 4. Legal values are `rks & 3`. This one is why the whole sweep
2023            // exists: I had claimed, after tracing by hand, that none of the
2024            // flag fields reach a subscript -- and this one does. The trace
2025            // was run with a broken grep whose empty output I read as proof.
2026            s.eg_rate_l = r.u8()? & 0x03;
2027            // CLAMPED, and this one is a shift amount rather than a
2028            // subscript -- `calc_envelope` computes `1u32 << eg_shift`, which
2029            // PANICS for any value >= 32. `commit_slot_update` only ever
2030            // produces `13 - eg_rate_h` with `eg_rate_h <= 13`, so 13 is the
2031            // real ceiling. Found by the randomized half of
2032            // `opll_restore_survives_a_hostile_blob`, NOT by its fixed
2033            // all-`0xFF` payload: with every byte 0xFF, `update_requests` is
2034            // also all-ones, so `commit_slot_update` recomputed `eg_shift`
2035            // before `calc_envelope` could use the restored one. The fixed blob
2036            // was too hostile in one dimension to expose a bug in another.
2037            s.eg_shift = r.u32()?.min(EG_SHIFT_MAX);
2038            s.eg_out = r.u32()?;
2039            s.update_requests = r.u32()?;
2040        }
2041        let mut ch_out = [0i16; 14];
2042        for v in &mut ch_out {
2043            *v = r.i16()?;
2044        }
2045        let mix_out = r.i16()?;
2046
2047        self.adr = adr;
2048        self.reg = reg;
2049        self.test_flag = test_flag;
2050        self.slot_key_status = slot_key_status;
2051        self.eg_counter = eg_counter;
2052        self.pm_phase = pm_phase;
2053        self.am_phase = am_phase;
2054        self.lfo_am = lfo_am;
2055        self.patch_number = patch_number;
2056        self.patch_set[0] = user_patch[0];
2057        self.patch_set[1] = user_patch[1];
2058        self.slot = slots;
2059        self.ch_out = ch_out;
2060        self.mix_out = mix_out;
2061        Ok(())
2062    }
2063}
2064
2065// ---------------------------------------------------------------------------
2066// Tests — verify the static tables match the C source byte-for-byte.
2067// These tests run unconditionally (no feature gate) since the OPLL
2068// constants are pure data and the tests are cheap.
2069// ---------------------------------------------------------------------------
2070
2071#[cfg(test)]
2072mod tests {
2073    use super::*;
2074
2075    #[test]
2076    fn exp_table_size_matches_c_source() {
2077        assert_eq!(EXP_TABLE.len(), 256);
2078        // Spot-check: emu2413.cpp line 138 value at index 0 is 0
2079        assert_eq!(EXP_TABLE[0], 0);
2080        // Index 255 = round((exp2(255/256) - 1) * 1024) = 1018
2081        assert_eq!(EXP_TABLE[255], 1018);
2082    }
2083
2084    #[test]
2085    fn fullsin_table_quarter_matches_c_source() {
2086        assert_eq!(FULLSIN_TABLE_QUARTER.len(), 256);
2087        // emu2413.cpp line 157 first value is 2137 (largest log-sin)
2088        assert_eq!(FULLSIN_TABLE_QUARTER[0], 2137);
2089        // Last value tapers to 0 (sin → 1, -log2(1) → 0)
2090        assert_eq!(FULLSIN_TABLE_QUARTER[255], 0);
2091    }
2092
2093    #[test]
2094    fn patch_dump_sizes_match_19_x_8() {
2095        assert_eq!(DEFAULT_INST_YM2413.len(), 19 * 8);
2096        assert_eq!(DEFAULT_INST_VRC7.len(), 19 * 8);
2097        assert_eq!(DEFAULT_INST_YMF281B.len(), 19 * 8);
2098    }
2099
2100    #[test]
2101    fn vrc7_patch_0_is_user_patch_all_zeros() {
2102        // The user-defined patch slot is always zeroed in the dump.
2103        // Lagrange Point uses patches 1-15 plus user patch via $00-$07.
2104        for b in &DEFAULT_INST_VRC7[0..8] {
2105            assert_eq!(*b, 0);
2106        }
2107    }
2108
2109    #[test]
2110    fn vrc7_patch_1_matches_nuke_ykt_reference() {
2111        // emu2413.cpp line 65 (VRC7 patch 1 from Nuke.YKT analysis).
2112        assert_eq!(
2113            DEFAULT_INST_VRC7[8..16],
2114            [0x03, 0x21, 0x05, 0x06, 0xe8, 0x81, 0x42, 0x27]
2115        );
2116    }
2117
2118    #[test]
2119    fn vrc7_all_15_melodic_patches_match_nuke_ykt_canonical() {
2120        // The real `patch_vrc7` (bbbradsmith) accuracy criterion: RustyNES's
2121        // built-in VRC7 instrument ROM must equal the canonical Nuke.YKT dump
2122        // ("March 15, 2019 dumped by Nuke.YKT"), which is byte-for-byte
2123        // IDENTICAL across three independent references — fceux
2124        // `emu2413.c:55-72`, Mesen2 `emu2413.cpp:63-79` (`default_inst[1]`, the
2125        // OPLL_VRC7 set), and nestopia's own `NstBoardKonamiVrc7.cpp:53-70`
2126        // `preset[15][8]`. Distinct from the YM2413 (`default_inst[0]`) and
2127        // YMF281B (`default_inst[2]`) sets, which differ (e.g. Piano). Verifies
2128        // all 15 melodic patches (slot 0 = User, always zero) plus the 3 VRC7
2129        // rhythm patches. This is what `db_vrc7`/`patch_vrc7` need for the FM
2130        // synth to reproduce Lagrange Point's instruments correctly.
2131        #[rustfmt::skip]
2132        const CANONICAL_VRC7: [u8; 19 * 8] = [
2133            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0: User
2134            0x03, 0x21, 0x05, 0x06, 0xe8, 0x81, 0x42, 0x27, // 1: Violin
2135            0x13, 0x41, 0x14, 0x0d, 0xd8, 0xf6, 0x23, 0x12, // 2: Guitar
2136            0x11, 0x11, 0x08, 0x08, 0xfa, 0xb2, 0x20, 0x12, // 3: Piano
2137            0x31, 0x61, 0x0c, 0x07, 0xa8, 0x64, 0x61, 0x27, // 4: Flute
2138            0x32, 0x21, 0x1e, 0x06, 0xe1, 0x76, 0x01, 0x28, // 5: Clarinet
2139            0x02, 0x01, 0x06, 0x00, 0xa3, 0xe2, 0xf4, 0xf4, // 6: Oboe
2140            0x21, 0x61, 0x1d, 0x07, 0x82, 0x81, 0x11, 0x07, // 7: Trumpet
2141            0x23, 0x21, 0x22, 0x17, 0xa2, 0x72, 0x01, 0x17, // 8: Organ
2142            0x35, 0x11, 0x25, 0x00, 0x40, 0x73, 0x72, 0x01, // 9: Horn
2143            0xb5, 0x01, 0x0f, 0x0f, 0xa8, 0xa5, 0x51, 0x02, // A: Synthesizer
2144            0x17, 0xc1, 0x24, 0x07, 0xf8, 0xf8, 0x22, 0x12, // B: Harpsichord
2145            0x71, 0x23, 0x11, 0x06, 0x65, 0x74, 0x18, 0x16, // C: Vibraphone
2146            0x01, 0x02, 0xd3, 0x05, 0xc9, 0x95, 0x03, 0x02, // D: Synth Bass
2147            0x61, 0x63, 0x0c, 0x00, 0x94, 0xc0, 0x33, 0xf6, // E: Acoustic Bass
2148            0x21, 0x72, 0x0d, 0x00, 0xc1, 0xd5, 0x56, 0x06, // F: Electric Guitar
2149            0x01, 0x01, 0x18, 0x0f, 0xdf, 0xf8, 0x6a, 0x6d, // R: Bass Drum
2150            0x01, 0x01, 0x00, 0x00, 0xc8, 0xd8, 0xa7, 0x68, // R: HH/SD
2151            0x05, 0x01, 0x00, 0x00, 0xf8, 0xaa, 0x59, 0x55, // R: Tom/Cymbal
2152        ];
2153        assert_eq!(
2154            DEFAULT_INST_VRC7, CANONICAL_VRC7,
2155            "VRC7 instrument ROM drifted from the canonical Nuke.YKT dump"
2156        );
2157    }
2158
2159    #[test]
2160    fn pm_table_first_row_is_all_zero() {
2161        // emu2413.cpp line 182: fnum=000xxxxxx row is no pitch modulation.
2162        assert_eq!(PM_TABLE[0], [0, 0, 0, 0, 0, 0, 0, 0]);
2163    }
2164
2165    #[test]
2166    fn am_table_length_matches_c_source() {
2167        // emu2413.cpp declares uint8_t am_table[210].
2168        assert_eq!(AM_TABLE.len(), 210);
2169        assert_eq!(AM_TABLE[0], 0);
2170        // Peak is 13 (per the C source comment, "13, 13, 13").
2171        let peak = *AM_TABLE.iter().max().unwrap();
2172        assert_eq!(peak, 13);
2173    }
2174
2175    #[test]
2176    fn ml_table_matches_c_source() {
2177        assert_eq!(ML_TABLE.len(), 16);
2178        // emu2413.cpp line 222: first entry is 1 (not doubled).
2179        assert_eq!(ML_TABLE[0], 1);
2180        assert_eq!(ML_TABLE[1], 2);
2181        assert_eq!(ML_TABLE[14], 30);
2182        assert_eq!(ML_TABLE[15], 30);
2183    }
2184
2185    #[test]
2186    fn constants_match_emu2413_defines() {
2187        assert_eq!(PG_BITS, 10);
2188        assert_eq!(PG_WIDTH, 1024);
2189        assert_eq!(DP_BITS, 19);
2190        assert_eq!(DP_BASE_BITS, 9);
2191        assert_eq!(EG_BITS, 7);
2192        assert_eq!(EG_MUTE, 127);
2193        assert_eq!(EG_MAX, 123);
2194        assert_eq!(TL_BITS, 6);
2195        assert_eq!(tl_to_eg(5), 10);
2196    }
2197
2198    #[test]
2199    fn new_vrc7_opll_has_vrc7_chip_type() {
2200        let opll = Opll::new(ChipType::Vrc7);
2201        assert_eq!(opll.chip_type(), ChipType::Vrc7);
2202    }
2203
2204    #[test]
2205    fn calc_returns_zero_in_scaffold_stage() {
2206        // ADR-0004 deferred behavior is preserved in v1.1.0-rc:
2207        // calc() returns 0 until the DSP lands.
2208        let mut opll = Opll::new(ChipType::Vrc7);
2209        opll.write_reg(0x30, 0x01);
2210        opll.write_reg(0x10, 0x80);
2211        opll.write_reg(0x20, 0x15);
2212        for _ in 0..100 {
2213            assert_eq!(opll.calc(), 0);
2214        }
2215    }
2216
2217    #[test]
2218    fn register_shadow_round_trips() {
2219        let mut opll = Opll::new(ChipType::Vrc7);
2220        opll.write_reg(0x10, 0xAB);
2221        opll.write_reg(0x30, 0x4F);
2222        assert_eq!(opll.read_reg(0x10), 0xAB);
2223        assert_eq!(opll.read_reg(0x30), 0x4F);
2224        // emu2413.cpp:1226 — write_reg silently ignores addresses
2225        // >= 0x40 (no wrap, no fault). Verify the high-byte write
2226        // does NOT bleed into register 0x00.
2227        let pre = opll.read_reg(0x00);
2228        opll.write_reg(0x80, 0x12);
2229        assert_eq!(
2230            opll.read_reg(0x00),
2231            pre,
2232            "writes to reg >= 0x40 must be ignored, not masked"
2233        );
2234        // Mirror registers ($19-$1F → $10-$16, etc.): writing 0x19
2235        // should land at 0x10 per emu2413.cpp:1230-1232.
2236        opll.write_reg(0x19, 0xCC);
2237        assert_eq!(
2238            opll.read_reg(0x10),
2239            0xCC,
2240            "register 0x19 should mirror to 0x10"
2241        );
2242    }
2243
2244    #[test]
2245    fn reset_clears_register_shadow() {
2246        let mut opll = Opll::new(ChipType::Vrc7);
2247        opll.write_reg(0x10, 0xFF);
2248        opll.write_reg(0x20, 0xAA);
2249        opll.reset();
2250        assert_eq!(opll.read_reg(0x10), 0);
2251        assert_eq!(opll.read_reg(0x20), 0);
2252    }
2253
2254    #[test]
2255    fn patch_set_loaded_from_vrc7_dump() {
2256        let opll = Opll::new(ChipType::Vrc7);
2257        // VRC7 patch 1 modulator decode: dump[8..16] = 0x03,0x21,0x05,0x06,...
2258        // ML = dump[0] & 0x0f = 0x03 & 0x0f = 3
2259        let p1_mod = opll.patch_set[2]; // patch 1 modulator = index 2
2260        assert_eq!(p1_mod.ml, 3);
2261    }
2262
2263    // -----------------------------------------------------------------------
2264    // Phase generator tests — verify calc_phase against the emu2413.cpp:765
2265    // closed-form. The expected values are hand-derived from the C formula
2266    // so any deviation (sign, mask, shift order) trips the assertion.
2267    // -----------------------------------------------------------------------
2268
2269    fn fresh_slot(number: u8) -> Slot {
2270        let mut s = Slot::default();
2271        s.reset_to_release(number);
2272        s
2273    }
2274
2275    #[test]
2276    fn calc_phase_zero_fnum_zero_increment() {
2277        let mut s = fresh_slot(0);
2278        s.fnum = 0;
2279        s.blk = 0;
2280        s.patch.ml = 0; // ML_TABLE[0] = 1
2281        s.patch.pm = 0;
2282        s.calc_phase(0, false);
2283        // Increment = (0 * 2 + 0) * 1 = 0; phase stays at 0.
2284        assert_eq!(s.pg_phase, 0);
2285        assert_eq!(s.pg_out, 0);
2286    }
2287
2288    #[test]
2289    fn calc_phase_no_pm_one_step() {
2290        let mut s = fresh_slot(0);
2291        s.fnum = 0x100; // 256
2292        s.blk = 2; // shift << 2
2293        s.patch.ml = 2; // ML_TABLE[2] = 4
2294        s.patch.pm = 0;
2295        // Increment per C: ((256 * 2 + 0) * 4) << 2 >> 2 = 2048.
2296        s.calc_phase(0, false);
2297        assert_eq!(s.pg_phase, 2048);
2298        // pg_out = pg_phase >> DP_BASE_BITS = 2048 >> 9 = 4.
2299        assert_eq!(s.pg_out, 4);
2300    }
2301
2302    #[test]
2303    fn calc_phase_pm_offset_applied_when_patch_pm_set() {
2304        let mut s = fresh_slot(0);
2305        s.fnum = 0x080; // fnum_row = (128 >> 6) & 7 = 2
2306        s.blk = 0;
2307        s.patch.ml = 0; // ML_TABLE[0] = 1
2308        s.patch.pm = 1;
2309        // pm_phase >> 10 = 2 → col 2 → pm_table[2][2] = 2.
2310        // Increment = (128*2 + 2) * 1 = 258. Shifted << 0 >> 2 = 64.
2311        s.calc_phase(2 << 10, false);
2312        assert_eq!(s.pg_phase, 64);
2313    }
2314
2315    #[test]
2316    fn calc_phase_pm_disabled_yields_pm_zero() {
2317        let mut s = fresh_slot(0);
2318        s.fnum = 0x080;
2319        s.patch.ml = 0;
2320        s.patch.pm = 0;
2321        // Same pm_phase as previous, but pm bit OFF: increment = (128*2)*1 = 256.
2322        s.calc_phase(2 << 10, false);
2323        // 256 >> 2 = 64.
2324        assert_eq!(s.pg_phase, 64);
2325    }
2326
2327    #[test]
2328    fn calc_phase_dp_width_wraps_modulo() {
2329        let mut s = fresh_slot(0);
2330        s.pg_phase = DP_WIDTH - 4;
2331        s.fnum = 0x040;
2332        s.blk = 1; // (64*2 * 1) << 1 = 256; >> 2 = 64.
2333        s.patch.ml = 0;
2334        s.patch.pm = 0;
2335        s.calc_phase(0, false);
2336        // (DP_WIDTH - 4 + 64) mod DP_WIDTH = 60.
2337        assert_eq!(s.pg_phase, 60);
2338    }
2339
2340    #[test]
2341    fn calc_phase_reset_zeros_phase_before_increment() {
2342        let mut s = fresh_slot(0);
2343        s.pg_phase = 12345;
2344        s.fnum = 0;
2345        s.patch.ml = 0;
2346        s.patch.pm = 0;
2347        s.calc_phase(0, true);
2348        // reset → pg_phase = 0; increment = 0 → stays 0.
2349        assert_eq!(s.pg_phase, 0);
2350    }
2351
2352    // -----------------------------------------------------------------------
2353    // Envelope generator tests — exercise reset → key-on → Damp → Attack →
2354    // Decay → Sustain transitions via direct slot mutation. The reference
2355    // values come from manually tracing emu2413.cpp:817-887 with specific
2356    // patch parameters.
2357    // -----------------------------------------------------------------------
2358
2359    #[test]
2360    fn reset_to_release_puts_slot_at_eg_mute() {
2361        let mut s = Slot::default();
2362        s.reset_to_release(3);
2363        assert_eq!(s.eg_state, EgState::Release);
2364        assert_eq!(s.eg_out, EG_MUTE);
2365        assert_eq!(s.number, 3);
2366        assert_eq!(s.type_flags, 1); // 3 % 2 = 1 (carrier)
2367    }
2368
2369    #[test]
2370    fn start_envelope_saturated_ar_skips_to_decay_at_zero() {
2371        let mut s = fresh_slot(0);
2372        s.patch.ar = 15;
2373        s.rks = 0;
2374        s.eg_out = 50;
2375        s.start_envelope();
2376        assert_eq!(s.eg_state, EgState::Decay);
2377        assert_eq!(s.eg_out, 0);
2378    }
2379
2380    #[test]
2381    fn start_envelope_non_saturated_ar_enters_attack() {
2382        let mut s = fresh_slot(0);
2383        s.patch.ar = 8;
2384        s.rks = 0;
2385        s.eg_out = 50;
2386        s.start_envelope();
2387        assert_eq!(s.eg_state, EgState::Attack);
2388        // eg_out preserved on Attack entry (only saturated AR zeros it).
2389        assert_eq!(s.eg_out, 50);
2390    }
2391
2392    #[test]
2393    fn start_envelope_rks_contributes_to_effective_ar() {
2394        let mut s = fresh_slot(0);
2395        s.patch.ar = 12;
2396        s.rks = 12; // rks >> 2 = 3 → effective = 15 → saturate.
2397        s.start_envelope();
2398        assert_eq!(s.eg_state, EgState::Decay);
2399        assert_eq!(s.eg_out, 0);
2400    }
2401
2402    #[test]
2403    fn calc_envelope_decay_to_sustain_at_sl_match() {
2404        // Decay → Sustain transition is checked unconditionally
2405        // (emu2413.cpp:870-875 — NOT synchronized with eg_counter mask).
2406        let mut s = fresh_slot(0);
2407        s.eg_state = EgState::Decay;
2408        s.eg_out = 32;
2409        s.patch.sl = 4; // 32 >> 3 = 4 → match.
2410        s.eg_rate_h = 0; // No decrement applied.
2411        let step = s.calc_envelope(false, 0, 0);
2412        assert_eq!(s.eg_state, EgState::Sustain);
2413        assert_eq!(step, EnvelopeStep::Continue);
2414    }
2415
2416    #[test]
2417    fn calc_envelope_attack_to_decay_when_eg_out_hits_zero() {
2418        let mut s = fresh_slot(0);
2419        s.eg_state = EgState::Attack;
2420        s.eg_out = 0; // Already at min attenuation = max volume.
2421        s.eg_rate_h = 0;
2422        s.calc_envelope(false, 0, 0);
2423        assert_eq!(s.eg_state, EgState::Decay);
2424    }
2425
2426    #[test]
2427    fn calc_envelope_damp_to_attack_via_carrier_buddy_reset() {
2428        let mut s = fresh_slot(1); // Carrier (odd index, type & 1 == 1)
2429        s.eg_state = EgState::Damp;
2430        s.eg_out = EG_MAX;
2431        s.eg_shift = 0; // mask = 0; (eg_counter & 0) == 0 always.
2432        s.patch.ar = 8;
2433        s.rks = 0;
2434        s.pg_keep = 0;
2435        s.pg_phase = 0x1234;
2436        let step = s.calc_envelope(false, 0, 0);
2437        // Damp → Attack via start_envelope; carrier resets pg_phase
2438        // and signals buddy reset to caller.
2439        assert_eq!(s.eg_state, EgState::Attack);
2440        assert_eq!(s.pg_phase, 0);
2441        assert_eq!(step, EnvelopeStep::ResetBuddyPhase);
2442    }
2443
2444    #[test]
2445    fn calc_envelope_test_flag_zeros_eg_out_each_tick() {
2446        let mut s = fresh_slot(0);
2447        s.eg_state = EgState::Sustain;
2448        s.eg_out = 64;
2449        s.calc_envelope(false, 0, 1);
2450        assert_eq!(s.eg_out, 0);
2451    }
2452
2453    #[test]
2454    fn lookup_decay_step_rate_15_returns_2() {
2455        let mut s = fresh_slot(0);
2456        s.eg_rate_h = 15;
2457        assert_eq!(s.lookup_decay_step(0), 2);
2458        assert_eq!(s.lookup_decay_step(0xff), 2);
2459    }
2460
2461    #[test]
2462    fn lookup_attack_step_rate_0_returns_0() {
2463        let mut s = fresh_slot(0);
2464        s.eg_rate_h = 0;
2465        s.eg_rate_l = 0;
2466        assert_eq!(s.lookup_attack_step(0), 0);
2467        s.eg_rate_h = 15;
2468        assert_eq!(s.lookup_attack_step(0), 0);
2469    }
2470
2471    #[test]
2472    fn lookup_attack_step_rate_12_uses_eg_step_table_complement() {
2473        // Direct port verification: at rate 12, value is `4 - EG_STEP_TABLES[L][index]`.
2474        let mut s = fresh_slot(0);
2475        s.eg_rate_h = 12;
2476        s.eg_rate_l = 0;
2477        // counter = 0 → index = (0 & 0xc) >> 1 = 0 → table[0][0] = 0 → 4 - 0 = 4.
2478        assert_eq!(s.lookup_attack_step(0), 4);
2479        // counter = 2 → index = (2 & 0xc) >> 1 = 0 → 4.
2480        assert_eq!(s.lookup_attack_step(2), 4);
2481        // counter = 4 → index = (4 & 0xc) >> 1 = 2 → table[0][2] = 0 → 4.
2482        assert_eq!(s.lookup_attack_step(4), 4);
2483        s.eg_rate_l = 3; // table[3] = [0,1,1,1,1,1,1,1]
2484        // counter = 4 → index = 2 → table[3][2] = 1 → 4 - 1 = 3.
2485        assert_eq!(s.lookup_attack_step(4), 3);
2486    }
2487
2488    // -----------------------------------------------------------------------
2489    // Wave table + exp/to_linear tests — verify table construction and the
2490    // log-to-linear decode produces emu2413-compatible values.
2491    // -----------------------------------------------------------------------
2492
2493    #[test]
2494    fn wave_tables_first_quarter_matches_quarter_lut() {
2495        let w = WaveTables::new();
2496        for (x, &expected) in FULLSIN_TABLE_QUARTER.iter().enumerate() {
2497            assert_eq!(w.fullsin[x], expected);
2498        }
2499    }
2500
2501    #[test]
2502    fn wave_tables_second_quarter_mirrors_first() {
2503        let w = WaveTables::new();
2504        // fullsin[256 + x] == fullsin[256 - x - 1]
2505        for x in 0..256 {
2506            assert_eq!(w.fullsin[256 + x], w.fullsin[256 - x - 1]);
2507        }
2508        // Endpoints: fullsin[256] (start of mirror) == fullsin[255] (last of first quarter).
2509        assert_eq!(w.fullsin[256], FULLSIN_TABLE_QUARTER[255]);
2510        // fullsin[511] (last of mirror) == fullsin[0] (first of first quarter).
2511        assert_eq!(w.fullsin[511], FULLSIN_TABLE_QUARTER[0]);
2512    }
2513
2514    #[test]
2515    fn wave_tables_second_half_has_sign_bit_set() {
2516        let w = WaveTables::new();
2517        for x in 0..512 {
2518            assert_eq!(w.fullsin[512 + x], 0x8000 | w.fullsin[x]);
2519        }
2520    }
2521
2522    #[test]
2523    fn halfsin_first_half_matches_fullsin_second_half_is_mute() {
2524        let w = WaveTables::new();
2525        for x in 0..512 {
2526            assert_eq!(w.halfsin[x], w.fullsin[x]);
2527        }
2528        for x in 512..1024 {
2529            assert_eq!(w.halfsin[x], 0xfff);
2530        }
2531    }
2532
2533    #[test]
2534    fn lookup_exp_table_positive_low_input_is_near_zero() {
2535        // i = 0x7fff (max positive log-magnitude before sign bit) → ~0.
2536        let v = lookup_exp_table(0x7f00);
2537        assert!(v.unsigned_abs() < 100, "got {v}");
2538    }
2539
2540    #[test]
2541    fn lookup_exp_table_signed_negates_via_bitwise_not() {
2542        // Same magnitude with sign bit set produces a sign-flipped value.
2543        let pos = lookup_exp_table(0x0010);
2544        let neg = lookup_exp_table(0x8010);
2545        // ~x in C; ~res when res is positive → ~res = -res - 1; << 1 doubles.
2546        // The relationship is: signed = !res; ((!res) << 1) == -(res << 1) - 2.
2547        // So neg ≈ -pos - 2 (depending on rounding).
2548        let diff = i32::from(neg) + i32::from(pos) + 2;
2549        assert!(diff.abs() <= 2, "pos={pos}, neg={neg}");
2550    }
2551
2552    #[test]
2553    fn to_linear_returns_zero_when_eg_out_above_eg_max() {
2554        let mut slot = fresh_slot(0);
2555        slot.eg_out = EG_MUTE; // > EG_MAX (123)
2556        slot.tll = 0;
2557        assert_eq!(to_linear(0, &slot, 0), 0);
2558    }
2559
2560    #[test]
2561    fn to_linear_zero_attenuation_yields_max_magnitude() {
2562        let mut slot = fresh_slot(0);
2563        slot.eg_out = 0;
2564        slot.tll = 0;
2565        // h = 0 → att = 0 → lookup_exp_table(0) ≈ +/- large magnitude.
2566        let out = to_linear(0, &slot, 0);
2567        assert!(out.unsigned_abs() > 1000, "expected loud, got {out}");
2568    }
2569
2570    // -----------------------------------------------------------------------
2571    // TLL + RKS table tests
2572    // -----------------------------------------------------------------------
2573
2574    #[test]
2575    fn tll_kl_zero_is_just_tl_doubled() {
2576        let t = TllRksTables::new();
2577        // KL=0 → TLL = TL2EG(TL) = TL * 2 for every block/fnum row.
2578        for block in 0..8 {
2579            for fnum in 0..16 {
2580                let row = (block << 4) | fnum;
2581                for tl in 0..64 {
2582                    assert_eq!(t.tll_at(row, tl, 0), tl_to_eg(tl as u32));
2583                }
2584            }
2585        }
2586    }
2587
2588    #[test]
2589    fn rks_kr_zero_is_block_shifted_right_two() {
2590        let t = TllRksTables::new();
2591        for block in 0..8 {
2592            for fnum8 in 0..2 {
2593                let idx = (block << 1) | fnum8;
2594                assert_eq!(t.rks[idx][0], (block >> 1) as u8);
2595                assert_eq!(t.rks[idx][1], ((block << 1) + fnum8) as u8);
2596            }
2597        }
2598    }
2599
2600    // -----------------------------------------------------------------------
2601    // LFO + operator + per-channel update tests
2602    // -----------------------------------------------------------------------
2603
2604    #[test]
2605    fn update_ampm_advances_am_phase_and_loads_lfo_am() {
2606        let mut opll = Opll::new(ChipType::Vrc7);
2607        let am0 = opll.lfo_am;
2608        // Drive 64 cycles — am_table index = (am_phase >> 6) — should advance by 1.
2609        for _ in 0..64 {
2610            opll.update_ampm();
2611        }
2612        let am1 = opll.lfo_am;
2613        // After 64 ticks the table index advances, so the value may
2614        // have changed (or stayed if next sample is the same).
2615        // Stronger assertion: drive 64 * 14 (one full peak-to-peak cycle)
2616        // and verify the LFO has visited multiple distinct values.
2617        let mut seen = alloc::vec::Vec::new();
2618        seen.push(am0);
2619        seen.push(am1);
2620        for _ in 0..(64 * 14) {
2621            opll.update_ampm();
2622            seen.push(opll.lfo_am);
2623        }
2624        let max = seen.iter().copied().max().unwrap();
2625        let min = seen.iter().copied().min().unwrap();
2626        assert!(max > min, "LFO did not sweep; seen min={min} max={max}");
2627        assert!(max <= 13, "LFO max should be 13 per AM_TABLE; got {max}");
2628    }
2629
2630    #[test]
2631    fn update_ampm_test_bit_1_resets_phases() {
2632        let mut opll = Opll::new(ChipType::Vrc7);
2633        for _ in 0..1000 {
2634            opll.update_ampm();
2635        }
2636        opll.test_flag = 0b10;
2637        opll.update_ampm();
2638        assert_eq!(opll.pm_phase, 0);
2639        assert_eq!(opll.am_phase, 0);
2640    }
2641
2642    #[test]
2643    fn opll_calc_silent_with_no_key_on() {
2644        let mut opll = Opll::new(ChipType::Vrc7);
2645        // Default state — all slots in Release, eg_out=EG_MUTE → mix_out = 0.
2646        for _ in 0..100 {
2647            assert_eq!(opll.calc(), 0);
2648        }
2649    }
2650
2651    #[test]
2652    fn opll_calc_runs_full_pipeline_advances_eg_counter() {
2653        let mut opll = Opll::new(ChipType::Vrc7);
2654        assert_eq!(opll.eg_counter, 0);
2655        opll.calc();
2656        assert_eq!(opll.eg_counter, 1);
2657        for _ in 0..99 {
2658            opll.calc();
2659        }
2660        assert_eq!(opll.eg_counter, 100);
2661    }
2662
2663    #[test]
2664    fn opll_keyed_on_channel_produces_nonzero_output_within_one_envelope() {
2665        let mut opll = Opll::new(ChipType::Vrc7);
2666        // Channel 0: assign VRC7 patch 1 (a non-zero instrument),
2667        // set frequency, key on. Then run enough cycles for the
2668        // envelope to traverse Damp → Attack → audible level.
2669        opll.set_patch(0, 1);
2670        opll.set_block(0, 4);
2671        opll.set_fnumber(0, 256);
2672        opll.set_volume(0, 0); // max volume (volume is attenuation; 0 = loud)
2673        opll.key_on(0);
2674        let mut peak_abs: i16 = 0;
2675        // Run ~16k cycles (≈ 330 ms at 49716 Hz) — should clear Damp
2676        // and reach Attack/Decay.
2677        for _ in 0..16_384 {
2678            let s = opll.calc();
2679            peak_abs = peak_abs.max(s.unsigned_abs() as i16);
2680        }
2681        assert!(
2682            peak_abs > 0,
2683            "expected non-silent output after key-on; peak_abs = {peak_abs}"
2684        );
2685    }
2686
2687    #[test]
2688    fn opll_reset_initializes_all_18_slots_to_release() {
2689        let mut opll = Opll::new(ChipType::Vrc7);
2690        // Mutate a slot to verify reset() truly resets it.
2691        opll.slot[0].eg_out = 0;
2692        opll.slot[0].eg_state = EgState::Attack;
2693        opll.reset();
2694        for (i, s) in opll.slot.iter().enumerate() {
2695            assert_eq!(s.eg_state, EgState::Release, "slot {i} eg_state");
2696            assert_eq!(s.eg_out, EG_MUTE, "slot {i} eg_out");
2697            assert_eq!(s.number, i as u8, "slot {i} number");
2698            assert_eq!(s.type_flags & 1, (i & 1) as u8, "slot {i} M/C bit");
2699        }
2700    }
2701
2702    // -----------------------------------------------------------------------
2703    // Save-state surface (v2.3.7)
2704    // -----------------------------------------------------------------------
2705
2706    /// Key a note and run it well past the attack phase, so the snapshot under
2707    /// test describes a genuinely mid-flight synthesizer rather than something
2708    /// a `reset()` could coincidentally reproduce.
2709    fn opll_mid_note() -> Opll {
2710        let mut opll = Opll::new(ChipType::Vrc7);
2711        opll.write_reg(0x30, 0x10); // channel 0: instrument 1, full volume
2712        opll.write_reg(0x10, 0xAD); // F-number low
2713        opll.write_reg(0x20, 0x15); // key on, block 2, F-number bit 8
2714        for _ in 0..600 {
2715            let _ = opll.calc();
2716        }
2717        opll
2718    }
2719
2720    #[test]
2721    fn opll_snapshot_length_matches_the_declared_constant() {
2722        // Pins SLOT_BYTES / PATCH_BYTES against the writer. A field added to
2723        // `Slot` without extending the arithmetic fails here rather than
2724        // silently shifting every subsequent field on restore.
2725        assert_eq!(
2726            Opll::new(ChipType::Vrc7).snapshot().len(),
2727            OPLL_SNAPSHOT_LEN
2728        );
2729    }
2730
2731    #[test]
2732    fn opll_snapshot_restore_reproduces_the_sample_stream_exactly() {
2733        let mut source = opll_mid_note();
2734        let blob = source.snapshot();
2735        let expected: Vec<i16> = (0..2000).map(|_| source.calc()).collect();
2736        assert!(
2737            expected.iter().any(|&s| s != 0),
2738            "fixture is silent — the comparison would pass vacuously"
2739        );
2740
2741        // Restore into a FRESH chip, not the one that produced the blob: this
2742        // has to work from power-on state, which is the actual rewind case.
2743        let mut restored = Opll::new(ChipType::Vrc7);
2744        restored.restore(&blob).expect("round-trip must load");
2745        let got: Vec<i16> = (0..2000).map(|_| restored.calc()).collect();
2746
2747        assert_eq!(
2748            got, expected,
2749            "restored OPLL diverged from the source stream"
2750        );
2751    }
2752
2753    #[test]
2754    fn opll_snapshot_is_stable_across_a_restore_cycle() {
2755        // Byte-level idempotence: snapshot -> restore -> snapshot must be the
2756        // same bytes. Catches a field that is written but not read back (which
2757        // the stream test above can miss if the field happens not to affect
2758        // the next 2000 samples).
2759        let source = opll_mid_note();
2760        let first = source.snapshot();
2761        let mut restored = Opll::new(ChipType::Vrc7);
2762        restored.restore(&first).unwrap();
2763        assert_eq!(restored.snapshot(), first);
2764    }
2765
2766    #[test]
2767    fn opll_restore_rejects_a_blob_from_a_different_chip() {
2768        // The patch ROM differs per chip type, so slot patches restored across
2769        // types would be reinterpreted against the wrong instrument set —
2770        // silently, since every field is otherwise structurally valid.
2771        let blob = Opll::new(ChipType::Ym2413).snapshot();
2772        let mut vrc7 = Opll::new(ChipType::Vrc7);
2773        let err = vrc7
2774            .restore(&blob)
2775            .expect_err("chip mismatch must be rejected");
2776        assert!(
2777            matches!(err, OpllStateError::ChipTypeMismatch { got: 0, want: 1 }),
2778            "expected ChipTypeMismatch, got {err:?}"
2779        );
2780    }
2781
2782    #[test]
2783    fn opll_restore_rejects_a_truncated_blob_without_mutating_state() {
2784        let source = opll_mid_note();
2785        let blob = source.snapshot();
2786        let mut target = Opll::new(ChipType::Vrc7);
2787        let before = target.snapshot();
2788
2789        let err = target
2790            .restore(&blob[..blob.len() - 1])
2791            .expect_err("a truncated blob must be rejected");
2792        assert!(
2793            matches!(err, OpllStateError::Truncated(_)),
2794            "expected Truncated, got {err:?}"
2795        );
2796        assert_eq!(
2797            target.snapshot(),
2798            before,
2799            "a rejected restore left the synthesizer half-overwritten"
2800        );
2801    }
2802
2803    /// A save state is a file on disk. A hand-edited one must not be able to
2804    /// crash the emulator.
2805    ///
2806    /// This FAILED before the parse-boundary masks: with every payload byte set
2807    /// to `0xFF` and only the envelope-state tags made valid, `commit_slot_update`
2808    /// computed a TLL index of 524,539 into a 32,768-entry table and panicked.
2809    ///
2810    /// Making the tags valid is the point of the test rather than an
2811    /// inconvenience. An all-`0xFF` blob is rejected by `EgState::from_tag`
2812    /// before any numeric field is touched, so the naive hostile input passes
2813    /// *by accident* and reports the emulator safe. The interesting input is the
2814    /// one that satisfies every explicit check and is still nonsense.
2815    #[test]
2816    fn opll_restore_survives_a_hostile_blob() {
2817        let mut blob = Opll::new(ChipType::Vrc7).snapshot();
2818        for b in blob.iter_mut().skip(2) {
2819            *b = 0xFF;
2820        }
2821        blob[0] = OPLL_SNAPSHOT_VERSION;
2822        blob[1] = ChipType::Vrc7.to_tag();
2823        for i in 0..SNAPSHOT_SLOTS {
2824            blob[EG_STATE_OFFSET + i * SLOT_BYTES] = EgState::Release.to_tag();
2825        }
2826
2827        let mut opll = Opll::new(ChipType::Vrc7);
2828        opll.restore(&blob)
2829            .expect("a structurally valid blob must load");
2830        // Run synthesis: the panic was not in `restore`, it was in the first
2831        // slot update the restored state provoked.
2832        for _ in 0..4_000 {
2833            let _ = opll.calc();
2834        }
2835
2836        // One fixed payload proves one path. Sweep pseudo-random ones too, so
2837        // the claim is "no hostile blob reaches a subscript" rather than "this
2838        // particular blob did not" -- which is the difference review asked
2839        // about. A tiny xorshift keeps it deterministic and dependency-free; a
2840        // flaky fuzz test would be worse than none.
2841        let mut state = 0x2545_F491_4F6C_DD1Du64;
2842        let mut next = move || {
2843            state ^= state << 13;
2844            state ^= state >> 7;
2845            state ^= state << 17;
2846            state
2847        };
2848        for round in 0..64 {
2849            let mut b = Opll::new(ChipType::Vrc7).snapshot();
2850            for byte in b.iter_mut().skip(2) {
2851                *byte = (next() & 0xFF) as u8;
2852            }
2853            b[0] = OPLL_SNAPSHOT_VERSION;
2854            b[1] = ChipType::Vrc7.to_tag();
2855            for i in 0..SNAPSHOT_SLOTS {
2856                // Keep the tag valid: an invalid one short-circuits the parse,
2857                // and the round would then prove nothing.
2858                b[EG_STATE_OFFSET + i * SLOT_BYTES] = (next() % 6) as u8;
2859            }
2860            let mut o = Opll::new(ChipType::Vrc7);
2861            o.restore(&b)
2862                .unwrap_or_else(|e| panic!("round {round}: valid-shaped blob rejected: {e}"));
2863            for _ in 0..1_000 {
2864                let _ = o.calc();
2865            }
2866
2867            // Then drive the REGISTER PORT on the restored chip. `calc()` alone
2868            // never exercises `write_reg`, so a restored field that is only
2869            // consumed on a subsequent port write -- `adr` is the candidate
2870            // review raised -- would sail past a synthesis-only sweep. Covering
2871            // both is cheaper than arguing about which fields reach a subscript,
2872            // and this session has shown my hand-tracing to be the less reliable
2873            // instrument.
2874            for _ in 0..64 {
2875                o.write_reg((next() & 0xFF) as u8, (next() & 0xFF) as u8);
2876                let _ = o.calc();
2877                let _ = o.read_reg((next() & 0xFF) as u8);
2878            }
2879        }
2880
2881        // And a hostile blob must not be able to smuggle out-of-range register
2882        // fields past the parse, which is what the masks are for.
2883        for (i, s) in opll.slot.iter().enumerate() {
2884            assert!(s.patch.tl <= 0x3F, "slot {i} tl out of range");
2885            assert!(s.patch.kl <= 0x03, "slot {i} kl out of range");
2886            assert!(s.blk_fnum <= 0x0FFF, "slot {i} blk_fnum out of range");
2887            assert!(usize::from(s.number) < SNAPSHOT_SLOTS, "slot {i} number");
2888        }
2889    }
2890
2891    #[test]
2892    fn opll_restore_rejects_an_unknown_version() {
2893        let mut blob = Opll::new(ChipType::Vrc7).snapshot();
2894        blob[0] = 99;
2895        let mut target = Opll::new(ChipType::Vrc7);
2896        assert!(matches!(
2897            target.restore(&blob),
2898            Err(OpllStateError::UnsupportedVersion(99))
2899        ));
2900    }
2901
2902    /// Byte offset of slot 0's `eg_state` tag within a snapshot blob.
2903    ///
2904    /// Header: `version(1) + chip_type(1) + adr(1) + reg(64) + test_flag(1) +
2905    /// slot_key_status/eg_counter/pm_phase/am_phase(16) + lfo_am(1) +
2906    /// patch_number(36) + user patch pair(26) = 147`. Then within slot 0:
2907    /// `number(1) + type_flags(1) + patch(13) + output(8) + wave_table_idx(1) +
2908    /// pg_phase(4) + pg_out(4) + pg_keep(1) + blk_fnum(2) + fnum(2) + blk(1) = 38`.
2909    const EG_STATE_OFFSET: usize = 147 + 38;
2910
2911    #[test]
2912    fn opll_restore_rejects_an_invalid_envelope_state_tag() {
2913        let mut blob = opll_mid_note().snapshot();
2914        assert!(blob[EG_STATE_OFFSET] <= 5, "offset does not point at a tag");
2915        blob[EG_STATE_OFFSET] = 6;
2916        let mut target = Opll::new(ChipType::Vrc7);
2917        assert!(matches!(
2918            target.restore(&blob),
2919            Err(OpllStateError::InvalidEgState(6))
2920        ));
2921    }
2922}