Skip to main content

game_toolkit_dosaudio/capture/
stream.rs

1use super::{Capture, PortWrite};
2use crate::{Error, OplChip, Result, Tick};
3use rtrb::{Consumer, Producer};
4use std::sync::{
5    Arc,
6    atomic::{AtomicBool, AtomicU64, Ordering},
7};
8
9#[derive(Clone, Copy)]
10struct RegisterWrite {
11    at: Tick,
12    register: u16,
13    value: u8,
14}
15
16struct State {
17    stop: AtomicBool,
18    finished: AtomicBool,
19    overflowed: AtomicBool,
20    end: AtomicU64,
21}
22
23/// Control-thread reader for synthesis register writes applied by the audio engine.
24///
25/// Poll [`Self::try_finish`] regularly to drain the bounded queue. Capture includes
26/// manual, prepared and live OPL writes and note releases at their actual output
27/// times. It records registers rather than PCM: fades, post-chip gain, analog
28/// mixing, MIDI and PC speaker operations are not representable in DRO.
29/// Start before programming instruments for a self-contained replay.
30///
31/// Stopping is acknowledged at an output-frame boundary. Keep rendering until
32/// [`Self::is_finished`] becomes true, then collect the result on this thread.
33/// Overflow permanently invalidates the capture and is reported explicitly.
34/// Dropping this reader requests a stop; callback-owned resources remain owned
35/// by the engine until it is safely dropped or reconfigured outside rendering.
36pub struct MusicCapture {
37    incoming: Consumer<RegisterWrite>,
38    state: Arc<State>,
39    chip: OplChip,
40    start: Tick,
41    writes: Vec<PortWrite>,
42    consumed: bool,
43}
44
45pub(crate) struct CaptureWriter {
46    outgoing: Producer<RegisterWrite>,
47    state: Arc<State>,
48}
49
50pub(crate) fn stream(
51    chip: OplChip,
52    start: Tick,
53    capacity: usize,
54) -> Result<(MusicCapture, CaptureWriter)> {
55    if capacity == 0 || capacity > 1_048_576 {
56        return Err(Error::Config(
57            "capture queue capacity must be 1..=1,048,576",
58        ));
59    }
60    let (outgoing, incoming) = rtrb::RingBuffer::new(capacity);
61    let state = Arc::new(State {
62        stop: AtomicBool::new(false),
63        finished: AtomicBool::new(false),
64        overflowed: AtomicBool::new(false),
65        end: AtomicU64::new(start.0),
66    });
67    Ok((
68        MusicCapture {
69            incoming,
70            state: Arc::clone(&state),
71            chip,
72            start,
73            writes: Vec::new(),
74            consumed: false,
75        },
76        CaptureWriter { outgoing, state },
77    ))
78}
79
80impl MusicCapture {
81    /// Request capture stop at the next output-frame boundary. Idempotent.
82    pub fn stop(&self) {
83        self.state.stop.store(true, Ordering::Release);
84    }
85
86    /// Whether the callback has acknowledged stop or capture overflow.
87    pub fn is_finished(&self) -> bool {
88        self.state.finished.load(Ordering::Acquire)
89    }
90
91    /// Whether the bounded queue has lost an event, invalidating this capture.
92    pub fn is_overflowed(&self) -> bool {
93        self.state.overflowed.load(Ordering::Acquire)
94    }
95
96    /// Drain pending writes on this thread and take an acknowledged capture once.
97    ///
98    /// Returns `None` until a frame boundary acknowledges stop, and after the
99    /// result has been consumed. Queue overflow returns [`Error::CaptureOverflow`]
100    /// once instead of a truncated capture. All conversion allocations occur here.
101    pub fn try_finish(&mut self) -> Result<Option<Capture>> {
102        if self.consumed {
103            return Ok(None);
104        }
105        // Observe the completion publication before draining: once observed, the
106        // producer cannot append more writes and all its final pushes are visible.
107        let finished = self.is_finished();
108        while let Ok(write) = self.incoming.pop() {
109            if !self.is_overflowed() {
110                let address = if write.register & 0x100 == 0 {
111                    0x388
112                } else {
113                    0x38a
114                };
115                self.writes.extend([
116                    PortWrite {
117                        at: write.at,
118                        port: address,
119                        value: write.register as u8,
120                    },
121                    PortWrite {
122                        at: write.at,
123                        port: address + 1,
124                        value: write.value,
125                    },
126                ]);
127            }
128        }
129        if !finished {
130            return Ok(None);
131        }
132        self.consumed = true;
133        if self.is_overflowed() {
134            self.writes.clear();
135            return Err(Error::CaptureOverflow);
136        }
137        Ok(Some(Capture {
138            start: self.start,
139            end: Tick(self.state.end.load(Ordering::Acquire)),
140            chip: Some(self.chip),
141            address: [0; 2],
142            card: None,
143            writes: std::mem::take(&mut self.writes),
144        }))
145    }
146}
147
148impl Drop for MusicCapture {
149    fn drop(&mut self) {
150        self.stop();
151    }
152}
153
154impl CaptureWriter {
155    #[cfg(feature = "opl")]
156    pub(crate) fn record(&mut self, at: Tick, register: u16, value: u8) {
157        if self.is_finished()
158            || self.state.overflowed.load(Ordering::Acquire)
159            || self.outgoing.is_abandoned()
160        {
161            return;
162        }
163        if self
164            .outgoing
165            .push(RegisterWrite {
166                at,
167                register,
168                value,
169            })
170            .is_err()
171        {
172            self.state.overflowed.store(true, Ordering::Release);
173        }
174    }
175
176    pub(crate) fn boundary(&mut self, at: Tick) {
177        if self.is_finished() {
178            return;
179        }
180        self.state.end.store(at.0, Ordering::Relaxed);
181        if self.state.stop.load(Ordering::Acquire)
182            || self.state.overflowed.load(Ordering::Acquire)
183            || self.outgoing.is_abandoned()
184        {
185            self.state.finished.store(true, Ordering::Release);
186        }
187    }
188
189    pub(crate) fn is_finished(&self) -> bool {
190        self.state.finished.load(Ordering::Acquire)
191    }
192}