Skip to main content

game_toolkit_dosaudio/
config.rs

1use crate::{Error, Result, Tick};
2
3/// Sound Blaster playback hardware and DSP generation.
4#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
5#[non_exhaustive]
6pub enum SbModel {
7    /// Original Sound Blaster, DSP 1.05 and one OPL2.
8    Sb1,
9    /// Sound Blaster 2.0, DSP 2.01 and one OPL2.
10    #[default]
11    Sb2,
12    /// First Sound Blaster Pro, DSP 3.00 and dual OPL2.
13    SbPro,
14    /// Sound Blaster Pro 2, DSP 3.02 and OPL3.
15    SbPro2,
16    /// Sound Blaster 16, DSP 4.05 and OPL3.
17    Sb16,
18}
19
20impl SbModel {
21    /// Version returned by the DSP E1h command.
22    pub const fn dsp_version(self) -> (u8, u8) {
23        match self {
24            Self::Sb1 => (1, 5),
25            Self::Sb2 => (2, 1),
26            Self::SbPro => (3, 0),
27            Self::SbPro2 => (3, 2),
28            Self::Sb16 => (4, 5),
29        }
30    }
31    /// Installed FM chips.
32    pub const fn opl_chip(self) -> OplChip {
33        match self {
34            Self::Sb1 | Self::Sb2 => OplChip::Opl2,
35            Self::SbPro => OplChip::DualOpl2,
36            Self::SbPro2 | Self::Sb16 => OplChip::Opl3,
37        }
38    }
39    pub(crate) const fn has_mixer(self) -> bool {
40        matches!(self, Self::SbPro | Self::SbPro2 | Self::Sb16)
41    }
42}
43
44/// ISA resources shared by Sound Blaster playback models.
45///
46/// Select the DSP generation and word DMA separately on [`SoundConfig`].
47/// The host may service DRQ or attach an emulated memory DMA controller.
48/// Resources are validated when constructing the machine.
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub struct SoundBlasterConfig {
51    /// Base I/O address, aligned to 10h in 200h through 280h.
52    pub base: u16,
53    /// ISA interrupt line: 2, 3, 5 or 7; SB16 also supports 10.
54    pub irq: u8,
55    /// Byte-wide DMA channel: 0, 1 or 3.
56    pub dma: u8,
57}
58
59impl Default for SoundBlasterConfig {
60    fn default() -> Self {
61        Self {
62            base: 0x220,
63            irq: 5,
64            dma: 1,
65        }
66    }
67}
68
69/// Installed Yamaha FM hardware.
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub enum OplChip {
72    /// Nine-channel mono YM3812.
73    Opl2,
74    /// Two independent YM3812 chips, routed left and right.
75    DualOpl2,
76    /// YMF262, initially in its OPL2 compatibility mode.
77    Opl3,
78}
79
80/// FM synthesis implementation selected at machine creation.
81#[derive(Clone, Default)]
82pub enum OplBackend {
83    /// MIT-licensed clean-room core.
84    #[default]
85    Oplon,
86    /// LGPL-2.1-or-later reference core, compiled with the `nuked` feature.
87    #[cfg(feature = "nuked")]
88    Nuked,
89    /// A factory invoked once per physical chip, before rendering starts.
90    ///
91    /// Dual OPL2 calls it twice, producing independent cores. A custom core's
92    /// methods must follow the allocation and native-rate contract of `OplCore`.
93    #[cfg(feature = "opl")]
94    Custom(std::sync::Arc<dyn Fn() -> Box<dyn crate::OplCore> + Send + Sync>),
95}
96
97impl core::fmt::Debug for OplBackend {
98    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
99        f.write_str(match self {
100            Self::Oplon => "Oplon",
101            #[cfg(feature = "nuked")]
102            Self::Nuked => "Nuked",
103            #[cfg(feature = "opl")]
104            Self::Custom(_) => "Custom",
105        })
106    }
107}
108
109/// Hardware and output configuration. Builders are validated by `DosAudio::split`.
110#[derive(Clone, Debug)]
111pub struct SoundConfig {
112    pub(crate) opl: Option<OplChip>,
113    pub(crate) sb: Option<SoundBlasterConfig>,
114    pub(crate) sb_model: SbModel,
115    pub(crate) dma16: u8,
116    pub(crate) backend: OplBackend,
117    pub(crate) speaker: bool,
118    pub(crate) native: bool,
119    pub(crate) psg: bool,
120    pub(crate) midi: Option<crate::MidiBackend>,
121    pub(crate) midi_port: u16,
122    pub(crate) sample_rate: u32,
123    pub(crate) queue_capacity: usize,
124    pub(crate) latency: Tick,
125}
126
127impl Default for SoundConfig {
128    fn default() -> Self {
129        Self::adlib()
130    }
131}
132
133impl SoundConfig {
134    /// No installed sound hardware.
135    pub fn none() -> Self {
136        Self {
137            opl: None,
138            sb: None,
139            sb_model: SbModel::Sb2,
140            dma16: 5,
141            backend: OplBackend::Oplon,
142            speaker: false,
143            native: false,
144            psg: false,
145            midi: None,
146            midi_port: 0x330,
147            sample_rate: 48_000,
148            queue_capacity: 16_384,
149            latency: Tick::from_micros(10_000),
150        }
151    }
152
153    /// An AdLib with an OPL2 at ports 388h/389h.
154    pub fn adlib() -> Self {
155        Self {
156            opl: Some(OplChip::Opl2),
157            ..Self::none()
158        }
159    }
160
161    /// Sound Blaster 2.0 at 220h, IRQ 5, DMA 1, with its OPL2.
162    pub fn sb2() -> Self {
163        Self::none().sound_blaster(SoundBlasterConfig::default())
164    }
165
166    /// An original Sound Blaster using the default ISA resources.
167    pub fn sb1() -> Self {
168        Self::none().sb_model(SbModel::Sb1)
169    }
170    /// A first-generation Sound Blaster Pro with dual OPL2.
171    pub fn sb_pro() -> Self {
172        Self::none().sb_model(SbModel::SbPro)
173    }
174    /// A Sound Blaster Pro 2 with OPL3.
175    pub fn sb_pro2() -> Self {
176        Self::none().sb_model(SbModel::SbPro2)
177    }
178    /// A Sound Blaster 16, with byte DMA 1 and word DMA 5.
179    pub fn sb16() -> Self {
180        Self::none().sb_model(SbModel::Sb16)
181    }
182
183    /// Select a DSP model, installing default ISA resources if no card is present.
184    #[must_use]
185    pub fn sb_model(mut self, model: SbModel) -> Self {
186        self.sb_model = model;
187        self.sb.get_or_insert_with(SoundBlasterConfig::default);
188        self.opl = Some(model.opl_chip());
189        self
190    }
191
192    /// Select SB16 DMA: word channel 5/6/7, or the same byte channel as D.
193    #[must_use]
194    pub fn dma16(mut self, channel: u8) -> Self {
195        self.dma16 = channel;
196        self
197    }
198
199    /// Parse a BLASTER value containing A (hex), I, D, T and optional H fields.
200    ///
201    /// A/I/D/T are required; T values 1/2/3/4/6 select SB1/Pro/SB2/Pro2/SB16.
202    /// H is valid only for SB16 and defaults to 5. Unknown or duplicate fields
203    /// are errors. Resource and feature validation also runs at construction.
204    pub fn from_blaster(value: &str) -> Result<Self> {
205        let mut fields = [None; 5];
206        for field in value.split_ascii_whitespace() {
207            if !field.is_ascii() {
208                return Err(Error::Config("BLASTER fields must be ASCII"));
209            }
210            let (key, number) = field.split_at(1);
211            let index = match key.to_ascii_uppercase().as_str() {
212                "A" => 0,
213                "I" => 1,
214                "D" => 2,
215                "T" => 3,
216                "H" => 4,
217                _ => return Err(Error::Config("unsupported BLASTER field")),
218            };
219            if fields[index].is_some() {
220                return Err(Error::Config("duplicate BLASTER field"));
221            }
222            let number = if index == 0 {
223                number
224                    .strip_prefix("0x")
225                    .or_else(|| number.strip_prefix("0X"))
226                    .unwrap_or(number)
227            } else {
228                number
229            };
230            fields[index] = Some(
231                u16::from_str_radix(number, if index == 0 { 16 } else { 10 })
232                    .map_err(|_| Error::Config("invalid BLASTER field value"))?,
233            );
234        }
235        let [Some(base), Some(irq), Some(dma), Some(card), high] = fields else {
236            return Err(Error::Config("BLASTER requires A, I, D and T fields"));
237        };
238        let model = match card {
239            1 => SbModel::Sb1,
240            2 => SbModel::SbPro,
241            3 => SbModel::Sb2,
242            4 => SbModel::SbPro2,
243            6 => SbModel::Sb16,
244            _ => return Err(Error::Config("unsupported BLASTER card type")),
245        };
246        if high.is_some() && model != SbModel::Sb16 {
247            return Err(Error::Config("BLASTER H requires an SB16"));
248        }
249        let resources = SoundBlasterConfig {
250            base,
251            irq: u8::try_from(irq).map_err(|_| Error::Config("invalid BLASTER IRQ"))?,
252            dma: u8::try_from(dma).map_err(|_| Error::Config("invalid BLASTER DMA"))?,
253        };
254        let config = Self::none().sb_model(model).sound_blaster(resources).dma16(
255            u8::try_from(high.unwrap_or(5))
256                .map_err(|_| Error::Config("invalid BLASTER high DMA"))?,
257        );
258        config.validate()?;
259        Ok(config)
260    }
261
262    /// Install a Sound Blaster 2.0 and its OPL2 using the supplied ISA resources.
263    #[must_use]
264    pub fn sound_blaster(mut self, resources: SoundBlasterConfig) -> Self {
265        self.sb = Some(resources);
266        self.opl = Some(self.sb_model.opl_chip());
267        self
268    }
269
270    /// An OPL3 at ports 388h through 38Bh.
271    pub fn opl3() -> Self {
272        Self {
273            opl: Some(OplChip::Opl3),
274            ..Self::none()
275        }
276    }
277
278    /// Two OPL2 chips at ports 388h through 38Bh.
279    pub fn dual_opl2() -> Self {
280        Self {
281            opl: Some(OplChip::DualOpl2),
282            ..Self::none()
283        }
284    }
285
286    /// Install or remove PIT channel 2 and the PC speaker.
287    #[must_use]
288    pub fn speaker(mut self, enabled: bool) -> Self {
289        self.speaker = enabled;
290        self
291    }
292
293    /// Enable prepared native PCM/synthie music alongside the hardware devices.
294    #[cfg(feature = "synthie")]
295    #[must_use]
296    pub fn native(mut self, enabled: bool) -> Self {
297        self.native = enabled;
298        self
299    }
300
301    /// Install an MPU-401 UART and MIDI synthesis backend.
302    #[must_use]
303    pub fn mpu401(mut self, backend: crate::MidiBackend) -> Self {
304        self.midi = Some(backend);
305        self
306    }
307    /// General MIDI using an MPU-401 UART and caller-supplied SoundFont.
308    #[cfg(feature = "gm")]
309    pub fn gm(font: std::sync::Arc<crate::SoundFont>) -> Self {
310        Self::none().mpu401(crate::MidiBackend::Gm(font))
311    }
312    /// CM-32L synthesis for MT-32 arrangements with caller-supplied ROMs.
313    #[cfg(feature = "mt32")]
314    pub fn mt32(roms: std::sync::Arc<crate::Mt32Roms>) -> Self {
315        Self::none().mpu401(crate::MidiBackend::Mt32(roms))
316    }
317
318    /// Relocate the MPU-401 to an aligned base in 300h through 360h.
319    #[must_use]
320    pub fn midi_port(mut self, base: u16) -> Self {
321        self.midi_port = base;
322        self
323    }
324    /// Install or remove the Tandy SN76489 PSG at C0h.
325    #[must_use]
326    pub fn psg(mut self, enabled: bool) -> Self {
327        self.psg = enabled;
328        self
329    }
330
331    /// Select the FM synthesis backend.
332    #[must_use]
333    pub fn opl_backend(mut self, backend: OplBackend) -> Self {
334        self.backend = backend;
335        self
336    }
337
338    /// Output sample rate, from 8,000 through 192,000 Hz.
339    #[must_use]
340    pub fn sample_rate(mut self, sample_rate: u32) -> Self {
341        self.sample_rate = sample_rate;
342        self
343    }
344
345    /// Fixed command queue capacity; must be nonzero.
346    #[must_use]
347    pub fn queue_capacity(mut self, capacity: usize) -> Self {
348        self.queue_capacity = capacity;
349        self
350    }
351
352    /// Delay timestamped port writes by this much output time.
353    ///
354    /// Live playback defaults to 10 ms. Offline machines force zero latency.
355    /// Increase this for a host with a larger audio callback or game-frame jitter.
356    #[must_use]
357    pub fn latency(mut self, latency: Tick) -> Self {
358        self.latency = latency;
359        self
360    }
361
362    pub(crate) fn validate(&self) -> Result<()> {
363        if self.midi.is_some() {
364            if !cfg!(feature = "mpu401") {
365                return Err(Error::Config("MPU-401 requires the mpu401 feature"));
366            }
367            if !(0x300..=0x360).contains(&self.midi_port) || self.midi_port & 0xf != 0 {
368                return Err(Error::Config("invalid MPU-401 base"));
369            }
370        }
371        if self.psg && !cfg!(feature = "psg") {
372            return Err(Error::Config("PSG requires the psg feature"));
373        }
374        if let Some(sb) = self.sb {
375            if !cfg!(feature = "sb") {
376                return Err(Error::Config("Sound Blaster requires the sb feature"));
377            }
378            if !(0x200..=0x280).contains(&sb.base) || sb.base & 0x0f != 0 {
379                return Err(Error::Config("invalid Sound Blaster base address"));
380            }
381            if !matches!(sb.irq, 2 | 3 | 5 | 7 | 10) || !matches!(sb.dma, 0 | 1 | 3) {
382                return Err(Error::Config("invalid Sound Blaster IRQ or DMA channel"));
383            }
384            if sb.irq == 10 && self.sb_model != SbModel::Sb16 {
385                return Err(Error::Config("IRQ 10 requires an SB16"));
386            }
387            if !(5..=7).contains(&self.dma16)
388                && !(self.sb_model == SbModel::Sb16 && self.dma16 == sb.dma)
389            {
390                return Err(Error::Config("word DMA channel must be 5, 6 or 7"));
391            }
392        }
393        if !(8_000..=192_000).contains(&self.sample_rate) {
394            return Err(Error::Config("sample rate must be 8,000..=192,000 Hz"));
395        }
396        if self.queue_capacity == 0 || self.queue_capacity > 1_048_576 {
397            return Err(Error::Config("queue capacity must be 1..=1,048,576"));
398        }
399        if self.opl.is_some() && !cfg!(feature = "opl") {
400            return Err(Error::Config("OPL hardware requires the opl feature"));
401        }
402        Ok(())
403    }
404}