use crate::operator::Operator;
pub const OPL_NATIVE_RATE: u32 = 49716;
pub const YM3812_CLOCK: u32 = 3_579_545;
pub const YMF262_CLOCK: u32 = 14_318_180;
pub const OPL2_CHANNELS: usize = 9;
pub const OPL3_CHANNELS: usize = 18;
struct OplChannel {
modulator: Operator,
carrier: Operator,
feedback: u8,
additive: bool,
pan_l: bool,
pan_r: bool,
fnum_low: u8,
fnum_high: u8,
block: u8,
}
impl OplChannel {
const fn new() -> Self {
Self {
modulator: Operator::new(),
carrier: Operator::new(),
feedback: 0,
additive: false,
pan_l: true,
pan_r: true,
fnum_low: 0,
fnum_high: 0,
block: 0,
}
}
fn refresh_freq(&mut self) {
let fnum = (self.fnum_low as u16) | (((self.fnum_high & 0x03) as u16) << 8);
self.modulator.set_frequency(fnum, self.block);
self.carrier.set_frequency(fnum, self.block);
}
fn is_silent(&self) -> bool {
self.carrier.is_silent() && (!self.additive || self.modulator.is_silent())
}
fn next_mono(&mut self, tremolo: u16, vibpos: u8, eg_cnt: u32) -> i32 {
let fb = self.modulator.feedback_phase(self.feedback);
let mod_out = self.modulator.next(fb, tremolo, vibpos, eg_cnt);
if self.additive {
let car = self.carrier.next(0, tremolo, vibpos, eg_cnt);
mod_out + car
} else {
self.carrier.next(mod_out, tremolo, vibpos, eg_cnt)
}
}
}
const CH_BD: usize = 6; const CH_HH_SD: usize = 7; const CH_TT_CY: usize = 8; const RM_BD: u8 = 0x10;
const RM_SD: u8 = 0x08;
const RM_TT: u8 = 0x04;
const RM_CY: u8 = 0x02;
const RM_HH: u8 = 0x01;
pub struct Opl2 {
channels: [OplChannel; OPL3_CHANNELS],
native_rate: u32,
resamp_step_q32: u64,
resamp_cursor_q32: u64,
native_hist: [(i32, i32); 4],
primed: bool,
lfo_timer: u32,
tremolopos: u32,
eg_cnt: u32,
rhythm_mode: bool,
rhythm_keys: u8,
noise: u32,
opl3_enable: bool,
fourop_mask: u8,
deep_tremolo: bool,
}
impl Opl2 {
pub fn new(output_rate: u32) -> Self {
Self::with_chip_clock(YM3812_CLOCK, output_rate)
}
pub fn with_chip_clock(chip_clock: u32, output_rate: u32) -> Self {
let native = (((chip_clock as u64 + 36) / 72).max(1)) as u32;
Self::with_native_rate(native, output_rate)
}
pub fn with_native_rate(native_rate: u32, output_rate: u32) -> Self {
let native = native_rate.max(1);
let r = output_rate.max(1) as u64;
Self {
channels: core::array::from_fn(|_| OplChannel::new()),
native_rate: native,
resamp_step_q32: ((native as u64) << 32) / r,
resamp_cursor_q32: 0,
native_hist: [(0, 0); 4],
primed: false,
lfo_timer: 0,
tremolopos: 0,
eg_cnt: 0,
rhythm_mode: false,
rhythm_keys: 0,
noise: 1,
opl3_enable: false,
fourop_mask: 0,
deep_tremolo: false,
}
}
pub fn native_rate(&self) -> u32 {
self.native_rate
}
pub fn set_opl3_enabled(&mut self, on: bool) {
self.opl3_enable = on;
for c in self.channels.iter_mut() {
c.modulator.set_opl3(on);
c.carrier.set_opl3(on);
}
}
pub fn opl3_enabled(&self) -> bool {
self.opl3_enable
}
pub fn write_reg(&mut self, reg: u8, val: u8) {
self.write_bank(0, reg, val);
}
pub fn write_reg_high(&mut self, reg: u8, val: u8) {
match reg {
0x04 => self.fourop_mask = val & 0x3F,
0x05 => self.set_opl3_enabled(val & 0x01 != 0),
_ => self.write_bank(OPL2_CHANNELS, reg, val),
}
}
fn write_bank(&mut self, base: usize, reg: u8, val: u8) {
fn op_slot(off: u8) -> Option<(usize, bool)> {
let (grp, idx) = (off / 8, off % 8);
if idx >= 6 || grp >= 3 {
return None;
}
let cbase = (grp * 3) as usize;
match idx {
0..=2 => Some((cbase + idx as usize, false)), 3..=5 => Some((cbase + (idx - 3) as usize, true)), _ => None,
}
}
fn op_mut(c: &mut OplChannel, carrier: bool) -> &mut Operator {
if carrier {
&mut c.carrier
} else {
&mut c.modulator
}
}
match reg {
0xBD if base == 0 => self.write_rhythm(val),
0x20..=0x35 => {
if let Some((ch, car)) = op_slot(reg - 0x20) {
if let Some(c) = self.channels.get_mut(base + ch) {
op_mut(c, car).write_reg20(val);
}
}
}
0x40..=0x55 => {
if let Some((ch, car)) = op_slot(reg - 0x40) {
if let Some(c) = self.channels.get_mut(base + ch) {
op_mut(c, car).write_reg40(val);
}
}
}
0x60..=0x75 => {
if let Some((ch, car)) = op_slot(reg - 0x60) {
if let Some(c) = self.channels.get_mut(base + ch) {
op_mut(c, car).write_reg60(val);
}
}
}
0x80..=0x95 => {
if let Some((ch, car)) = op_slot(reg - 0x80) {
if let Some(c) = self.channels.get_mut(base + ch) {
op_mut(c, car).write_reg80(val);
}
}
}
0xE0..=0xF5 => {
if let Some((ch, car)) = op_slot(reg - 0xE0) {
if let Some(c) = self.channels.get_mut(base + ch) {
op_mut(c, car).write_reg_e0(val);
}
}
}
0xA0..=0xA8 => {
let ch = (reg - 0xA0) as usize;
if let Some(c) = self.channels.get_mut(base + ch) {
c.fnum_low = val;
c.refresh_freq();
}
}
0xB0..=0xB8 => {
let ch = (reg - 0xB0) as usize;
let rhythm_ch = base == 0
&& self.rhythm_mode
&& (ch == CH_BD || ch == CH_HH_SD || ch == CH_TT_CY);
if let Some(c) = self.channels.get_mut(base + ch) {
c.fnum_high = val & 0x03;
c.block = (val >> 2) & 0x07;
c.refresh_freq();
if !rhythm_ch {
if val & 0x20 != 0 {
c.modulator.key_on();
c.carrier.key_on();
} else {
c.modulator.key_off();
c.carrier.key_off();
}
}
}
}
0xC0..=0xC8 => {
let opl3 = self.opl3_enable;
let ch = (reg - 0xC0) as usize;
if let Some(c) = self.channels.get_mut(base + ch) {
c.feedback = (val >> 1) & 0x07;
c.additive = val & 0x01 != 0;
if opl3 || val & 0x30 != 0 {
c.pan_l = val & 0x10 != 0;
c.pan_r = val & 0x20 != 0;
}
}
}
_ => {}
}
}
pub fn write_rhythm(&mut self, bd: u8) {
let vib_shift = ((bd >> 6) & 1) ^ 1;
for c in self.channels.iter_mut() {
c.modulator.set_vib_shift(vib_shift);
c.carrier.set_vib_shift(vib_shift);
}
self.deep_tremolo = bd & 0x80 != 0;
let was_enabled = self.rhythm_mode;
self.rhythm_mode = bd & 0x20 != 0;
let old = if was_enabled { self.rhythm_keys } else { 0 };
let new = bd & 0x1F;
self.rhythm_keys = new;
if !self.rhythm_mode {
return;
}
let edge = |mask: u8| (new & mask != 0, (new & mask) != (old & mask));
for &(ch, modu, mask) in &[
(CH_BD, true, RM_BD),
(CH_BD, false, RM_BD),
(CH_HH_SD, true, RM_HH),
(CH_HH_SD, false, RM_SD),
(CH_TT_CY, true, RM_TT),
(CH_TT_CY, false, RM_CY),
] {
let (on, changed) = edge(mask);
if !changed {
continue;
}
if let Some(c) = self.channels.get_mut(ch) {
let op = if modu { &mut c.modulator } else { &mut c.carrier };
if on {
op.key_on();
} else {
op.key_off();
}
}
}
}
pub fn rhythm_enabled(&self) -> bool {
self.rhythm_mode
}
pub fn channel_count(&self) -> usize {
if self.opl3_enable {
OPL3_CHANNELS
} else {
OPL2_CHANNELS
}
}
pub fn set_patch(&mut self, ch: usize, patch: &ChannelPatch) {
let Some(c) = self.channels.get_mut(ch) else {
return;
};
c.feedback = patch.feedback & 0x07;
c.additive = patch.additive;
let m = &patch.modulator;
c.modulator.set_patch(
m.mul, m.waveform, m.tl, m.ksl, m.ksr, m.sustaining, m.attack, m.decay, m.sustain,
m.release, m.am, m.vib,
);
let cr = &patch.carrier;
c.carrier.set_patch(
cr.mul, cr.waveform, cr.tl, cr.ksl, cr.ksr, cr.sustaining, cr.attack, cr.decay,
cr.sustain, cr.release, cr.am, cr.vib,
);
}
pub fn set_rhythm_operator(&mut self, ch: usize, carrier: bool, p: &OpPatch) {
let Some(c) = self.channels.get_mut(ch) else {
return;
};
let op = if carrier { &mut c.carrier } else { &mut c.modulator };
op.set_patch(
p.mul, p.waveform, p.tl, p.ksl, p.ksr, p.sustaining, p.attack, p.decay, p.sustain,
p.release, p.am, p.vib,
);
}
pub fn set_operator_frequency(&mut self, ch: usize, carrier: bool, fnum: u16, block: u8) {
if let Some(c) = self.channels.get_mut(ch) {
let op = if carrier { &mut c.carrier } else { &mut c.modulator };
op.set_frequency(fnum, block);
}
}
pub fn set_frequency(&mut self, ch: usize, fnum: u16, block: u8) {
if let Some(c) = self.channels.get_mut(ch) {
c.modulator.set_frequency(fnum, block);
c.carrier.set_frequency(fnum, block);
}
}
pub fn set_carrier_tl(&mut self, ch: usize, tl: u8) {
if let Some(c) = self.channels.get_mut(ch) {
c.carrier.set_total_level(tl);
if c.additive {
c.modulator.set_total_level(tl);
}
}
}
pub fn set_rhythm_operator_tl(&mut self, ch: usize, carrier: bool, tl: u8) {
if let Some(c) = self.channels.get_mut(ch) {
let op = if carrier { &mut c.carrier } else { &mut c.modulator };
op.set_total_level(tl);
}
}
pub fn set_pan(&mut self, ch: usize, left: bool, right: bool) {
if let Some(c) = self.channels.get_mut(ch) {
c.pan_l = left;
c.pan_r = right;
}
}
pub fn key_on(&mut self, ch: usize) {
if let Some(c) = self.channels.get_mut(ch) {
c.modulator.key_on();
c.carrier.key_on();
}
}
pub fn key_off(&mut self, ch: usize) {
if let Some(c) = self.channels.get_mut(ch) {
c.modulator.key_off();
c.carrier.key_off();
}
}
pub fn channel_is_silent(&self, ch: usize) -> bool {
self.channels.get(ch).map(|c| c.is_silent()).unwrap_or(true)
}
pub fn any_active(&self) -> bool {
self.channels.iter().any(|c| !c.is_silent())
}
fn render_native(&mut self) -> (i32, i32) {
self.lfo_timer = self.lfo_timer.wrapping_add(1);
self.eg_cnt = self.eg_cnt.wrapping_add(1);
let eg_cnt = self.eg_cnt;
if self.lfo_timer & 0x3f == 0x3f {
self.tremolopos = (self.tremolopos + 1) % 210;
}
let ramp = if self.tremolopos < 105 {
self.tremolopos
} else {
209 - self.tremolopos
};
let am26 = (ramp * 26) / 104;
let tremolo = (if self.deep_tremolo { am26 } else { am26 >> 2 }) as u16;
let vibpos = ((self.lfo_timer >> 10) & 7) as u8;
let mut l: i32 = 0;
let mut r: i32 = 0;
let rhythm = self.rhythm_mode;
let opl3 = self.opl3_enable;
let nch = self.channel_count();
for ch in 0..nch {
if rhythm && (ch == CH_HH_SD || ch == CH_TT_CY) {
continue;
}
if opl3 {
if let Some(bit) = fourop_secondary_bit(ch) {
if self.fourop_mask & (1 << bit) != 0 {
continue;
}
}
if let Some(bit) = fourop_primary_bit(ch) {
if self.fourop_mask & (1 << bit) != 0 {
let (dl, dr) = self.render_fourop(ch, tremolo, vibpos, eg_cnt);
l += dl;
r += dr;
continue;
}
}
}
let c = &mut self.channels[ch];
if c.is_silent() {
continue;
}
let s = c.next_mono(tremolo, vibpos, eg_cnt);
if c.pan_l {
l += s;
}
if c.pan_r {
r += s;
}
}
if rhythm {
let (dl, dr) = self.render_rhythm_percussion(tremolo, vibpos, eg_cnt);
l += dl;
r += dr;
}
let n_bit = ((self.noise >> 14) ^ self.noise) & 1;
self.noise = (self.noise >> 1) | (n_bit << 22);
(l, r)
}
fn render_fourop(&mut self, n: usize, tremolo: u16, vibpos: u8, eg_cnt: u32) -> (i32, i32) {
let sec = n + 3;
if self.channels[n].modulator.is_silent()
&& self.channels[n].carrier.is_silent()
&& self.channels[sec].modulator.is_silent()
&& self.channels[sec].carrier.is_silent()
{
return (0, 0);
}
let (lo, hi) = self.channels.split_at_mut(sec);
let c0 = &mut lo[n];
let c3 = &mut hi[0];
let (cnt0, cnt3) = (c0.additive, c3.additive);
let (p0l, p0r) = (c0.pan_l, c0.pan_r);
let (p3l, p3r) = (c3.pan_l, c3.pan_r);
let fb = c0.modulator.feedback_phase(c0.feedback);
let op1 = c0.modulator.next(fb, tremolo, vibpos, eg_cnt);
let mut l = 0i32;
let mut r = 0i32;
macro_rules! emit {
($v:expr, $pl:expr, $pr:expr) => {{
let v = $v;
if $pl {
l += v;
}
if $pr {
r += v;
}
}};
}
match (cnt0, cnt3) {
(false, false) => {
let op2 = c0.carrier.next(op1, tremolo, vibpos, eg_cnt);
let op3 = c3.modulator.next(op2, tremolo, vibpos, eg_cnt);
let op4 = c3.carrier.next(op3, tremolo, vibpos, eg_cnt);
emit!(op4, p3l, p3r);
}
(true, false) => {
let op2 = c0.carrier.next(0, tremolo, vibpos, eg_cnt);
let op3 = c3.modulator.next(op2, tremolo, vibpos, eg_cnt);
let op4 = c3.carrier.next(op3, tremolo, vibpos, eg_cnt);
emit!(op1, p0l, p0r);
emit!(op4, p3l, p3r);
}
(false, true) => {
let op2 = c0.carrier.next(op1, tremolo, vibpos, eg_cnt);
let op3 = c3.modulator.next(0, tremolo, vibpos, eg_cnt);
let op4 = c3.carrier.next(op3, tremolo, vibpos, eg_cnt);
emit!(op2, p0l, p0r);
emit!(op4, p3l, p3r);
}
(true, true) => {
let op2 = c0.carrier.next(0, tremolo, vibpos, eg_cnt);
let op3 = c3.modulator.next(op2, tremolo, vibpos, eg_cnt);
let op4 = c3.carrier.next(0, tremolo, vibpos, eg_cnt);
emit!(op1, p0l, p0r);
emit!(op3, p3l, p3r);
emit!(op4, p3l, p3r);
}
}
(l, r)
}
fn render_rhythm_percussion(&mut self, tremolo: u16, vibpos: u8, eg_cnt: u32) -> (i32, i32) {
let noise_bit = self.noise & 1;
self.channels[CH_HH_SD].modulator.advance(vibpos, eg_cnt); self.channels[CH_HH_SD].carrier.advance(vibpos, eg_cnt); self.channels[CH_TT_CY].modulator.advance(vibpos, eg_cnt); self.channels[CH_TT_CY].carrier.advance(vibpos, eg_cnt);
let hh_p = self.channels[CH_HH_SD].modulator.phase_out();
let cy_p = self.channels[CH_TT_CY].carrier.phase_out();
let bit = |v: u32, n: u32| (v >> n) & 1;
let rm = (bit(hh_p, 2) ^ bit(hh_p, 7))
| (bit(hh_p, 3) ^ bit(cy_p, 5))
| (bit(cy_p, 3) ^ bit(cy_p, 5));
let hh_phase = (rm << 9) | if (rm ^ noise_bit) != 0 { 0xD0 } else { 0x34 };
let hh_out = self.channels[CH_HH_SD].modulator.output_at(hh_phase, tremolo);
let hh_bit8 = bit(hh_p, 8);
let sd_phase = (hh_bit8 << 9) | ((hh_bit8 ^ noise_bit) << 8);
let sd_out = self.channels[CH_HH_SD].carrier.output_at(sd_phase, tremolo);
let tt_phase = self.channels[CH_TT_CY].modulator.phase_out() & 0x3FF;
let tt_out = self.channels[CH_TT_CY].modulator.output_at(tt_phase, tremolo);
let cy_phase = (rm << 9) | 0x80;
let cy_out = self.channels[CH_TT_CY].carrier.output_at(cy_phase, tremolo);
let d7 = (hh_out + sd_out) * 2;
let d8 = (tt_out + cy_out) * 2;
let mut l = 0;
let mut r = 0;
if self.channels[CH_HH_SD].pan_l {
l += d7;
}
if self.channels[CH_HH_SD].pan_r {
r += d7;
}
if self.channels[CH_TT_CY].pan_l {
l += d8;
}
if self.channels[CH_TT_CY].pan_r {
r += d8;
}
(l, r)
}
pub fn render_frame(&mut self) -> (i32, i32) {
if !self.primed {
for i in 0..4 {
self.native_hist[i] = self.render_native();
}
self.primed = true;
}
self.resamp_cursor_q32 += self.resamp_step_q32;
while self.resamp_cursor_q32 >= (1u64 << 32) {
self.resamp_cursor_q32 -= 1u64 << 32;
self.native_hist[0] = self.native_hist[1];
self.native_hist[1] = self.native_hist[2];
self.native_hist[2] = self.native_hist[3];
self.native_hist[3] = self.render_native();
}
let t = ((self.resamp_cursor_q32 & 0xFFFF_FFFF) >> 16) as i64;
let h = self.native_hist;
(
cubic(h[0].0, h[1].0, h[2].0, h[3].0, t),
cubic(h[0].1, h[1].1, h[2].1, h[3].1, t),
)
}
}
fn fourop_primary_bit(ch: usize) -> Option<u8> {
match ch {
0..=2 => Some(ch as u8),
9..=11 => Some((ch - 6) as u8),
_ => None,
}
}
fn fourop_secondary_bit(ch: usize) -> Option<u8> {
match ch {
3..=5 => Some((ch - 3) as u8),
12..=14 => Some((ch - 9) as u8),
_ => None,
}
}
#[inline]
fn cubic(p0: i32, p1: i32, p2: i32, p3: i32, t: i64) -> i32 {
let (p0, p1, p2, p3) = (p0 as i64, p1 as i64, p2 as i64, p3 as i64);
let a = -p0 + 3 * p1 - 3 * p2 + p3;
let b = 2 * p0 - 5 * p1 + 4 * p2 - p3;
let c = p2 - p0;
let inner = b + ((a * t) >> 16);
let inner = c + ((inner * t) >> 16);
let half = (inner * t) >> 16;
(p1 + (half >> 1)) as i32
}
#[derive(Clone, Copy, Default)]
pub struct OpPatch {
pub mul: u8,
pub waveform: u8,
pub tl: u8,
pub ksl: u8,
pub ksr: bool,
pub sustaining: bool,
pub attack: u8,
pub decay: u8,
pub sustain: u8,
pub release: u8,
pub am: bool,
pub vib: bool,
}
#[derive(Clone, Copy, Default)]
pub struct ChannelPatch {
pub modulator: OpPatch,
pub carrier: OpPatch,
pub feedback: u8,
pub additive: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opl2_raw_registers_render_a_tone() {
let mut opl = Opl2::new(44100);
opl.write_reg(0x20, 0x01); opl.write_reg(0x23, 0x01); opl.write_reg(0x40, 0x3F); opl.write_reg(0x43, 0x00); opl.write_reg(0x60, 0xF0); opl.write_reg(0x63, 0xF0); opl.write_reg(0x80, 0x00); opl.write_reg(0x83, 0x00); opl.write_reg(0xC0, 0x00); opl.write_reg(0xA0, 0x98); opl.write_reg(0xB0, 0x20 | (4 << 2) | 0x01); let mut peak = 0i32;
for _ in 0..8820 {
let (l, _r) = opl.render_frame();
peak = peak.max(l.abs());
}
assert!(peak > 100, "raw-register tone silent, peak={peak}");
}
#[test]
fn chip_renders_and_releases_a_tone() {
let mut chip = Opl2::new(44100);
let patch = ChannelPatch {
modulator: OpPatch {
mul: 1,
tl: 63, attack: 15,
release: 7,
sustaining: true,
..Default::default()
},
carrier: OpPatch {
mul: 1,
tl: 0,
attack: 15,
release: 7,
sustaining: true,
..Default::default()
},
..Default::default()
};
chip.set_patch(0, &patch);
chip.set_frequency(0, 0x2AE, 4);
chip.key_on(0);
let mut peak = 0i32;
for _ in 0..4410 {
let (l, _r) = chip.render_frame();
peak = peak.max(l.abs());
}
assert!(peak > 100, "expected an audible tone, peak={peak}");
chip.key_off(0);
for _ in 0..44100 {
chip.render_frame();
if chip.channel_is_silent(0) {
break;
}
}
assert!(chip.channel_is_silent(0), "channel should release to silence");
}
#[test]
fn non_standard_clock_scales_pitch() {
let a = Opl2::new(44100);
let b = Opl2::with_chip_clock(YM3812_CLOCK / 2, 44100);
assert_eq!(a.native_rate(), 49716);
assert_eq!(b.native_rate(), 49716 / 2);
}
#[test]
fn opl3_clock_and_opl2_clock_agree_on_native_rate() {
let opl2 = Opl2::with_chip_clock(YM3812_CLOCK, 44100);
let opl3 = Opl2::with_native_rate((YMF262_CLOCK + 144) / 288, 44100);
assert_eq!(opl2.native_rate(), opl3.native_rate());
assert_eq!(opl3.native_rate(), 49716);
}
#[test]
fn opl2_mode_ignores_second_bank_and_stays_9_channels() {
let mut opl = Opl2::new(44100);
assert_eq!(opl.channel_count(), 9);
for (r, v) in [(0x43u8, 0x00u8), (0x63, 0xF0), (0xA0, 0x98), (0xB0, 0x31)] {
opl.write_reg_high(r, v);
}
let mut peak = 0i32;
for _ in 0..4410 {
let (l, r) = opl.render_frame();
peak = peak.max(l.abs()).max(r.abs());
}
assert_eq!(peak, 0, "port-1 channel sounded in OPL2 mode (peak={peak})");
}
#[test]
fn opl3_fourop_group_sounds_and_consumes_secondary() {
let mut opl = Opl2::new(44100);
opl.write_reg_high(0x05, 0x01); opl.write_reg_high(0x04, 0x01); assert_eq!(fourop_primary_bit(0), Some(0));
assert_eq!(fourop_secondary_bit(3), Some(0));
for (r, v) in [
(0x20u8, 0x01u8),
(0x23, 0x01),
(0x40, 0x1F),
(0x43, 0x1F),
(0x60, 0xF0),
(0x63, 0xF0),
(0x80, 0x00),
(0x83, 0x00),
(0xC0, 0x30), (0xA0, 0x98),
(0xB0, 0x31), ] {
opl.write_reg(r, v);
}
for (r, v) in [
(0x28u8, 0x01u8),
(0x2B, 0x01),
(0x48, 0x1F),
(0x4B, 0x00), (0x68, 0xF0),
(0x6B, 0xF0),
(0x88, 0x00),
(0x8B, 0x00),
(0xC3, 0x30), (0xA3, 0x98),
(0xB3, 0x31), ] {
opl.write_reg(r, v);
}
let mut peak = 0i32;
for _ in 0..8820 {
let (l, _r) = opl.render_frame();
peak = peak.max(l.abs());
}
assert!(peak > 100, "4-op group silent (peak={peak})");
assert_eq!(opl.channel_count(), 18);
}
#[test]
fn fourop_pairing_map_is_correct() {
for (prim, bit) in [(0, 0), (1, 1), (2, 2), (9, 3), (10, 4), (11, 5)] {
assert_eq!(fourop_primary_bit(prim), Some(bit));
assert_eq!(fourop_secondary_bit(prim + 3), Some(bit));
assert_eq!(fourop_secondary_bit(prim), None);
assert_eq!(fourop_primary_bit(prim + 3), None);
}
for ch in [6, 7, 8, 15, 16, 17] {
assert_eq!(fourop_primary_bit(ch), None);
assert_eq!(fourop_secondary_bit(ch), None);
}
}
#[test]
fn opl2_render_golden_hash() {
let mut opl = Opl2::new(44100);
for (r, v) in [
(0x20u8, 0x21u8), (0x23, 0x21), (0x40, 0x10), (0x43, 0x00),
(0x60, 0xF2), (0x63, 0xF2), (0x80, 0x05), (0x83, 0x05),
(0xC0, 0x06), (0xA0, 0x98), (0xB0, 0x31),
(0x28, 0x22), (0x2B, 0x22), (0x48, 0x00), (0x4B, 0x00),
(0x68, 0xF4), (0x6B, 0xF4), (0x88, 0x03), (0x8B, 0x03),
(0xE8, 0x02), (0xEB, 0x03), (0xC8, 0x0A), (0xA8, 0x40), (0xB8, 0x25),
] {
opl.write_reg(r, v);
}
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for i in 0..44100u32 {
if i == 33000 {
opl.write_reg(0xB0, 0x11);
opl.write_reg(0xB8, 0x05);
}
let (l, r) = opl.render_frame();
for b in l.to_le_bytes().iter().chain(r.to_le_bytes().iter()) {
h ^= *b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
}
assert_eq!(h, 0xa17a_333a_f7d2_d119, "OPL2 render changed (hash {h:016x})");
}
#[test]
fn opl3_second_bank_channel_sounds_and_pans() {
let mut opl = Opl2::new(44100);
opl.write_reg_high(0x05, 0x01); assert!(opl.opl3_enabled());
assert_eq!(opl.channel_count(), 18);
opl.write_reg_high(0x40, 0x3F); opl.write_reg_high(0x43, 0x00); opl.write_reg_high(0x60, 0xF0);
opl.write_reg_high(0x63, 0xF0);
opl.write_reg_high(0x80, 0x0F);
opl.write_reg_high(0x83, 0x0F);
opl.write_reg_high(0xC0, 0x20); opl.write_reg_high(0xA0, 0x98);
opl.write_reg_high(0xB0, 0x31); let (mut lpk, mut rpk) = (0i32, 0i32);
for _ in 0..8820 {
let (l, r) = opl.render_frame();
lpk = lpk.max(l.abs());
rpk = rpk.max(r.abs());
}
assert!(rpk > 100, "port-1 channel silent on the right (peak={rpk})");
assert_eq!(lpk, 0, "right-panned channel leaked to the left ({lpk})");
}
}