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,
}
impl OplChannel {
fn new() -> Self {
Self {
modulator: Operator::new(),
carrier: Operator::new(),
feedback: 0,
additive: false,
pan_l: true,
pan_r: true,
}
}
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,
}
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,
}
}
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_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_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;
for c in &mut self.channels {
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;
}
}
(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),
)
}
}
#[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 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;
}
}