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(®)
989 || (0x29..=0x2F).contains(®)
990 || (0x39..=0x3F).contains(®)
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}