pub mod driver;
mod operator;
mod tables;
use alloc::vec::Vec;
use operator::Operator;
pub(crate) const OPL_NATIVE_RATE: u32 = 49716;
pub(crate) const OPL2_CHANNELS: usize = 9;
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 {
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, 1 << 16);
self.carrier.set_frequency(fnum, self.block, 1 << 16);
}
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)
}
}
}
pub(crate) struct OplChip {
channels: Vec<OplChannel>,
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,
}
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;
impl OplChip {
pub(crate) fn new(output_rate: u32) -> Self {
let r = output_rate.max(1) as u64;
Self {
channels: (0..OPL2_CHANNELS).map(|_| OplChannel::new()).collect(),
resamp_step_q32: ((OPL_NATIVE_RATE 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,
}
}
pub(crate) fn write_rhythm(&mut self, bd: u8) {
self.rhythm_mode = bd & 0x20 != 0;
let old = self.rhythm_keys;
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(crate) fn rhythm_enabled(&self) -> bool {
self.rhythm_mode
}
pub(crate) fn write_reg(&mut self, 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 base = (grp * 3) as usize;
match idx {
0..=2 => Some((base + idx as usize, false)), 3..=5 => Some((base + (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 => self.write_rhythm(val),
0x20..=0x35 => {
if let Some((ch, car)) = op_slot(reg - 0x20) {
if let Some(c) = self.channels.get_mut(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(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(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(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(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(ch) {
c.fnum_low = val;
c.refresh_freq();
}
}
0xB0..=0xB8 => {
let ch = (reg - 0xB0) as usize;
let rhythm_ch =
self.rhythm_mode && (ch == CH_BD || ch == CH_HH_SD || ch == CH_TT_CY);
if let Some(c) = self.channels.get_mut(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 ch = (reg - 0xC0) as usize;
if let Some(c) = self.channels.get_mut(ch) {
c.feedback = (val >> 1) & 0x07;
c.additive = val & 0x01 != 0;
}
}
_ => {}
}
}
pub(crate) fn channel_count(&self) -> usize {
self.channels.len()
}
#[allow(clippy::too_many_arguments)]
pub(crate) 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(crate) 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(crate) 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, 1 << 16);
}
}
pub(crate) 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, 1 << 16);
c.carrier.set_frequency(fnum, block, 1 << 16);
}
}
pub(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) fn channel_is_silent(&self, ch: usize) -> bool {
self.channels.get(ch).map(|c| c.is_silent()).unwrap_or(true)
}
pub(crate) 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 = (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 && self.channels.len() >= 9;
for ch in 0..self.channels.len() {
if rhythm && (ch == CH_HH_SD || ch == CH_TT_CY) {
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;
}
self.noise = if self.noise & 1 != 0 {
(self.noise >> 1) ^ 0x0080_0302
} else {
self.noise >> 1
};
(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(crate) 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),
)
}
}
pub struct Opl2 {
chip: OplChip,
}
impl Opl2 {
pub fn new(output_rate: u32) -> Self {
Self {
chip: OplChip::new(output_rate),
}
}
pub fn write_reg(&mut self, reg: u8, val: u8) {
self.chip.write_reg(reg, val);
}
pub fn render_frame(&mut self) -> (i32, i32) {
self.chip.render_frame()
}
pub fn any_active(&self) -> bool {
self.chip.any_active()
}
}
#[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(crate) 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(crate) 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_a_tone() {
let mut chip = OplChip::new(44100);
let patch = ChannelPatch {
modulator: OpPatch {
mul: 1,
tl: 63, attack: 15,
decay: 0,
sustain: 0,
release: 7,
sustaining: true,
..Default::default()
},
carrier: OpPatch {
mul: 1,
tl: 0,
attack: 15,
decay: 0,
sustain: 0,
release: 7,
sustaining: true,
..Default::default()
},
feedback: 0,
additive: false,
};
chip.set_patch(0, &patch);
chip.set_frequency(0, 0x2AE, 4);
chip.key_on(0);
let mut peak = 0i32;
let mut nonzero = 0;
for _ in 0..4410 {
let (l, _r) = chip.render_frame();
peak = peak.max(l.abs());
if l != 0 {
nonzero += 1;
}
}
assert!(peak > 100, "expected an audible tone, peak={peak}");
assert!(
nonzero > 4000,
"expected a sustained tone, nonzero={nonzero}"
);
chip.key_off(0);
let mut tail_peak = 0i32;
for _ in 0..44100 {
let (l, _r) = chip.render_frame();
tail_peak = tail_peak.max(l.abs());
if chip.channel_is_silent(0) {
break;
}
}
assert!(
chip.channel_is_silent(0),
"channel should release to silence"
);
let _ = tail_peak;
}
#[test]
fn rhythm_mode_percussion_sounds() {
let mut chip = OplChip::new(44100);
let perc = ChannelPatch {
modulator: OpPatch {
mul: 1,
tl: 0,
attack: 15,
decay: 4,
sustain: 0,
release: 7,
sustaining: false,
..Default::default()
},
carrier: OpPatch {
mul: 1,
tl: 0,
attack: 15,
decay: 4,
sustain: 0,
release: 7,
sustaining: false,
..Default::default()
},
feedback: 0,
additive: false,
};
for ch in [CH_BD, CH_HH_SD, CH_TT_CY] {
chip.set_patch(ch, &perc);
chip.set_frequency(ch, 0x200, 4);
}
chip.write_rhythm(0x20 | RM_BD | RM_SD | RM_HH);
assert!(chip.rhythm_enabled());
let mut peak = 0i32;
for _ in 0..4410 {
let (l, _r) = chip.render_frame();
peak = peak.max(l.abs());
}
assert!(peak > 100, "rhythm percussion is silent, peak={peak}");
}
}