use game_toolkit_dosaudio::{AudioEngine, DosAudio, PreparedMusic, SoundConfig, Tick};
pub use game_toolkit_dosaudio::{Melody, PlayOptions, SpeakerHandle};
use crate::machine::Dos;
pub const SPEAKER_RATE: u32 = 44_100;
const BACKLOG_MAX: usize = SPEAKER_RATE as usize;
pub(crate) struct PcSpeaker {
ports: DosAudio,
engine: AudioEngine,
backlog: Vec<f32>,
}
impl PcSpeaker {
pub fn new() -> Self {
let config = SoundConfig::none()
.speaker(true)
.sample_rate(SPEAKER_RATE)
.latency(Tick::ZERO);
let (ports, engine) = DosAudio::split(config).expect("PC speaker configuration is valid");
Self {
ports,
engine,
backlog: Vec::new(),
}
}
pub fn outb(&mut self, at: Tick, port: u16, v: u8) {
if self.ports.try_outb_at(at, port, v).is_err() {
self.make_room(at);
self.ports
.try_outb_at(at, port, v)
.expect("speaker command queue has room after rendering");
}
}
pub fn inb(&mut self, at: Tick, port: u16) -> u8 {
self.ports
.try_inb_at(at, port)
.expect("machine time runs forward")
}
fn render(&mut self, at: Tick, out: &mut Vec<f32>) {
let target =
(u128::from(at.0) * u128::from(SPEAKER_RATE) * u128::from(Tick::HZ_DENOMINATOR)
/ u128::from(Tick::HZ_NUMERATOR)) as u64;
let mut frames = [[0.0f32; 2]; 512];
let mut left = target.saturating_sub(self.engine.stats().frames);
while left > 0 {
let n = left.min(frames.len() as u64) as usize;
self.engine.render(&mut frames[..n]);
out.extend(frames[..n].iter().map(|f| f[0]));
left -= n as u64;
}
}
fn make_room(&mut self, at: Tick) {
let mut backlog = std::mem::take(&mut self.backlog);
self.render(at, &mut backlog);
let excess = backlog.len().saturating_sub(BACKLOG_MAX);
backlog.drain(..excess);
self.backlog = backlog;
}
pub fn take(&mut self, at: Tick, out: &mut Vec<f32>) {
out.append(&mut self.backlog);
self.render(at, out);
}
pub fn skip(&mut self, at: Tick) {
self.make_room(at);
self.backlog.clear();
}
pub fn play(
&mut self,
at: Tick,
music: PreparedMusic,
options: PlayOptions,
) -> game_toolkit_dosaudio::Result<SpeakerHandle> {
self.engine.play_speaker_at(music, options, at)
}
pub fn pit(&self) -> &game_toolkit_dosbus::Pit {
self.ports.pit().expect("the speaker installs the PIT")
}
pub fn tone(&self) -> Option<f64> {
self.ports.speaker_tone()
}
}
impl Dos {
pub fn sound(&mut self, hz: f64) {
let d = (crate::PIT_HZ / hz.max(19.0)).round().clamp(1.0, 65535.0) as u16;
self.outb(0x43, 0xB6);
self.outb(0x42, d as u8);
self.outb(0x42, (d >> 8) as u8);
let v = self.inb(0x61);
self.outb(0x61, v | 0x03);
}
pub fn nosound(&mut self) {
let v = self.inb(0x61);
self.outb(0x61, v & !0x03);
}
pub fn speaker_pwm(&mut self, samples: &[u8], rate: u32) {
let period = 1e9 / rate.max(1) as f64;
let base = self.inb(0x61) & !0x03;
let start = self.now_ns();
for (i, &s) in samples.iter().enumerate() {
let t0 = start + i as f64 * period;
self.idle_until(t0);
let high = period * s as f64 / 255.0;
if high > 0.0 {
self.outb(0x61, base | 0x02);
self.idle_until(t0 + high);
}
self.outb(0x61, base);
}
self.idle_until(start + samples.len() as f64 * period);
}
pub fn play_speaker(
&mut self,
music: impl Into<PreparedMusic>,
options: PlayOptions,
) -> game_toolkit_dosaudio::Result<SpeakerHandle> {
let at = self.now_tick();
self.speaker.play(at, music.into(), options)
}
pub fn speaker_render(&mut self, out: &mut Vec<f32>) {
let at = self.now_tick();
self.speaker.take(at, out);
}
pub fn speaker_skip(&mut self) {
let at = self.now_tick();
self.speaker.skip(at);
}
pub fn speaker_tone(&self) -> Option<f64> {
self.speaker.tone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::machine::{BusTiming, MachineConfig};
fn dos() -> Dos {
Dos::new(MachineConfig::default().with_bus(BusTiming::INSTANT))
}
fn crossings_hz(s: &[f32], rate: u32) -> f64 {
let n = s.windows(2).filter(|w| w[0] < 0.0 && w[1] >= 0.0).count();
n as f64 * rate as f64 / s.len() as f64
}
#[test]
fn a_tone_comes_out_at_its_pitch() {
let mut d = dos();
d.sound(440.0);
d.delay_us(500_000.0);
let mut out = Vec::new();
d.speaker_render(&mut out);
assert!((out.len() as i64 - 22_050).abs() < 5);
let tail = &out[4410..];
assert!((crossings_hz(tail, SPEAKER_RATE) - 440.0).abs() < 5.0);
assert!(tail.iter().any(|&v| v > 0.1));
d.nosound();
d.delay_us(500_000.0);
let mut quiet = Vec::new();
d.speaker_render(&mut quiet);
assert!(
quiet[8000..].iter().all(|v| v.abs() < 0.01),
"silence after nosound"
);
}
#[test]
fn port61_reflects_gate_and_refresh() {
let mut d = dos();
let a = d.inb(0x61) & 0x10;
d.delay_us(15.085);
let b = d.inb(0x61) & 0x10;
assert_ne!(a, b, "refresh bit toggles every ~15 us");
d.sound(1000.0);
assert_eq!(d.inb(0x61) & 0x03, 0x03);
assert!(d.speaker_tone().is_some_and(|f| (f - 1000.0).abs() < 1.0));
d.nosound();
assert!(d.speaker_tone().is_none());
}
#[test]
fn pwm_plays_amplitudes() {
let mut d = dos();
let s: Vec<u8> = (0..8000)
.map(|i| {
(128.0 + 100.0 * (i as f64 * 500.0 / 16_000.0 * std::f64::consts::TAU).sin()) as u8
})
.collect();
let t0 = d.now();
d.speaker_pwm(&s, 16_000);
assert!((d.now() - t0 - 0.5).abs() < 0.01);
let mut out = Vec::new();
d.speaker_render(&mut out);
let smooth: Vec<f32> = out
.windows(8)
.map(|w| w.iter().sum::<f32>() / 8.0)
.collect();
let tail = &smooth[4410..smooth.len() - 100];
let hz = crossings_hz(tail, SPEAKER_RATE);
assert!((hz - 500.0).abs() < 15.0, "{hz}");
}
#[test]
fn long_bit_banging_overflows_into_the_backlog() {
let mut d = dos();
d.speaker_pwm(&vec![128; 48_000], 16_000);
let mut out = Vec::new();
d.speaker_render(&mut out);
assert!(out.len() >= BACKLOG_MAX, "{}", out.len());
}
#[test]
fn melody_plays_in_the_background() {
let mut d = dos();
let tune = d
.play_speaker(
Melody::parse("T120 L4 ML O3 A").unwrap(),
PlayOptions::default(),
)
.unwrap();
d.delay_us(400_000.0);
let mut out = Vec::new();
d.speaker_render(&mut out);
let hz = crossings_hz(&out[4410..], SPEAKER_RATE);
assert!((hz - 440.0).abs() < 5.0, "{hz}");
assert!(d.speaker_tone().is_none(), "music does not touch the ports");
assert!(tune.tone().is_some_and(|f| (f - 440.0).abs() < 1.0));
d.delay_us(200_000.0);
d.speaker_render(&mut out);
assert!(tune.is_finished());
}
}