use crate::{DosAudio, Error, OplChip, Result};
mod bank;
mod driver;
pub use bank::{OplBank, OplBankEntry, OplBankKey, OplInstrument};
pub use driver::OplMidiDriver;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct OplOperator {
pub characteristics: u8,
pub level: u8,
pub attack_decay: u8,
pub sustain_release: u8,
pub waveform: u8,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct OplPatch {
pub modulator: OplOperator,
pub carrier: OplOperator,
pub feedback: u8,
}
impl OplPatch {
pub fn from_sbi(bytes: &[u8]) -> Result<Self> {
if bytes.len() < 52 || &bytes[..4] != b"SBI\x1a" {
return Err(Error::Format("invalid or truncated SBI instrument"));
}
let data = &bytes[36..52];
let operator = |offset: usize| OplOperator {
characteristics: data[offset],
level: data[offset + 2],
attack_decay: data[offset + 4],
sustain_release: data[offset + 6],
waveform: data[offset + 8],
};
Ok(Self {
modulator: operator(0),
carrier: operator(1),
feedback: data[10],
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Note(u8);
impl Note {
pub const C4: Self = Self(60);
pub const A4: Self = Self(69);
pub fn from_midi(note: u8) -> Result<Self> {
if note > 127 {
return Err(Error::Config("MIDI note must be in 0 through 127"));
}
Ok(Self(note))
}
pub fn frequency(self) -> f64 {
440.0 * 2.0f64.powf((f64::from(self.0) - 69.0) / 12.0)
}
fn tuning(self) -> Result<(u16, u8)> {
frequency_tuning(self.frequency())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Drum {
BassDrum,
Snare,
Tom,
Cymbal,
HiHat,
}
impl Drum {
fn mask(self) -> u8 {
match self {
Self::BassDrum => 0x10,
Self::Snare => 0x08,
Self::Tom => 0x04,
Self::Cymbal => 0x02,
Self::HiHat => 0x01,
}
}
}
pub struct Opl<'a> {
ports: &'a mut DosAudio,
chip: OplChip,
}
impl<'a> Opl<'a> {
pub(crate) fn new(ports: &'a mut DosAudio, chip: OplChip) -> Self {
Self { ports, chip }
}
fn channel(&self, channel: u8) -> Result<(u16, u16)> {
channel_address(self.chip, channel)
}
fn write(&mut self, writes: &[(u16, u8)]) -> Result<()> {
let mut batch = [(0, 0); 13];
let offset = usize::from(self.chip == OplChip::Opl3);
if offset != 0 {
batch[0] = (0x105, 1);
}
batch[offset..offset + writes.len()].copy_from_slice(writes);
self.ports.opl_write(&batch[..offset + writes.len()])
}
pub fn set_patch(&mut self, channel: u8, patch: &OplPatch) -> Result<()> {
let (channel_register, modulator) = self.channel(channel)?;
let mut routing = 0;
if self.chip == OplChip::Opl3 {
routing = self.ports.opl_register(0xc0 | channel_register)? & 0xf0;
if routing == 0 {
routing = 0x30;
}
}
let a = patch.modulator;
let b = patch.carrier;
let waveform_register = 0x01 | (modulator & 0x100);
let waveform_enable = self.ports.opl_register(waveform_register)? | 0x20;
self.write(&patch_writes(
channel_register,
modulator,
patch,
routing,
waveform_enable,
))?;
self.ports.opl_set_patch_levels(channel, [a.level, b.level])
}
pub fn note_on(&mut self, channel: u8, note: Note, velocity: u8) -> Result<()> {
let (channel_register, modulator) = self.channel(channel)?;
if velocity > 127 {
return Err(Error::Config("MIDI velocity must be in 0 through 127"));
}
if velocity == 0 {
return self.note_off(channel);
}
let (number, block) = note.tuning()?;
let levels = self.ports.opl_patch_levels(channel)?;
let attenuation = ((127 - u16::from(velocity)) * 63 + 63) / 127;
let attenuate =
|level: u8| (level & 0xc0) | ((u16::from(level & 0x3f) + attenuation).min(63) as u8);
let additive = self.ports.opl_register(0xc0 | channel_register)? & 1 != 0;
let old_key = self.ports.opl_register(0xb0 | channel_register)?;
self.write(&[
(
0x40 | modulator,
if additive {
attenuate(levels[0])
} else {
levels[0]
},
),
(0x40 | (modulator + 3), attenuate(levels[1])),
(0xa0 | channel_register, number as u8),
(
0xb0 | channel_register,
(old_key & 0xc0) | 0x20 | (block << 2) | (number >> 8) as u8,
),
])
}
pub fn note_off(&mut self, channel: u8) -> Result<()> {
let (channel_register, _) = self.channel(channel)?;
let register = 0xb0 | channel_register;
self.write(&[(register, self.ports.opl_register(register)? & !0x20)])
}
pub fn rhythm(&mut self, enabled: bool) -> Result<()> {
let value = self.ports.opl_register(0xbd)?;
self.write(&[(0xbd, if enabled { value | 0x20 } else { value & !0x3f })])
}
pub fn hit(&mut self, drum: Drum) -> Result<()> {
let value = self.ports.opl_register(0xbd)?;
self.write(&[
(0xbd, value & !drum.mask()),
(0xbd, value | 0x20 | drum.mask()),
])
}
pub fn release(&mut self, drum: Drum) -> Result<()> {
let value = self.ports.opl_register(0xbd)?;
self.write(&[(0xbd, value & !drum.mask())])
}
}
fn channel_address(chip: OplChip, channel: u8) -> Result<(u16, u16)> {
let channels = if chip == OplChip::Opl2 { 9 } else { 18 };
if channel >= channels {
return Err(Error::Config("OPL channel is outside the installed chip"));
}
let bank = u16::from(channel / 9) << 8;
let local = usize::from(channel % 9);
let operator = [0, 1, 2, 8, 9, 10, 16, 17, 18][local];
Ok((bank | u16::from(channel % 9), bank | operator))
}
fn frequency_tuning(hz: f64) -> Result<(u16, u8)> {
let native_rate = 315_000_000.0 / (22.0 * 288.0);
let mut number = hz * 1_048_576.0 / native_rate;
for block in 0..=7 {
let rounded = number.round();
if rounded <= 1023.0 {
return Ok((rounded as u16, block));
}
number *= 0.5;
}
Err(Error::Config("note exceeds the OPL frequency range"))
}
fn patch_writes(
channel: u16,
modulator: u16,
patch: &OplPatch,
routing: u8,
waveform_enable: u8,
) -> [(u16, u8); 12] {
let carrier = modulator + 3;
let a = patch.modulator;
let b = patch.carrier;
[
(0x01 | (modulator & 0x100), waveform_enable),
(0x20 | modulator, a.characteristics),
(0x20 | carrier, b.characteristics),
(0x40 | modulator, a.level),
(0x40 | carrier, b.level),
(0x60 | modulator, a.attack_decay),
(0x60 | carrier, b.attack_decay),
(0x80 | modulator, a.sustain_release),
(0x80 | carrier, b.sustain_release),
(0xe0 | modulator, a.waveform),
(0xe0 | carrier, b.waveform),
(0xc0 | channel, routing | (patch.feedback & 0x0f)),
]
}