Skip to main content

game_toolkit_dosaudio/
bus.rs

1#[cfg(feature = "mpu401")]
2use crate::device::mpu401::MpuControl;
3#[cfg(feature = "opl")]
4use crate::device::opl::OplControl;
5#[cfg(feature = "sb")]
6use crate::device::sb::{DacState, SbControl};
7use crate::device::speaker::{SpeakerControl, SpeakerState};
8#[cfg(feature = "sb")]
9use crate::dma::playback::{MemoryPlan, MemoryPlayback};
10#[cfg(feature = "sb")]
11use crate::sfx::{PcmPlan, PcmPlayback};
12use crate::{
13    AudioEngine, Capture, Error, IoDevice, PortRange, PortWrite, Result, SoundConfig, Tick,
14};
15use rtrb::Producer;
16
17#[derive(Clone, Copy, Debug)]
18pub(crate) enum Command {
19    Speaker(SpeakerState),
20    #[cfg(feature = "mpu401")]
21    MidiByte(u8),
22    #[cfg(feature = "psg")]
23    Psg(u8),
24    #[cfg(feature = "sb")]
25    Dac(DacState),
26    #[cfg(feature = "opl")]
27    Opl {
28        register: u16,
29        value: u8,
30    },
31    #[cfg(feature = "opl")]
32    OplPair {
33        first: (u16, u8),
34        second: (u16, u8),
35    },
36}
37
38#[derive(Clone, Copy, Debug)]
39pub(crate) struct TimedCommand {
40    pub at: Tick,
41    pub command: Command,
42}
43
44#[cfg(feature = "sb")]
45#[derive(Clone, Copy)]
46enum AutomaticPlan {
47    Buffer(PcmPlan),
48    Memory(MemoryPlan),
49}
50#[cfg(feature = "sb")]
51impl AutomaticPlan {
52    fn sb(self) -> SbControl {
53        match self {
54            Self::Buffer(p) => p.sb,
55            Self::Memory(p) => p.sb,
56        }
57    }
58    fn commands(self) -> usize {
59        match self {
60            Self::Buffer(p) => p.commands,
61            Self::Memory(p) => p.commands,
62        }
63    }
64}
65
66/// Game-thread hardware control, including port reads and device timers.
67///
68/// There is one owner of port I/O. Implementing [`IoDevice`] allows moving this
69/// object directly into a host bus. `outb`/`inb` do not charge time themselves;
70/// use `advance`, `wait_us`, or timestamps supplied by the host.
71///
72/// Fallible writes leave state unchanged when the queue is full. The infallible
73/// helpers and [`IoDevice::outb`] panic on overflow rather than lose commands.
74pub struct DosAudio {
75    now: Tick,
76    ports: Vec<PortRange>,
77    speaker: Option<SpeakerControl>,
78    #[cfg(feature = "mpu401")]
79    mpu: Option<MpuControl>,
80    #[cfg(feature = "psg")]
81    psg: bool,
82    #[cfg(feature = "opl")]
83    opl: Option<OplControl>,
84    #[cfg(feature = "sb")]
85    sb: Option<SbControl>,
86    #[cfg(feature = "sb")]
87    pcm: Option<PcmPlayback>,
88    #[cfg(feature = "sb")]
89    memory_dma: Option<MemoryPlayback>,
90    #[cfg(feature = "sb")]
91    queue_capacity: usize,
92    commands: Producer<TimedCommand>,
93    capture: Option<Capture>,
94    latency: Tick,
95}
96
97impl DosAudio {
98    /// Allocate the bounded command queue and construct both thread halves.
99    pub fn split(config: SoundConfig) -> Result<(Self, AudioEngine)> {
100        config.validate()?;
101        let (producer, consumer) = rtrb::RingBuffer::new(config.queue_capacity);
102        let mut ports = Vec::new();
103        if config.midi.is_some() {
104            ports.push(PortRange::new(config.midi_port, config.midi_port + 1));
105        }
106        if config.psg {
107            ports.push(PortRange::single(0xc0));
108        }
109        if config.speaker {
110            ports.extend([PortRange::new(0x40, 0x43), PortRange::single(0x61)]);
111        }
112        if let Some(chip) = config.opl {
113            ports.push(PortRange::new(
114                0x388,
115                if chip == crate::OplChip::Opl2 {
116                    0x389
117                } else {
118                    0x38b
119                },
120            ));
121        }
122        if let Some(sb) = config.sb {
123            if config.sb_model.has_mixer() {
124                ports.push(PortRange::new(sb.base, sb.base + 5));
125            }
126            ports.extend([
127                PortRange::single(sb.base + 6),
128                PortRange::new(sb.base + 8, sb.base + 0x0a),
129                PortRange::single(sb.base + 0x0c),
130                PortRange::single(sb.base + 0x0e),
131            ]);
132            if config.sb_model == crate::SbModel::Sb16 {
133                ports.push(PortRange::single(sb.base + 0x0f));
134            }
135        }
136        let control = Self {
137            now: Tick::ZERO,
138            ports,
139            speaker: config.speaker.then(SpeakerControl::default),
140            #[cfg(feature = "mpu401")]
141            mpu: config
142                .midi
143                .as_ref()
144                .map(|_| MpuControl::new(config.midi_port)),
145            #[cfg(feature = "psg")]
146            psg: config.psg,
147            #[cfg(feature = "opl")]
148            opl: config.opl.map(OplControl::new),
149            #[cfg(feature = "sb")]
150            sb: config
151                .sb
152                .map(|resources| SbControl::new(resources, config.sb_model, config.dma16)),
153            #[cfg(feature = "sb")]
154            pcm: None,
155            #[cfg(feature = "sb")]
156            memory_dma: None,
157            #[cfg(feature = "sb")]
158            queue_capacity: config.queue_capacity,
159            commands: producer,
160            capture: None,
161            latency: config.latency,
162        };
163        Ok((control, AudioEngine::new(&config, consumer)?))
164    }
165
166    /// Current machine time, initially zero.
167    pub fn now(&self) -> Tick {
168        self.now
169    }
170
171    /// Frequency in hertz of the PIT tone that speaker port I/O currently plays.
172    ///
173    /// `None` when the speaker is absent or port 61h has the PIT gate (bit 0) or
174    /// speaker data (bit 1) clear, which includes bit-banged output. Melodies from
175    /// [`AudioEngine::play_speaker`] run on the engine and are not reflected.
176    pub fn speaker_tone(&self) -> Option<f64> {
177        let state = self.speaker.as_ref()?.state;
178        (state.gate && state.data && state.mode != 0)
179            .then(|| crate::device::speaker::pit_hz(state.divisor))
180    }
181
182    /// The machine's 8253 timer, present with the PC speaker.
183    ///
184    /// With the speaker installed this object owns ports 40h-43h: route them only
185    /// here and read IRQ 0 timing from this timer instead of keeping a second one.
186    /// `None` without a speaker; the host then owns its own
187    /// [`Pit`](game_toolkit_dosbus::Pit).
188    pub fn pit(&self) -> Option<&game_toolkit_dosbus::Pit> {
189        self.speaker.as_ref().map(|speaker| &speaker.pit)
190    }
191
192    /// Advance machine time without consuming wall time.
193    pub fn advance(&mut self, duration: Tick) {
194        self.advance_to(self.now + duration);
195    }
196
197    /// Advance to an absolute timestamp, servicing an attached PCM buffer.
198    /// Panics on backwards time or command queue overflow.
199    pub fn advance_to(&mut self, at: Tick) {
200        self.try_advance_to(at).expect("sound time advance failed");
201    }
202
203    /// Advance and enqueue every due buffer DMA sample, or change nothing.
204    ///
205    /// A full queue requires rendering queued audio, or advancing in smaller
206    /// intervals. Infallible `advance`/`advance_to` and bus calls panic instead.
207    pub fn try_advance_to(&mut self, at: Tick) -> Result<()> {
208        self.check_time(at)?;
209        #[cfg(feature = "sb")]
210        if let Some(plan) = self.plan_pcm(at)? {
211            self.reserve(plan.commands())?;
212            self.commit_pcm(at, plan);
213        }
214        self.set_time(at);
215        Ok(())
216    }
217
218    fn check_time(&self, at: Tick) -> Result<()> {
219        if at < self.now {
220            return Err(Error::BackwardsTime);
221        }
222        at.checked_add(self.latency).ok_or(Error::TooLarge)?;
223        Ok(())
224    }
225
226    fn reserve(&mut self, count: usize) -> Result<()> {
227        if self.commands.slots() < count {
228            return Err(Error::QueueFull);
229        }
230        Ok(())
231    }
232
233    fn set_time(&mut self, at: Tick) {
234        #[cfg(feature = "opl")]
235        if let Some(opl) = &mut self.opl {
236            opl.sync(at);
237        }
238        self.now = at;
239    }
240
241    /// Busy-wait for an integer number of emulated microseconds.
242    pub fn wait_us(&mut self, micros: u64) {
243        self.advance(Tick::from_micros(micros));
244    }
245
246    /// Write at current machine time. Panics on command queue overflow.
247    pub fn outb(&mut self, port: u16, value: u8) {
248        self.try_outb_at(self.now, port, value)
249            .expect("sound port write failed");
250    }
251
252    /// Read at current machine time. Unattached ports read FFh.
253    pub fn inb(&mut self, port: u16) -> u8 {
254        self.try_inb_at(self.now, port)
255            .expect("sound port read failed")
256    }
257
258    /// Timestamped port write, with transactional queue overflow handling.
259    pub fn try_outb_at(&mut self, at: Tick, port: u16, value: u8) -> Result<()> {
260        self.check_time(at)?;
261        #[cfg(feature = "sb")]
262        let plan = self.plan_pcm(at)?;
263        let mut command = None;
264        #[cfg(feature = "mpu401")]
265        let mut next_mpu = self.mpu;
266        #[cfg(feature = "mpu401")]
267        if let Some(mpu) = &mut next_mpu
268            && (mpu.base..=mpu.base + 1).contains(&port)
269            && let Some(byte) = mpu.write(port, value)
270        {
271            command = Some(Command::MidiByte(byte));
272        }
273        #[cfg(feature = "psg")]
274        if self.psg && port == 0xc0 {
275            command = Some(Command::Psg(value));
276        }
277        // Pit is plain data without heap storage; cloning keeps the write transactional.
278        let mut next_speaker = self.speaker.clone();
279        if matches!(port, 0x40..=0x43 | 0x61)
280            && let Some(speaker) = &mut next_speaker
281            && let Some(state) = speaker.write(at, port, value)
282        {
283            command = Some(Command::Speaker(state));
284        }
285        #[cfg(feature = "opl")]
286        let mut next_opl = self.opl;
287        #[cfg(feature = "opl")]
288        if let Some(opl) = &mut next_opl {
289            let writes = self
290                .opl_ports(port)
291                .map(|port| port.and_then(|port| opl.write(at, port, value)));
292            match writes {
293                [Some(first), Some(second)] => command = Some(Command::OplPair { first, second }),
294                [Some((register, value)), None] | [None, Some((register, value))] => {
295                    command = Some(Command::Opl { register, value })
296                }
297                _ => {}
298            }
299        }
300        #[cfg(feature = "sb")]
301        let mut next_sb = plan.map_or(self.sb, |plan| Some(plan.sb()));
302        #[cfg(feature = "sb")]
303        let mut cancel_pcm = false;
304        #[cfg(feature = "sb")]
305        if let Some(sb) = &mut next_sb
306            && (sb.resources.base..=sb.resources.base + 0x0f).contains(&port)
307            && let Some(state) = sb.write(at, port, value)
308        {
309            command = Some(Command::Dac(state));
310        }
311        #[cfg(feature = "sb")]
312        if self.pcm.is_some()
313            && let Some(sb) = &mut next_sb
314            && sb.dma_channel().is_none()
315        {
316            command = Some(Command::Dac(sb.stop_pcm()));
317            cancel_pcm = true;
318        }
319        let needed = usize::from(command.is_some());
320        #[cfg(feature = "sb")]
321        let needed = needed + plan.map_or(0, AutomaticPlan::commands);
322        self.reserve(needed)?;
323        #[cfg(feature = "sb")]
324        if let Some(plan) = plan {
325            self.commit_pcm(at, plan);
326        }
327        #[cfg(feature = "sb")]
328        if matches!(plan, Some(AutomaticPlan::Memory(_))) {
329            // Read shared memory only during the committed walk. Recreate the
330            // port result from its actual final DAC state, not the preview's bytes.
331            next_sb = self.sb;
332            if let Some(sb) = &mut next_sb
333                && (sb.resources.base..=sb.resources.base + 0x0f).contains(&port)
334                && let Some(state) = sb.write(at, port, value)
335            {
336                command = Some(Command::Dac(state));
337            }
338        }
339        if let Some(command) = command {
340            self.push(at, command).expect("reserved port command");
341        }
342        self.speaker = next_speaker;
343        #[cfg(feature = "mpu401")]
344        {
345            self.mpu = next_mpu;
346        }
347        #[cfg(feature = "opl")]
348        {
349            self.opl = next_opl;
350        }
351        #[cfg(feature = "sb")]
352        {
353            self.sb = next_sb;
354            if let Some(memory) = &mut self.memory_dma {
355                memory.controller.outb(port, value);
356            }
357            if let Some(sb) = &self.sb
358                && (cancel_pcm || matches!(port.checked_sub(sb.resources.base), Some(6 | 0x0c)))
359            {
360                self.pcm = None; // Explicit DSP programming takes ownership.
361            }
362        }
363        self.set_time(at);
364        if let Some(capture) = &mut self.capture {
365            capture.writes.push(PortWrite { at, port, value });
366        }
367        Ok(())
368    }
369
370    /// Play one 1..=65,536-byte unsigned mono PCM buffer through the installed DSP.
371    ///
372    /// Starting allocates and replaces the previous DSP transfer. Requested rates
373    /// are 4,000..=23,000 Hz, rounded to the nearest integer-microsecond period;
374    /// the returned handle reports the actual rate. Retain it while playing.
375    /// Advance the control clock to supply bytes automatically. Buffer data and
376    /// handles remain on the control side; the audio thread receives only DAC values.
377    ///
378    /// Raw writes to the DSP reset/command ports cancel buffer ownership. FM and
379    /// speaker I/O continue alongside PCM. Failed starts leave existing playback intact.
380    #[cfg(feature = "sb")]
381    pub fn play_pcm(
382        &mut self,
383        samples: impl Into<std::sync::Arc<[u8]>>,
384        sample_rate: u32,
385    ) -> Result<crate::SfxHandle> {
386        if !(4_000..=23_000).contains(&sample_rate) {
387            return Err(Error::Config("PCM sample rate must be 4,000..=23,000 Hz"));
388        }
389        let mut sb = self
390            .sb
391            .ok_or(Error::Config("Sound Blaster is not installed"))?;
392        let samples = samples.into();
393        if samples.is_empty() || samples.len() > 65_536 {
394            return Err(Error::Config("PCM buffer must contain 1..=65,536 bytes"));
395        }
396        let period = ((1_000_000 + sample_rate / 2) / sample_rate).clamp(44, 250) as u16;
397        let (state, end) = sb.begin_pcm(self.now, samples.len() as u32, (256 - period) as u8)?;
398        end.checked_add(self.latency).ok_or(Error::TooLarge)?;
399        self.reserve(1)?;
400        let handle = crate::SfxHandle::new(period);
401        self.push(self.now, Command::Dac(state))
402            .expect("reserved PCM start");
403        self.pcm = Some(PcmPlayback::new(samples, &handle, end));
404        self.sb = Some(sb);
405        Ok(handle)
406    }
407
408    #[cfg(feature = "sb")]
409    fn plan_pcm(&self, at: Tick) -> Result<Option<AutomaticPlan>> {
410        if let Some(pcm) = &self.pcm {
411            return Ok(Some(AutomaticPlan::Buffer(pcm.plan(
412                self.sb.expect("buffer owns DSP"),
413                self.now,
414                at,
415            ))));
416        }
417        if let (Some(memory), Some(sb)) = (&self.memory_dma, self.sb) {
418            return memory
419                .plan(sb, self.now, at)
420                .map(|p| Some(AutomaticPlan::Memory(p)));
421        }
422        Ok(None)
423    }
424
425    #[cfg(feature = "sb")]
426    fn commit_pcm(&mut self, at: Tick, plan: AutomaticPlan) {
427        let commands = &mut self.commands;
428        let mut emit = |at, state| {
429            commands
430                .push(TimedCommand {
431                    at,
432                    command: Command::Dac(state),
433                })
434                .expect("reserved DMA command");
435        };
436        if let AutomaticPlan::Memory(_) = plan {
437            self.memory_dma
438                .as_mut()
439                .expect("prepared memory DMA")
440                .commit(self.sb.as_mut().expect("installed DSP"), self.now, at, emit)
441                .expect("preflighted memory DMA");
442            return;
443        }
444        let AutomaticPlan::Buffer(plan) = plan else {
445            unreachable!()
446        };
447        let finished = self.pcm.as_mut().expect("prepared buffer").commit(
448            plan,
449            self.sb.expect("buffer owns DSP"),
450            self.now,
451            at,
452            &mut emit,
453        );
454        self.sb = Some(plan.sb);
455        if finished {
456            self.pcm = None;
457        }
458    }
459
460    #[cfg(feature = "sb")]
461    pub(crate) fn next_pcm_tick(&self) -> Option<Tick> {
462        if let Some(pcm) = &self.pcm {
463            return Some(pcm.next(self.sb.as_ref().expect("buffer owns DSP"), self.now));
464        }
465        self.memory_dma
466            .as_ref()
467            .and_then(|memory| self.sb.as_ref().and_then(|sb| memory.next(sb, self.now)))
468    }
469
470    /// Attach shared conventional memory and an internal 8237 controller.
471    /// Call before attaching this object to a host bus: this expands its port map.
472    /// Memory bytes are atomic; coordinate writes with advances for coherent samples.
473    #[cfg(feature = "sb")]
474    pub fn attach_dma(&mut self, memory: std::sync::Arc<crate::ConventionalMemory>) -> Result<()> {
475        #[cfg(feature = "psg")]
476        if self.psg {
477            return Err(Error::Config(
478                "Tandy PSG overlaps the internal AT DMA controller",
479            ));
480        }
481        if self.memory_dma.is_some() {
482            return Err(Error::Config("DMA memory is already attached"));
483        }
484        if self.sb.is_none() {
485            return Err(Error::Config("Sound Blaster is not installed"));
486        }
487        let dma = MemoryPlayback::new(memory, self.queue_capacity);
488        self.ports.extend_from_slice(dma.controller.ports());
489        self.memory_dma = Some(dma);
490        Ok(())
491    }
492
493    /// Pop one memory-DMA block completion. This does not acknowledge the ISA IRQ.
494    /// Drain before retrying IrqQueueFull; refill memory before advancing past a block.
495    #[cfg(feature = "sb")]
496    pub fn poll_dma_completion(&mut self) -> Option<crate::DmaCompletion> {
497        self.memory_dma.as_mut().and_then(MemoryPlayback::poll)
498    }
499
500    /// Next predicted DSP block boundary; masks/controller terminal count may stall it.
501    #[cfg(feature = "sb")]
502    pub fn next_dma_block_tick(&self) -> Option<Tick> {
503        self.sb.as_ref().and_then(SbControl::next_block_tick)
504    }
505
506    #[cfg(feature = "sb")]
507    pub(crate) fn pcm_end(&self) -> Option<Tick> {
508        self.pcm.as_ref().map(|pcm| pcm.end(self.now))
509    }
510
511    fn push(&mut self, at: Tick, command: Command) -> Result<()> {
512        self.commands
513            .push(TimedCommand { at, command })
514            .map_err(|_| Error::QueueFull)
515    }
516
517    #[cfg(feature = "opl")]
518    fn opl_ports(&self, port: u16) -> [Option<u16>; 2] {
519        #[cfg(feature = "sb")]
520        let card = self.sb.as_ref().map(|sb| (sb.resources.base, sb.model));
521        #[cfg(not(feature = "sb"))]
522        let card = None;
523        map_opl_ports(port, card)
524    }
525
526    /// Earliest external DSP byte request, or `None` when stopped, paused, or buffer-owned.
527    ///
528    /// Service each byte at this tick to preserve the DSP's fractional clock
529    /// phase. The timestamp can precede `now()` if the host has fallen behind.
530    /// In that case service at `now()`; subsequent requests resume from there.
531    #[cfg(feature = "sb")]
532    pub fn next_dma_tick(&self) -> Option<Tick> {
533        if self.pcm.is_some() {
534            return None;
535        }
536        self.sb.as_ref().and_then(SbControl::next_dma_tick)
537    }
538
539    /// Supply one unsigned PCM byte from the host's DMA controller.
540    ///
541    /// The host owns masking, memory, page wrapping and terminal count. Call at
542    /// `next_dma_tick()` after checking DRQ. Early, paused, unmatched and backwards
543    /// transfers fail. Queue overflow leaves time, DSP state and IRQ unchanged,
544    /// so the host should advance its DMA address only after this succeeds.
545    /// Samples receive the same fixed output latency as port writes.
546    #[cfg(feature = "sb")]
547    pub fn try_dma_write_at(
548        &mut self,
549        at: Tick,
550        channel: crate::DmaChannel,
551        value: u8,
552    ) -> Result<()> {
553        self.check_time(at)?;
554        if self.pcm.is_some() || self.memory_dma.is_some() {
555            return Err(Error::Config("PCM buffer owns Sound Blaster DMA"));
556        }
557        let mut sb = self
558            .sb
559            .ok_or(Error::Config("Sound Blaster is not installed"))?;
560        let state = sb.dma_write(at, channel, value)?;
561        self.push(at, Command::Dac(state))?;
562        self.sb = Some(sb);
563        self.set_time(at);
564        Ok(())
565    }
566
567    /// Supply one SB16 word-wide DMA unit; bit interpretation follows the DSP mode.
568    /// Queue overflow leaves the DSP, IRQ, and machine timestamp unchanged.
569    #[cfg(feature = "sb")]
570    pub fn try_dma_write16_at(
571        &mut self,
572        at: Tick,
573        channel: crate::DmaChannel,
574        value: u16,
575    ) -> Result<()> {
576        self.check_time(at)?;
577        if self.pcm.is_some() || self.memory_dma.is_some() {
578            return Err(Error::Config("PCM buffer owns Sound Blaster DMA"));
579        }
580        let mut sb = self
581            .sb
582            .ok_or(Error::Config("Sound Blaster is not installed"))?;
583        let state = sb.dma_write16(at, channel, value)?;
584        self.push(at, Command::Dac(state))?;
585        self.sb = Some(sb);
586        self.set_time(at);
587        Ok(())
588    }
589
590    /// Timestamped port read. Rejects backwards time without changing state.
591    pub fn try_inb_at(&mut self, at: Tick, port: u16) -> Result<u8> {
592        self.try_advance_to(at)?;
593        #[cfg(feature = "mpu401")]
594        if let Some(mpu) = &mut self.mpu
595            && (mpu.base..=mpu.base + 1).contains(&port)
596        {
597            return Ok(mpu.read(port));
598        }
599        #[cfg(feature = "sb")]
600        if let Some(memory) = &mut self.memory_dma
601            && memory
602                .controller
603                .ports()
604                .iter()
605                .any(|range| range.contains(port))
606        {
607            return Ok(memory.controller.inb(port));
608        }
609        if matches!(port, 0x40..=0x42 | 0x61)
610            && let Some(speaker) = &mut self.speaker
611        {
612            return Ok(speaker.read(at, port));
613        }
614        #[cfg(feature = "opl")]
615        if let Some(opl_port) = self.opl_ports(port)[0]
616            && let Some(opl) = &mut self.opl
617        {
618            return Ok(opl.read(at, opl_port));
619        }
620        #[cfg(feature = "sb")]
621        if let Some(sb) = &mut self.sb
622            && (sb.resources.base..=sb.resources.base + 0x0f).contains(&port)
623        {
624            return Ok(sb.read(port));
625        }
626        Ok(0xff)
627    }
628
629    /// Play a square-wave PC speaker tone through the emulated ports.
630    ///
631    /// Frequency must be finite and positive. The divisor is clamped to the PIT
632    /// range, including zero as the encoding for 65,536.
633    pub fn sound(&mut self, hz: f64) -> Result<()> {
634        if !hz.is_finite() || hz <= 0.0 {
635            return Err(Error::Config("invalid speaker frequency"));
636        }
637        if self.speaker.is_none() {
638            return Err(Error::Config("PC speaker is not installed"));
639        }
640        // Reserve the complete sequence so overflow cannot leave half a divisor.
641        let needed = 3;
642        // A concurrent last-handle drop can add one stop command during this sequence.
643        #[cfg(feature = "sb")]
644        let needed = needed + usize::from(self.pcm.is_some());
645        self.reserve(needed)?;
646        let divisor = (Tick::HZ_NUMERATOR as f64 / (Tick::HZ_DENOMINATOR as f64 * 12.0 * hz))
647            .round()
648            .clamp(1.0, 65_536.0) as u32 as u16;
649        self.try_outb_at(self.now, 0x43, 0xb6)?;
650        self.try_outb_at(self.now, 0x42, divisor as u8)?;
651        self.try_outb_at(self.now, 0x42, (divisor >> 8) as u8)?;
652        let bits = self.inb(0x61);
653        self.try_outb_at(self.now, 0x61, bits | 3)
654    }
655
656    /// Disconnect the PC speaker through port 61h.
657    pub fn nosound(&mut self) -> Result<()> {
658        let bits = self.inb(0x61);
659        self.try_outb_at(self.now, 0x61, bits & !3)
660    }
661
662    /// Consume this control half as the object attached to a host's bus.
663    pub fn bus_device(self) -> impl IoDevice {
664        self
665    }
666    /// Borrow tone/noise helpers for the installed Tandy SN76489.
667    #[cfg(feature = "psg")]
668    pub fn psg(&mut self) -> Result<crate::Psg<'_>> {
669        if !self.psg {
670            return Err(Error::Config("PSG is not installed"));
671        }
672        Ok(crate::Psg::new(self))
673    }
674    #[cfg(feature = "psg")]
675    pub(crate) fn psg_write(&mut self, bytes: &[u8]) -> Result<()> {
676        if !self.psg {
677            return Err(Error::Config("PSG is not installed"));
678        }
679        let needed = bytes.len();
680        #[cfg(feature = "sb")]
681        let needed = needed
682            + self.plan_pcm(self.now)?.map_or(0, AutomaticPlan::commands)
683            + usize::from(self.pcm.is_some());
684        self.reserve(needed)?;
685        for &byte in bytes {
686            self.try_outb_at(self.now, 0xc0, byte)?;
687        }
688        Ok(())
689    }
690
691    /// Borrow musical patch, note, and rhythm helpers for the installed OPL chips.
692    #[cfg(feature = "opl")]
693    pub fn opl(&mut self) -> Result<crate::Opl<'_>> {
694        let chip = self
695            .opl
696            .as_ref()
697            .ok_or(Error::Config("OPL hardware is not installed"))?
698            .chip;
699        Ok(crate::Opl::new(self, chip))
700    }
701
702    #[cfg(feature = "opl")]
703    pub(crate) fn opl_register(&self, register: u16) -> Result<u8> {
704        let opl = self
705            .opl
706            .as_ref()
707            .ok_or(Error::Config("OPL hardware is not installed"))?;
708        if register > 0x1ff || opl.chip == crate::OplChip::Opl2 && register > 0xff {
709            return Err(Error::Config("OPL register is not installed"));
710        }
711        Ok(opl.register(register))
712    }
713
714    #[cfg(feature = "opl")]
715    pub(crate) fn opl_patch_levels(&self, channel: u8) -> Result<[u8; 2]> {
716        let opl = self
717            .opl
718            .as_ref()
719            .ok_or(Error::Config("OPL hardware is not installed"))?;
720        if channel
721            >= if opl.chip == crate::OplChip::Opl2 {
722                9
723            } else {
724                18
725            }
726        {
727            return Err(Error::Config("invalid OPL channel"));
728        }
729        Ok(opl.patch_levels(usize::from(channel)))
730    }
731
732    #[cfg(feature = "opl")]
733    pub(crate) fn opl_set_patch_levels(&mut self, channel: u8, levels: [u8; 2]) -> Result<()> {
734        self.opl_patch_levels(channel)?;
735        self.opl
736            .as_mut()
737            .expect("validated OPL")
738            .set_patch_levels(usize::from(channel), levels);
739        Ok(())
740    }
741
742    #[cfg(feature = "opl")]
743    pub(crate) fn opl_write(&mut self, writes: &[(u16, u8)]) -> Result<()> {
744        for &(register, _) in writes {
745            self.opl_register(register)?;
746        }
747        let needed = writes.len();
748        #[cfg(feature = "sb")]
749        let needed = needed
750            + self.plan_pcm(self.now)?.map_or(0, AutomaticPlan::commands)
751            + usize::from(self.pcm.is_some());
752        self.reserve(needed)?;
753        for &(register, value) in writes {
754            let port = 0x388 + (register >> 8) * 2;
755            #[cfg(feature = "sb")]
756            let port = if let Some(sb) = &self.sb
757                && sb.model == crate::SbModel::SbPro
758            {
759                sb.resources.base + (register >> 8) * 2
760            } else {
761                port
762            };
763            self.try_outb_at(self.now, port, register as u8)?;
764            self.try_outb_at(self.now, port + 1, value)?;
765        }
766        Ok(())
767    }
768
769    /// Begin recording game-thread port writes. Returns an error if already active.
770    pub fn start_capture(&mut self) -> Result<()> {
771        if self.capture.is_some() {
772            return Err(Error::Config("a capture is already active"));
773        }
774        #[cfg(feature = "opl")]
775        let (chip, address) = self
776            .opl
777            .as_ref()
778            .map_or((None, [0; 2]), |opl| (Some(opl.chip), opl.addresses()));
779        #[cfg(not(feature = "opl"))]
780        let (chip, address) = (None, [0; 2]);
781        self.capture = Some(Capture {
782            start: self.now,
783            end: self.now,
784            chip,
785            address,
786            card: self.opl_card(),
787            writes: Vec::new(),
788        });
789        Ok(())
790    }
791
792    fn opl_card(&self) -> Option<(u16, crate::SbModel)> {
793        #[cfg(feature = "sb")]
794        if let Some(sb) = &self.sb {
795            return Some((sb.resources.base, sb.model));
796        }
797        None
798    }
799
800    /// End the active capture, including idle time through the current timestamp.
801    pub fn finish_capture(&mut self) -> Option<Capture> {
802        self.capture.take().map(|mut capture| {
803            capture.end = self.now;
804            capture
805        })
806    }
807}
808
809pub(crate) fn map_opl_ports(port: u16, card: Option<(u16, crate::SbModel)>) -> [Option<u16>; 2] {
810    if let Some((base, model)) = card {
811        let common = (base + 8..=base + 9).contains(&port);
812        if model == crate::SbModel::SbPro && (common || (0x388..=0x389).contains(&port)) {
813            let data = port & 1;
814            return [Some(0x388 + data), Some(0x38a + data)];
815        }
816        if model.has_mixer() && (base..=base + 3).contains(&port) {
817            return [Some(0x388 + port - base), None];
818        }
819        if common {
820            return [Some(0x388 + port - base - 8), None];
821        }
822    }
823    [(0x388..=0x38b).contains(&port).then_some(port), None]
824}
825
826impl IoDevice for DosAudio {
827    fn ports(&self) -> &[PortRange] {
828        &self.ports
829    }
830    fn advance_to(&mut self, at: Tick) {
831        DosAudio::advance_to(self, at);
832    }
833    fn outb(&mut self, at: Tick, port: u16, value: u8) {
834        self.try_outb_at(at, port, value)
835            .expect("sound port write failed");
836    }
837    fn inb(&mut self, at: Tick, port: u16) -> u8 {
838        self.try_inb_at(at, port).expect("sound port read failed")
839    }
840
841    fn irq_request(&self, _at: Tick) -> Option<crate::IrqRequest> {
842        #[cfg(feature = "sb")]
843        if let Some(sb) = &self.sb {
844            return Some(sb.irq_request());
845        }
846        None
847    }
848
849    fn dma_request(&self, _at: Tick) -> Option<crate::DmaRequest> {
850        #[cfg(feature = "sb")]
851        if self.pcm.is_some() || self.memory_dma.is_some() {
852            return None;
853        }
854        #[cfg(feature = "sb")]
855        if let Some(sb) = &self.sb {
856            return sb.dma_request(_at);
857        }
858        None
859    }
860}