use crate::{Error, Result, Tick};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum SbModel {
Sb1,
#[default]
Sb2,
SbPro,
SbPro2,
Sb16,
}
impl SbModel {
pub const fn dsp_version(self) -> (u8, u8) {
match self {
Self::Sb1 => (1, 5),
Self::Sb2 => (2, 1),
Self::SbPro => (3, 0),
Self::SbPro2 => (3, 2),
Self::Sb16 => (4, 5),
}
}
pub const fn opl_chip(self) -> OplChip {
match self {
Self::Sb1 | Self::Sb2 => OplChip::Opl2,
Self::SbPro => OplChip::DualOpl2,
Self::SbPro2 | Self::Sb16 => OplChip::Opl3,
}
}
pub(crate) const fn has_mixer(self) -> bool {
matches!(self, Self::SbPro | Self::SbPro2 | Self::Sb16)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SoundBlasterConfig {
pub base: u16,
pub irq: u8,
pub dma: u8,
}
impl Default for SoundBlasterConfig {
fn default() -> Self {
Self {
base: 0x220,
irq: 5,
dma: 1,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OplChip {
Opl2,
DualOpl2,
Opl3,
}
#[derive(Clone, Default)]
pub enum OplBackend {
#[default]
Oplon,
#[cfg(feature = "nuked")]
Nuked,
#[cfg(feature = "opl")]
Custom(std::sync::Arc<dyn Fn() -> Box<dyn crate::OplCore> + Send + Sync>),
}
impl core::fmt::Debug for OplBackend {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(match self {
Self::Oplon => "Oplon",
#[cfg(feature = "nuked")]
Self::Nuked => "Nuked",
#[cfg(feature = "opl")]
Self::Custom(_) => "Custom",
})
}
}
#[derive(Clone, Debug)]
pub struct SoundConfig {
pub(crate) opl: Option<OplChip>,
pub(crate) sb: Option<SoundBlasterConfig>,
pub(crate) sb_model: SbModel,
pub(crate) dma16: u8,
pub(crate) backend: OplBackend,
pub(crate) speaker: bool,
pub(crate) native: bool,
pub(crate) psg: bool,
pub(crate) midi: Option<crate::MidiBackend>,
pub(crate) midi_port: u16,
pub(crate) sample_rate: u32,
pub(crate) queue_capacity: usize,
pub(crate) latency: Tick,
}
impl Default for SoundConfig {
fn default() -> Self {
Self::adlib()
}
}
impl SoundConfig {
pub fn none() -> Self {
Self {
opl: None,
sb: None,
sb_model: SbModel::Sb2,
dma16: 5,
backend: OplBackend::Oplon,
speaker: false,
native: false,
psg: false,
midi: None,
midi_port: 0x330,
sample_rate: 48_000,
queue_capacity: 16_384,
latency: Tick::from_micros(10_000),
}
}
pub fn adlib() -> Self {
Self {
opl: Some(OplChip::Opl2),
..Self::none()
}
}
pub fn sb2() -> Self {
Self::none().sound_blaster(SoundBlasterConfig::default())
}
pub fn sb1() -> Self {
Self::none().sb_model(SbModel::Sb1)
}
pub fn sb_pro() -> Self {
Self::none().sb_model(SbModel::SbPro)
}
pub fn sb_pro2() -> Self {
Self::none().sb_model(SbModel::SbPro2)
}
pub fn sb16() -> Self {
Self::none().sb_model(SbModel::Sb16)
}
#[must_use]
pub fn sb_model(mut self, model: SbModel) -> Self {
self.sb_model = model;
self.sb.get_or_insert_with(SoundBlasterConfig::default);
self.opl = Some(model.opl_chip());
self
}
#[must_use]
pub fn dma16(mut self, channel: u8) -> Self {
self.dma16 = channel;
self
}
pub fn from_blaster(value: &str) -> Result<Self> {
let mut fields = [None; 5];
for field in value.split_ascii_whitespace() {
if !field.is_ascii() {
return Err(Error::Config("BLASTER fields must be ASCII"));
}
let (key, number) = field.split_at(1);
let index = match key.to_ascii_uppercase().as_str() {
"A" => 0,
"I" => 1,
"D" => 2,
"T" => 3,
"H" => 4,
_ => return Err(Error::Config("unsupported BLASTER field")),
};
if fields[index].is_some() {
return Err(Error::Config("duplicate BLASTER field"));
}
let number = if index == 0 {
number
.strip_prefix("0x")
.or_else(|| number.strip_prefix("0X"))
.unwrap_or(number)
} else {
number
};
fields[index] = Some(
u16::from_str_radix(number, if index == 0 { 16 } else { 10 })
.map_err(|_| Error::Config("invalid BLASTER field value"))?,
);
}
let [Some(base), Some(irq), Some(dma), Some(card), high] = fields else {
return Err(Error::Config("BLASTER requires A, I, D and T fields"));
};
let model = match card {
1 => SbModel::Sb1,
2 => SbModel::SbPro,
3 => SbModel::Sb2,
4 => SbModel::SbPro2,
6 => SbModel::Sb16,
_ => return Err(Error::Config("unsupported BLASTER card type")),
};
if high.is_some() && model != SbModel::Sb16 {
return Err(Error::Config("BLASTER H requires an SB16"));
}
let resources = SoundBlasterConfig {
base,
irq: u8::try_from(irq).map_err(|_| Error::Config("invalid BLASTER IRQ"))?,
dma: u8::try_from(dma).map_err(|_| Error::Config("invalid BLASTER DMA"))?,
};
let config = Self::none().sb_model(model).sound_blaster(resources).dma16(
u8::try_from(high.unwrap_or(5))
.map_err(|_| Error::Config("invalid BLASTER high DMA"))?,
);
config.validate()?;
Ok(config)
}
#[must_use]
pub fn sound_blaster(mut self, resources: SoundBlasterConfig) -> Self {
self.sb = Some(resources);
self.opl = Some(self.sb_model.opl_chip());
self
}
pub fn opl3() -> Self {
Self {
opl: Some(OplChip::Opl3),
..Self::none()
}
}
pub fn dual_opl2() -> Self {
Self {
opl: Some(OplChip::DualOpl2),
..Self::none()
}
}
#[must_use]
pub fn speaker(mut self, enabled: bool) -> Self {
self.speaker = enabled;
self
}
#[cfg(feature = "synthie")]
#[must_use]
pub fn native(mut self, enabled: bool) -> Self {
self.native = enabled;
self
}
#[must_use]
pub fn mpu401(mut self, backend: crate::MidiBackend) -> Self {
self.midi = Some(backend);
self
}
#[cfg(feature = "gm")]
pub fn gm(font: std::sync::Arc<crate::SoundFont>) -> Self {
Self::none().mpu401(crate::MidiBackend::Gm(font))
}
#[cfg(feature = "mt32")]
pub fn mt32(roms: std::sync::Arc<crate::Mt32Roms>) -> Self {
Self::none().mpu401(crate::MidiBackend::Mt32(roms))
}
#[must_use]
pub fn midi_port(mut self, base: u16) -> Self {
self.midi_port = base;
self
}
#[must_use]
pub fn psg(mut self, enabled: bool) -> Self {
self.psg = enabled;
self
}
#[must_use]
pub fn opl_backend(mut self, backend: OplBackend) -> Self {
self.backend = backend;
self
}
#[must_use]
pub fn sample_rate(mut self, sample_rate: u32) -> Self {
self.sample_rate = sample_rate;
self
}
#[must_use]
pub fn queue_capacity(mut self, capacity: usize) -> Self {
self.queue_capacity = capacity;
self
}
#[must_use]
pub fn latency(mut self, latency: Tick) -> Self {
self.latency = latency;
self
}
pub(crate) fn validate(&self) -> Result<()> {
if self.midi.is_some() {
if !cfg!(feature = "mpu401") {
return Err(Error::Config("MPU-401 requires the mpu401 feature"));
}
if !(0x300..=0x360).contains(&self.midi_port) || self.midi_port & 0xf != 0 {
return Err(Error::Config("invalid MPU-401 base"));
}
}
if self.psg && !cfg!(feature = "psg") {
return Err(Error::Config("PSG requires the psg feature"));
}
if let Some(sb) = self.sb {
if !cfg!(feature = "sb") {
return Err(Error::Config("Sound Blaster requires the sb feature"));
}
if !(0x200..=0x280).contains(&sb.base) || sb.base & 0x0f != 0 {
return Err(Error::Config("invalid Sound Blaster base address"));
}
if !matches!(sb.irq, 2 | 3 | 5 | 7 | 10) || !matches!(sb.dma, 0 | 1 | 3) {
return Err(Error::Config("invalid Sound Blaster IRQ or DMA channel"));
}
if sb.irq == 10 && self.sb_model != SbModel::Sb16 {
return Err(Error::Config("IRQ 10 requires an SB16"));
}
if !(5..=7).contains(&self.dma16)
&& !(self.sb_model == SbModel::Sb16 && self.dma16 == sb.dma)
{
return Err(Error::Config("word DMA channel must be 5, 6 or 7"));
}
}
if !(8_000..=192_000).contains(&self.sample_rate) {
return Err(Error::Config("sample rate must be 8,000..=192,000 Hz"));
}
if self.queue_capacity == 0 || self.queue_capacity > 1_048_576 {
return Err(Error::Config("queue capacity must be 1..=1,048,576"));
}
if self.opl.is_some() && !cfg!(feature = "opl") {
return Err(Error::Config("OPL hardware requires the opl feature"));
}
Ok(())
}
}