Skip to main content

game_toolkit_dosaudio/
offline.rs

1use crate::{
2    AudioEngine, DosAudio, Error, Frame, MusicHandle, PlayOptions, PreparedMusic, Result,
3    SoundConfig, Tick,
4};
5use std::time::Duration;
6
7/// Stereo 16-bit PCM WAV output from a deterministic offline render.
8pub struct Wav {
9    rate: u32,
10    frames: Vec<Frame>,
11}
12impl Wav {
13    /// Output sample rate.
14    pub fn sample_rate(&self) -> u32 {
15        self.rate
16    }
17    /// Captured frames before conversion to signed PCM.
18    pub fn frames(&self) -> &[Frame] {
19        &self.frames
20    }
21    /// Encode a RIFF/WAVE file with two interleaved 16-bit channels.
22    pub fn to_bytes(&self) -> Result<Vec<u8>> {
23        let length = self
24            .frames
25            .len()
26            .checked_mul(4)
27            .and_then(|n| u32::try_from(n).ok())
28            .ok_or(Error::TooLarge)?;
29        let riff_length = length.checked_add(36).ok_or(Error::TooLarge)?;
30        let mut bytes = Vec::with_capacity(
31            usize::try_from(length)
32                .map_err(|_| Error::TooLarge)?
33                .checked_add(44)
34                .ok_or(Error::TooLarge)?,
35        );
36        bytes.extend(b"RIFF");
37        bytes.extend(riff_length.to_le_bytes());
38        bytes.extend(b"WAVEfmt ");
39        bytes.extend(16u32.to_le_bytes());
40        bytes.extend(1u16.to_le_bytes());
41        bytes.extend(2u16.to_le_bytes());
42        bytes.extend(self.rate.to_le_bytes());
43        bytes.extend((self.rate * 4).to_le_bytes());
44        bytes.extend(4u16.to_le_bytes());
45        bytes.extend(16u16.to_le_bytes());
46        bytes.extend(b"data");
47        bytes.extend(length.to_le_bytes());
48        for frame in &self.frames {
49            for &value in frame {
50                let pcm = (value.clamp(-1.0, 1.0) * 32767.0).round() as i16;
51                bytes.extend(pcm.to_le_bytes());
52            }
53        }
54        Ok(bytes)
55    }
56}
57
58/// Both hardware halves on one thread, with no audio device or window.
59///
60/// Writes and time advances use the same queue and renderer as live output.
61/// Output is accumulated as time advances. Music sequencers run in the engine,
62/// so their tempo is independent of those advances and the chosen render chunk.
63pub struct OfflineMachine {
64    control: DosAudio,
65    engine: AudioEngine,
66    frames: Vec<Frame>,
67}
68impl OfflineMachine {
69    /// Create an offline machine at the supplied sample rate, with zero latency.
70    pub fn new(config: SoundConfig, sample_rate: u32) -> Result<Self> {
71        let (control, engine) =
72            DosAudio::split(config.sample_rate(sample_rate).latency(Tick::ZERO))?;
73        Ok(Self {
74            control,
75            engine,
76            frames: Vec::new(),
77        })
78    }
79    /// Current emulated machine time.
80    pub fn now(&self) -> Tick {
81        self.control.now()
82    }
83    /// Read a port at current machine time.
84    pub fn inb(&mut self, port: u16) -> u8 {
85        self.control.inb(port)
86    }
87    /// Write a port at current machine time.
88    pub fn outb(&mut self, port: u16, value: u8) -> Result<()> {
89        self.control.try_outb_at(self.now(), port, value)
90    }
91    /// Earliest outstanding Sound Blaster DMA request.
92    #[cfg(feature = "sb")]
93    pub fn next_dma_tick(&self) -> Option<Tick> {
94        self.control.next_dma_tick()
95    }
96    /// Deliver a host DMA byte at current machine time, after advancing to its request.
97    #[cfg(feature = "sb")]
98    pub fn dma_write(&mut self, channel: crate::DmaChannel, value: u8) -> Result<()> {
99        self.control.try_dma_write_at(self.now(), channel, value)
100    }
101    /// Deliver one word-wide SB16 DMA unit at the current machine timestamp.
102    #[cfg(feature = "sb")]
103    pub fn dma_write16(&mut self, channel: crate::DmaChannel, value: u16) -> Result<()> {
104        self.control.try_dma_write16_at(self.now(), channel, value)
105    }
106    /// Attach conventional memory and route the internal 8237's programming ports.
107    #[cfg(feature = "sb")]
108    pub fn attach_dma(&mut self, memory: std::sync::Arc<crate::ConventionalMemory>) -> Result<()> {
109        self.control.attach_dma(memory)
110    }
111    /// Observe one memory-DMA block completion without acknowledging its IRQ.
112    #[cfg(feature = "sb")]
113    pub fn poll_dma_completion(&mut self) -> Option<crate::DmaCompletion> {
114        self.control.poll_dma_completion()
115    }
116    /// Next predicted DSP block boundary, unless stopped or paused.
117    #[cfg(feature = "sb")]
118    pub fn next_dma_block_tick(&self) -> Option<Tick> {
119        self.control.next_dma_block_tick()
120    }
121    /// Current Sound Blaster IRQ line and level, without acknowledging it.
122    #[cfg(feature = "sb")]
123    pub fn irq_request(&self) -> Option<crate::IrqRequest> {
124        crate::IoDevice::irq_request(&self.control, self.now())
125    }
126    /// Start a PC speaker tone through the PIT registers.
127    pub fn sound(&mut self, hz: f64) -> Result<()> {
128        self.control.sound(hz)
129    }
130    /// Disconnect the PC speaker.
131    pub fn nosound(&mut self) -> Result<()> {
132        self.control.nosound()
133    }
134    /// Install a finite, non-looping soundtrack at current output time.
135    pub fn play(&mut self, sequence: impl Into<PreparedMusic>) -> Result<MusicHandle> {
136        self.engine
137            .play_at(sequence, PlayOptions::default(), self.now())
138    }
139    /// Play a switchable soundtrack on the same offline synthesis timeline.
140    pub fn play_soundtrack(
141        &mut self,
142        soundtrack: std::sync::Arc<crate::Soundtrack>,
143        preferred: crate::MusicDevice,
144        options: crate::PlayOptions,
145    ) -> Result<MusicHandle> {
146        self.engine.play_soundtrack(soundtrack, preferred, options)
147    }
148
149    /// Start an unsigned mono PCM buffer, serviced automatically as time advances.
150    /// Keep its handle alive through rendering; the last byte gets a full hold interval.
151    #[cfg(feature = "sb")]
152    pub fn play_pcm(
153        &mut self,
154        samples: impl Into<std::sync::Arc<[u8]>>,
155        sample_rate: u32,
156    ) -> Result<crate::SfxHandle> {
157        self.control.play_pcm(samples, sample_rate)
158    }
159    /// Install independent speaker music or PWM without replacing the main soundtrack.
160    pub fn play_speaker(
161        &mut self,
162        music: impl Into<PreparedMusic>,
163        options: PlayOptions,
164    ) -> Result<crate::SpeakerHandle> {
165        self.engine.play_speaker(music, options)
166    }
167    /// Advance both halves and capture every complete output sample up to `at`.
168    pub fn advance_to(&mut self, at: Tick) -> Result<()> {
169        if at < self.now() {
170            return Err(Error::BackwardsTime);
171        }
172        let target = u128::from(at.0)
173            * u128::from(self.engine.sample_rate())
174            * u128::from(Tick::HZ_DENOMINATOR)
175            / u128::from(Tick::HZ_NUMERATOR);
176        let target = u64::try_from(target).map_err(|_| Error::TooLarge)?;
177        let extra = target.saturating_sub(self.engine.stats().frames);
178        let extra = usize::try_from(extra).map_err(|_| Error::TooLarge)?;
179        // A RIFF file has a 32-bit size. Reject impossible captures before allocation.
180        if self
181            .frames
182            .len()
183            .checked_add(extra)
184            .ok_or(Error::TooLarge)?
185            > (u32::MAX as usize - 36) / 4
186        {
187            return Err(Error::TooLarge);
188        }
189        loop {
190            #[cfg(feature = "sb")]
191            let next = self.control.next_pcm_tick().map_or(at, |next| next.min(at));
192            #[cfg(not(feature = "sb"))]
193            let next = at;
194            // Render complete frames before the next DMA event, then publish it.
195            // Rendering a partial frame early could miss later events in it.
196            self.render_until(next);
197            self.control.try_advance_to(next)?;
198            if next == at {
199                break;
200            }
201        }
202        Ok(())
203    }
204
205    fn render_until(&mut self, at: Tick) {
206        let target = u128::from(at.0)
207            * u128::from(self.engine.sample_rate())
208            * u128::from(Tick::HZ_DENOMINATOR)
209            / u128::from(Tick::HZ_NUMERATOR);
210        let mut buffer = [[0.0; 2]; 512];
211        let mut remaining = (target as u64).saturating_sub(self.engine.stats().frames);
212        while remaining > 0 {
213            let count = remaining.min(buffer.len() as u64) as usize;
214            self.engine.render(&mut buffer[..count]);
215            self.frames.extend_from_slice(&buffer[..count]);
216            remaining -= count as u64;
217        }
218    }
219    /// Advance by a duration, retaining the existing machine timestamp.
220    pub fn render_for(&mut self, duration: Duration) -> Result<()> {
221        let delta = Tick::checked_from_duration(duration).ok_or(Error::TooLarge)?;
222        self.advance_to(self.now().checked_add(delta).ok_or(Error::TooLarge)?)
223    }
224    /// Render through finite music and/or PCM and a 250 ms release tail.
225    ///
226    /// Keep playback handles alive until this call has completed.
227    pub fn render_to_end(mut self) -> Result<Wav> {
228        #[cfg(feature = "sb")]
229        let pcm_end = self.control.pcm_end();
230        #[cfg(not(feature = "sb"))]
231        let pcm_end = None;
232        let until = self
233            .engine
234            .music_end()?
235            .into_iter()
236            .chain(pcm_end)
237            .chain(self.engine.speaker_end()?)
238            .max()
239            .ok_or(Error::Config("no music or PCM buffer is installed"))?
240            .checked_add(Tick::from_micros(250_000))
241            .ok_or(Error::TooLarge)?;
242        self.advance_to(until.max(self.now()))?;
243        Ok(self.finish())
244    }
245    /// Finish a manually timed capture without adding any samples.
246    pub fn finish(self) -> Wav {
247        Wav {
248            rate: self.engine.sample_rate(),
249            frames: self.frames,
250        }
251    }
252}