use alloc::vec;
use alloc::vec::Vec;
use crate::tracker::instr_opl::{InstrOpl, MdiOpl, OplRhythm};
use super::{ChannelPatch, OpPatch, OplChip, OPL_NATIVE_RATE};
#[derive(Clone, Copy, Default)]
struct SlotState {
owner: Option<usize>, keyed: bool,
carrier_tl: u8,
modulator_tl: u8,
additive: bool,
}
#[derive(Clone, Copy)]
struct RhythmVoice {
track_ch: usize,
ch: usize,
carrier: bool,
full: bool,
resting_tl: u8,
}
pub struct OplDriver {
chip: OplChip,
slots: Vec<SlotState>,
rhythm_owner: Vec<(usize, u8)>,
rhythm_vol: Vec<RhythmVoice>,
rhythm_on: bool,
}
fn touch_tl(resting_tl: u8, vol: u8) -> u8 {
let v = vol.min(63) as i32;
(63 + (resting_tl as i32 * v / 63) - v).clamp(0, 63) as u8
}
fn rhythm_placement(role: OplRhythm) -> Option<(usize, bool, bool, u8)> {
match role {
OplRhythm::Melodic => None,
OplRhythm::BassDrum => Some((6, false, true, 0x10)),
OplRhythm::HiHat => Some((7, false, false, 0x01)),
OplRhythm::Snare => Some((7, true, false, 0x08)),
OplRhythm::TomTom => Some((8, false, false, 0x04)),
OplRhythm::Cymbal => Some((8, true, false, 0x02)),
}
}
impl OplDriver {
pub fn new(output_rate: u32) -> Self {
let chip = OplChip::new(output_rate);
let n = chip.channel_count();
Self {
chip,
slots: vec![SlotState::default(); n],
rhythm_owner: Vec::new(),
rhythm_vol: Vec::new(),
rhythm_on: false,
}
}
fn drum_bit_of(&self, track_ch: usize) -> Option<u8> {
self.rhythm_owner
.iter()
.find(|(c, _)| *c == track_ch)
.map(|(_, b)| *b)
}
fn set_drum(&mut self, track_ch: usize, bit: u8, on: bool) {
self.rhythm_owner.retain(|(c, _)| *c != track_ch);
if on {
self.rhythm_owner.push((track_ch, bit));
}
let drums = self.rhythm_owner.iter().fold(0u8, |a, (_, b)| a | b);
self.chip.write_rhythm(0x20 | drums); }
pub fn any_active(&self) -> bool {
self.chip.any_active()
}
pub fn render_frame(&mut self) -> (i32, i32) {
self.chip.render_frame()
}
fn assign_slot(&mut self, track_ch: usize) -> usize {
let melodic_max = if self.rhythm_on {
self.slots.len().min(6)
} else {
self.slots.len()
};
if let Some(i) = self.slots[..melodic_max]
.iter()
.position(|s| s.owner == Some(track_ch))
{
return i;
}
if let Some(i) = self.slots[..melodic_max]
.iter()
.position(|s| s.owner.is_none() || !s.keyed)
{
return i;
}
if let Some(i) = (0..melodic_max).find(|&i| self.chip.channel_is_silent(i)) {
return i;
}
0
}
fn slot_of(&self, track_ch: usize) -> Option<usize> {
self.slots.iter().position(|s| s.owner == Some(track_ch))
}
pub fn note_on(
&mut self,
track_ch: usize,
instr: &InstrOpl,
milli_hz: u32,
volume_0_63: u8,
pan_l: bool,
pan_r: bool,
) {
if let Some((ch, carrier, full, bit)) = rhythm_placement(instr.rhythm) {
self.rhythm_on = true;
let patch = channel_patch_from_instr(instr);
let (fnum, block) = hz_to_fnum_block(milli_hz);
let resting_tl = if full || carrier {
patch.carrier.tl
} else {
patch.modulator.tl
};
let touched = touch_tl(resting_tl, volume_0_63);
if full {
self.chip.set_patch(ch, &patch);
self.chip.set_frequency(ch, fnum, block);
self.chip.set_carrier_tl(ch, touched);
} else {
let op = if carrier {
&patch.carrier
} else {
&patch.modulator
};
self.chip.set_rhythm_operator(ch, carrier, op);
self.chip.set_operator_frequency(ch, carrier, fnum, block);
self.chip.set_rhythm_operator_tl(ch, carrier, touched);
}
self.chip.set_pan(ch, pan_l, pan_r);
self.rhythm_vol.retain(|v| v.track_ch != track_ch);
self.rhythm_vol.push(RhythmVoice {
track_ch,
ch,
carrier,
full,
resting_tl,
});
self.set_drum(track_ch, bit, false);
self.set_drum(track_ch, bit, true);
return;
}
self.rhythm_vol.retain(|v| v.track_ch != track_ch);
let slot = self.assign_slot(track_ch);
let patch = channel_patch_from_instr(instr);
self.slots[slot] = SlotState {
owner: Some(track_ch),
keyed: true,
carrier_tl: patch.carrier.tl,
modulator_tl: patch.modulator.tl,
additive: patch.additive,
};
self.chip.set_patch(slot, &patch);
let (fnum, block) = hz_to_fnum_block(milli_hz);
self.chip.set_frequency(slot, fnum, block);
self.chip.set_pan(slot, pan_l, pan_r);
self.apply_volume(slot, volume_0_63);
self.chip.key_on(slot);
}
pub fn set_frequency(&mut self, track_ch: usize, milli_hz: u32) {
if let Some(slot) = self.slot_of(track_ch) {
let (fnum, block) = hz_to_fnum_block(milli_hz);
self.chip.set_frequency(slot, fnum, block);
}
}
pub fn set_volume(&mut self, track_ch: usize, volume_0_63: u8) {
if let Some(v) = self
.rhythm_vol
.iter()
.find(|v| v.track_ch == track_ch)
.copied()
{
let tl = touch_tl(v.resting_tl, volume_0_63);
if v.full {
self.chip.set_carrier_tl(v.ch, tl);
} else {
self.chip.set_rhythm_operator_tl(v.ch, v.carrier, tl);
}
return;
}
if let Some(slot) = self.slot_of(track_ch) {
self.apply_volume(slot, volume_0_63);
}
}
pub fn set_pan(&mut self, track_ch: usize, pan_l: bool, pan_r: bool) {
if let Some(slot) = self.slot_of(track_ch) {
self.chip.set_pan(slot, pan_l, pan_r);
}
}
pub fn note_off(&mut self, track_ch: usize) {
if let Some(bit) = self.drum_bit_of(track_ch) {
self.set_drum(track_ch, bit, false);
return;
}
if let Some(slot) = self.slot_of(track_ch) {
self.chip.key_off(slot);
self.slots[slot].keyed = false;
}
}
pub fn note_cut(&mut self, track_ch: usize) {
if let Some(bit) = self.drum_bit_of(track_ch) {
self.set_drum(track_ch, bit, false);
return;
}
if let Some(slot) = self.slot_of(track_ch) {
self.chip.key_off(slot);
self.slots[slot] = SlotState::default();
}
}
fn apply_volume(&mut self, slot: usize, vol: u8) {
let st = self.slots[slot];
self.chip.set_carrier_tl(slot, touch_tl(st.carrier_tl, vol));
let _ = st.modulator_tl;
}
}
fn channel_patch_from_instr(instr: &InstrOpl) -> ChannelPatch {
let m = op_patch(
&instr.element.modulator,
instr.element.modulator_wave_select,
);
let c = op_patch(&instr.element.carrier, instr.element.carrier_wave_select);
ChannelPatch {
modulator: m,
carrier: c,
feedback: instr.element.modulator.feedback & 0x07,
additive: instr.element.modulator.con,
}
}
fn op_patch(mdi: &MdiOpl, waveform: u8) -> OpPatch {
OpPatch {
mul: mdi.multiple,
waveform,
tl: mdi.total_level,
ksl: mdi.ksl,
ksr: mdi.ksr,
sustaining: mdi.eg,
attack: mdi.attack,
decay: mdi.decay,
sustain: mdi.sustain,
release: mdi.release,
am: mdi.am,
vib: mdi.vib,
}
}
pub fn hz_to_fnum_block(milli_hz: u32) -> (u16, u8) {
let mhz = milli_hz as u64;
if mhz == 0 {
return (0, 0);
}
let block: u32 = if mhz > 3_104_215 {
7
} else if mhz > 1_552_107 {
6
} else if mhz > 776_053 {
5
} else if mhz > 388_026 {
4
} else if mhz > 194_013 {
3
} else if mhz > 97_006 {
2
} else if mhz > 48_503 {
1
} else {
0
};
let denom = OPL_NATIVE_RATE as u64 * 1000;
let mut fnum = (mhz << (20 - block)) / denom;
let mut block = block;
if fnum > 1023 && block < 7 {
block += 1;
fnum = (mhz << (20 - block)) / denom;
}
((fnum.min(1023)) as u16, block as u8)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fnum_block_round_trips_a440() {
let (fnum, block) = hz_to_fnum_block(440_000);
let freq = (fnum as u64 * OPL_NATIVE_RATE as u64) as f64 / (1u64 << (20 - block)) as f64;
assert!((freq - 440.0).abs() < 2.0, "freq={freq}");
}
#[test]
fn rhythm_instrument_routes_to_percussion() {
use crate::tracker::instr_opl::{InstrOpl, MdiInstr, MdiOpl, OplRhythm};
let mut drv = OplDriver::new(44100);
let op = MdiOpl {
multiple: 1,
attack: 15,
decay: 4,
sustain: 0,
release: 7,
total_level: 0,
..Default::default()
};
let bd = InstrOpl {
element: MdiInstr {
modulator: op,
carrier: op,
..Default::default()
},
volume: 63,
rhythm: OplRhythm::BassDrum,
..Default::default()
};
drv.note_on(0, &bd, 100_000, 63, true, true);
let mut peak = 0i32;
for _ in 0..4410 {
let (l, _r) = drv.render_frame();
peak = peak.max(l.abs());
}
assert!(peak > 100, "bass drum silent, peak={peak}");
assert!(
drv.slots.iter().all(|s| s.owner.is_none()),
"no melodic slot used"
);
}
#[test]
fn rhythm_note_volume_attenuates() {
use crate::tracker::instr_opl::{InstrOpl, MdiInstr, MdiOpl, OplRhythm};
let op = MdiOpl {
multiple: 1,
attack: 15,
eg: true,
total_level: 0,
..Default::default()
};
let make = |role| InstrOpl {
element: MdiInstr {
modulator: op,
carrier: op,
..Default::default()
},
volume: 63,
rhythm: role,
..Default::default()
};
let peak = |instr: &InstrOpl, vol: u8| -> i32 {
let mut drv = OplDriver::new(44100);
drv.note_on(0, instr, 200_000, vol, true, true);
let mut p = 0i32;
for _ in 0..4410 {
let (l, _r) = drv.render_frame();
p = p.max(l.abs());
}
p
};
for role in [OplRhythm::BassDrum, OplRhythm::Cymbal, OplRhythm::HiHat] {
let instr = make(role);
let loud = peak(&instr, 63);
let soft = peak(&instr, 12);
assert!(
loud > 100,
"{role:?}: full-volume drum silent (peak={loud})"
);
assert!(
soft * 2 < loud,
"{role:?}: note volume ignored (loud={loud}, soft={soft})"
);
}
}
#[test]
fn rhythm_set_volume_attenuates() {
use crate::tracker::instr_opl::{InstrOpl, MdiInstr, MdiOpl, OplRhythm};
let op = MdiOpl {
multiple: 1,
attack: 15,
eg: true,
total_level: 0,
..Default::default()
};
let instr = InstrOpl {
element: MdiInstr {
modulator: op,
carrier: op,
..Default::default()
},
volume: 63,
rhythm: OplRhythm::BassDrum,
..Default::default()
};
let mut drv = OplDriver::new(44100);
drv.note_on(0, &instr, 200_000, 63, true, true);
let mut loud = 0i32;
for _ in 0..2205 {
let (l, _r) = drv.render_frame();
loud = loud.max(l.abs());
}
drv.set_volume(0, 8);
for _ in 0..512 {
let _ = drv.render_frame();
}
let mut soft = 0i32;
for _ in 0..2205 {
let (l, _r) = drv.render_frame();
soft = soft.max(l.abs());
}
assert!(
loud > 100 && soft * 2 < loud,
"set_volume ignored (loud={loud}, soft={soft})"
);
}
}