Skip to main content

game_toolkit_dosaudio/
opl.rs

1//! Musical helpers that program the installed OPL through its normal register path.
2
3use crate::{DosAudio, Error, OplChip, Result};
4
5mod bank;
6mod driver;
7pub use bank::{OplBank, OplBankEntry, OplBankKey, OplInstrument};
8pub use driver::OplMidiDriver;
9
10/// The five register values describing one FM operator.
11#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
12pub struct OplOperator {
13    /// Tremolo, vibrato, envelope mode, key scaling and frequency multiplier (20h).
14    pub characteristics: u8,
15    /// Key-scale level and total attenuation (40h).
16    pub level: u8,
17    /// Attack and decay rates (60h).
18    pub attack_decay: u8,
19    /// Sustain attenuation and release rate (80h).
20    pub sustain_release: u8,
21    /// Waveform selection (E0h).
22    pub waveform: u8,
23}
24
25/// A two-operator instrument, including channel feedback and connection mode.
26#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
27pub struct OplPatch {
28    /// First operator, used as a modulator in FM connection mode.
29    pub modulator: OplOperator,
30    /// Second operator, used as the carrier in FM connection mode.
31    pub carrier: OplOperator,
32    /// Feedback and connection bits, the low four bits of register C0h.
33    pub feedback: u8,
34}
35
36impl OplPatch {
37    /// Read an SBI instrument: `SBI\x1a`, a 32-byte name and 16 instrument bytes.
38    /// Trailing metadata is accepted; the name and five reserved data bytes are ignored.
39    pub fn from_sbi(bytes: &[u8]) -> Result<Self> {
40        if bytes.len() < 52 || &bytes[..4] != b"SBI\x1a" {
41            return Err(Error::Format("invalid or truncated SBI instrument"));
42        }
43        let data = &bytes[36..52];
44        let operator = |offset: usize| OplOperator {
45            characteristics: data[offset],
46            level: data[offset + 2],
47            attack_decay: data[offset + 4],
48            sustain_release: data[offset + 6],
49            waveform: data[offset + 8],
50        };
51        Ok(Self {
52            modulator: operator(0),
53            carrier: operator(1),
54            feedback: data[10],
55        })
56    }
57}
58
59/// An equal-tempered MIDI note, with A4 tuned to 440 Hz.
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61pub struct Note(u8);
62
63impl Note {
64    /// Middle C, MIDI note 60.
65    pub const C4: Self = Self(60);
66    /// Concert A, MIDI note 69.
67    pub const A4: Self = Self(69);
68
69    /// Accept MIDI note numbers 0 through 127.
70    pub fn from_midi(note: u8) -> Result<Self> {
71        if note > 127 {
72            return Err(Error::Config("MIDI note must be in 0 through 127"));
73        }
74        Ok(Self(note))
75    }
76
77    /// Frequency before quantization to the OPL's F-number and block.
78    pub fn frequency(self) -> f64 {
79        440.0 * 2.0f64.powf((f64::from(self.0) - 69.0) / 12.0)
80    }
81
82    fn tuning(self) -> Result<(u16, u8)> {
83        frequency_tuning(self.frequency())
84    }
85}
86
87/// Percussion key bits in the first OPL bank's rhythm register.
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
89pub enum Drum {
90    /// Bass drum, using channel 6.
91    BassDrum,
92    /// Snare drum, using channel 7's carrier.
93    Snare,
94    /// Tom, using channel 8's modulator.
95    Tom,
96    /// Cymbal, using channel 8's carrier.
97    Cymbal,
98    /// Hi-hat, using channel 7's modulator.
99    HiHat,
100}
101
102impl Drum {
103    fn mask(self) -> u8 {
104        match self {
105            Self::BassDrum => 0x10,
106            Self::Snare => 0x08,
107            Self::Tom => 0x04,
108            Self::Cymbal => 0x02,
109            Self::HiHat => 0x01,
110        }
111    }
112}
113
114/// Borrowed musical access to the same FM hardware used by port I/O and music.
115pub struct Opl<'a> {
116    ports: &'a mut DosAudio,
117    chip: OplChip,
118}
119
120impl<'a> Opl<'a> {
121    pub(crate) fn new(ports: &'a mut DosAudio, chip: OplChip) -> Self {
122        Self { ports, chip }
123    }
124
125    fn channel(&self, channel: u8) -> Result<(u16, u16)> {
126        channel_address(self.chip, channel)
127    }
128
129    fn write(&mut self, writes: &[(u16, u8)]) -> Result<()> {
130        let mut batch = [(0, 0); 13];
131        let offset = usize::from(self.chip == OplChip::Opl3);
132        if offset != 0 {
133            batch[0] = (0x105, 1);
134        }
135        batch[offset..offset + writes.len()].copy_from_slice(writes);
136        self.ports.opl_write(&batch[..offset + writes.len()])
137    }
138
139    /// Program a two-operator patch, preserving OPL3 output routing.
140    /// Channels without explicit OPL3 routing default to both stereo outputs.
141    pub fn set_patch(&mut self, channel: u8, patch: &OplPatch) -> Result<()> {
142        let (channel_register, modulator) = self.channel(channel)?;
143        let mut routing = 0;
144        if self.chip == OplChip::Opl3 {
145            routing = self.ports.opl_register(0xc0 | channel_register)? & 0xf0;
146            if routing == 0 {
147                routing = 0x30;
148            }
149        }
150        let a = patch.modulator;
151        let b = patch.carrier;
152        let waveform_register = 0x01 | (modulator & 0x100);
153        let waveform_enable = self.ports.opl_register(waveform_register)? | 0x20;
154        self.write(&patch_writes(
155            channel_register,
156            modulator,
157            patch,
158            routing,
159            waveform_enable,
160        ))?;
161        self.ports.opl_set_patch_levels(channel, [a.level, b.level])
162    }
163
164    /// Start a note with MIDI velocity 1 through 127; zero releases the note.
165    /// Pitch uses the lowest fitting block and the nearest F-number. Notes above
166    /// the chip's maximum frequency return an error before changing registers.
167    pub fn note_on(&mut self, channel: u8, note: Note, velocity: u8) -> Result<()> {
168        let (channel_register, modulator) = self.channel(channel)?;
169        if velocity > 127 {
170            return Err(Error::Config("MIDI velocity must be in 0 through 127"));
171        }
172        if velocity == 0 {
173            return self.note_off(channel);
174        }
175        let (number, block) = note.tuning()?;
176        let levels = self.ports.opl_patch_levels(channel)?;
177        let attenuation = ((127 - u16::from(velocity)) * 63 + 63) / 127;
178        let attenuate =
179            |level: u8| (level & 0xc0) | ((u16::from(level & 0x3f) + attenuation).min(63) as u8);
180        let additive = self.ports.opl_register(0xc0 | channel_register)? & 1 != 0;
181        let old_key = self.ports.opl_register(0xb0 | channel_register)?;
182        self.write(&[
183            (
184                0x40 | modulator,
185                if additive {
186                    attenuate(levels[0])
187                } else {
188                    levels[0]
189                },
190            ),
191            (0x40 | (modulator + 3), attenuate(levels[1])),
192            (0xa0 | channel_register, number as u8),
193            (
194                0xb0 | channel_register,
195                (old_key & 0xc0) | 0x20 | (block << 2) | (number >> 8) as u8,
196            ),
197        ])
198    }
199
200    /// Release a melodic key without changing its block or F-number.
201    pub fn note_off(&mut self, channel: u8) -> Result<()> {
202        let (channel_register, _) = self.channel(channel)?;
203        let register = 0xb0 | channel_register;
204        self.write(&[(register, self.ports.opl_register(register)? & !0x20)])
205    }
206
207    /// Enable percussion on bank zero; disabling also releases all drum keys.
208    /// Tremolo and vibrato depth bits remain unchanged.
209    pub fn rhythm(&mut self, enabled: bool) -> Result<()> {
210        let value = self.ports.opl_register(0xbd)?;
211        self.write(&[(0xbd, if enabled { value | 0x20 } else { value & !0x3f })])
212    }
213
214    /// Enable rhythm mode and retrigger one drum, preserving the other drum keys.
215    pub fn hit(&mut self, drum: Drum) -> Result<()> {
216        let value = self.ports.opl_register(0xbd)?;
217        self.write(&[
218            (0xbd, value & !drum.mask()),
219            (0xbd, value | 0x20 | drum.mask()),
220        ])
221    }
222
223    /// Release one drum without changing rhythm mode or the other drum keys.
224    pub fn release(&mut self, drum: Drum) -> Result<()> {
225        let value = self.ports.opl_register(0xbd)?;
226        self.write(&[(0xbd, value & !drum.mask())])
227    }
228}
229
230fn channel_address(chip: OplChip, channel: u8) -> Result<(u16, u16)> {
231    let channels = if chip == OplChip::Opl2 { 9 } else { 18 };
232    if channel >= channels {
233        return Err(Error::Config("OPL channel is outside the installed chip"));
234    }
235    let bank = u16::from(channel / 9) << 8;
236    let local = usize::from(channel % 9);
237    let operator = [0, 1, 2, 8, 9, 10, 16, 17, 18][local];
238    Ok((bank | u16::from(channel % 9), bank | operator))
239}
240
241fn frequency_tuning(hz: f64) -> Result<(u16, u8)> {
242    let native_rate = 315_000_000.0 / (22.0 * 288.0);
243    let mut number = hz * 1_048_576.0 / native_rate;
244    for block in 0..=7 {
245        let rounded = number.round();
246        if rounded <= 1023.0 {
247            return Ok((rounded as u16, block));
248        }
249        number *= 0.5;
250    }
251    Err(Error::Config("note exceeds the OPL frequency range"))
252}
253
254fn patch_writes(
255    channel: u16,
256    modulator: u16,
257    patch: &OplPatch,
258    routing: u8,
259    waveform_enable: u8,
260) -> [(u16, u8); 12] {
261    let carrier = modulator + 3;
262    let a = patch.modulator;
263    let b = patch.carrier;
264    [
265        (0x01 | (modulator & 0x100), waveform_enable),
266        (0x20 | modulator, a.characteristics),
267        (0x20 | carrier, b.characteristics),
268        (0x40 | modulator, a.level),
269        (0x40 | carrier, b.level),
270        (0x60 | modulator, a.attack_decay),
271        (0x60 | carrier, b.attack_decay),
272        (0x80 | modulator, a.sustain_release),
273        (0x80 | carrier, b.sustain_release),
274        (0xe0 | modulator, a.waveform),
275        (0xe0 | carrier, b.waveform),
276        (0xc0 | channel, routing | (patch.feedback & 0x0f)),
277    ]
278}