Skip to main content

game_toolkit_dosaudio/
capture.rs

1use crate::{Error, OplChip, Result, Tick};
2pub(crate) mod stream;
3pub use stream::MusicCapture;
4
5/// A successful game-thread port write and its absolute machine timestamp.
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub struct PortWrite {
8    /// Timestamp supplied to the bus.
9    pub at: Tick,
10    /// I/O port number.
11    pub port: u16,
12    /// Value written.
13    pub value: u8,
14}
15
16/// A game-thread register trace. Capturing allocates only on the control thread.
17///
18/// Music events scheduled directly inside `AudioEngine` are not port accesses
19/// and are not included. A capture preserves writes, not a snapshot of a playing
20/// chip: start it before programming instruments for a self-contained replay.
21#[derive(Clone, Debug)]
22pub struct Capture {
23    pub(crate) start: Tick,
24    pub(crate) end: Tick,
25    pub(crate) chip: Option<OplChip>,
26    pub(crate) address: [u8; 2],
27    pub(crate) card: Option<(u16, crate::SbModel)>,
28    pub(crate) writes: Vec<PortWrite>,
29}
30impl Capture {
31    /// Recorded port writes, in dispatch order.
32    pub fn writes(&self) -> &[PortWrite] {
33        &self.writes
34    }
35    /// Capture duration through the time `finish_capture` was called.
36    pub fn duration(&self) -> Tick {
37        self.end - self.start
38    }
39    /// Export OPL accesses as a legacy v1 DRO capture with millisecond timing.
40    ///
41    /// Absolute relative timestamps are rounded to the nearest millisecond, preventing
42    /// cumulative delay drift. Speaker and unrelated port writes are omitted.
43    pub fn to_dro(&self) -> Result<Vec<u8>> {
44        let chip = self
45            .chip
46            .ok_or(Error::Config("DRO export requires installed OPL hardware"))?;
47        let milliseconds = |at: Tick| -> Result<u32> {
48            let elapsed = at.checked_sub(self.start).ok_or(Error::BackwardsTime)?;
49            u32::try_from(
50                (u128::from(elapsed.0) * u128::from(Tick::HZ_DENOMINATOR) * 1_000
51                    + u128::from(Tick::HZ_NUMERATOR) / 2)
52                    / u128::from(Tick::HZ_NUMERATOR),
53            )
54            .map_err(|_| Error::TooLarge)
55        };
56        let mut data = Vec::new();
57        let mut address = self.address;
58        let mut bank = 0usize;
59        let mut time = 0u32;
60        for write in &self.writes {
61            for port in crate::bus::map_opl_ports(write.port, self.card)
62                .into_iter()
63                .flatten()
64            {
65                if !(0x388..=0x38b).contains(&port) {
66                    continue;
67                }
68                let target = usize::from(port >= 0x38a);
69                if target == 1 && chip == OplChip::Opl2 {
70                    continue;
71                }
72                if port & 1 == 0 {
73                    address[target] = write.value;
74                    continue;
75                }
76                let next = milliseconds(write.at)?;
77                delay(&mut data, next - time);
78                time = next;
79                if bank != target {
80                    data.push(if target == 0 { 2 } else { 3 });
81                    bank = target;
82                }
83                let register = address[target];
84                if register <= 4 {
85                    data.push(4);
86                }
87                data.extend([register, write.value]);
88            }
89        }
90        let end = milliseconds(self.end)?;
91        delay(&mut data, end - time);
92        let size = u32::try_from(data.len()).map_err(|_| Error::TooLarge)?;
93        let mut bytes = Vec::with_capacity(data.len().checked_add(24).ok_or(Error::TooLarge)?);
94        bytes.extend(b"DBRAWOPL");
95        bytes.extend(0u16.to_le_bytes());
96        bytes.extend(1u16.to_le_bytes());
97        bytes.extend(end.to_le_bytes());
98        bytes.extend(size.to_le_bytes());
99        bytes.extend(
100            match chip {
101                OplChip::Opl2 => 0u32,
102                OplChip::Opl3 => 1,
103                OplChip::DualOpl2 => 2,
104            }
105            .to_le_bytes(),
106        );
107        bytes.extend(data);
108        Ok(bytes)
109    }
110}
111
112fn delay(data: &mut Vec<u8>, mut milliseconds: u32) {
113    while milliseconds > 0 {
114        let amount = milliseconds.min(65_536);
115        if amount <= 256 {
116            data.extend([0, (amount - 1) as u8]);
117        } else {
118            data.push(1);
119            data.extend(((amount - 1) as u16).to_le_bytes());
120        }
121        milliseconds -= amount;
122    }
123}