ndsd_playback/dsd_readers/
dff_reader.rs

1use byteorder::{BigEndian, ReadBytesExt};
2use std::fs::File;
3use std::io;
4use std::io::{Read, Seek, SeekFrom};
5use crate::dsd_readers::{DSDFormat, DSDReader};
6
7pub struct DFFReader {
8    file: File,
9    buf: Vec<u8>,          // internal interleaved read buffer (bytes: frames * channels)
10    ch: usize,             // channels
11    block_frames: usize,   // frames per internal read block (1 frame == 1 byte per channel)
12    filled_frames: usize,  // frames currently in buf
13    pos_frames: usize,     // current read position in frames inside buf
14    total_frames: u64,     // total frames (samples per channel)
15    read_frames: u64,      // frames read so far (position)
16    data_start: u64,       // start offset of DSD chunk payload
17}
18
19impl DFFReader {
20    pub fn new(path: &str) -> io::Result<Self> {
21        let file = File::open(path)?;
22        Ok(Self {
23            file,
24            buf: Vec::new(),
25            ch: 0,
26            block_frames: 4096, // default frames per block
27            filled_frames: 0,
28            pos_frames: 0,
29            total_frames: 0,
30            read_frames: 0,
31            data_start: 0,
32        })
33    }
34
35    pub fn empty() -> Self {
36        Self {
37            file: File::create("super_empty").unwrap(),
38            buf: Vec::new(),
39            ch: 0,
40            block_frames: 4096,
41            filled_frames: 0,
42            pos_frames: 0,
43            total_frames: 0,
44            read_frames: 0,
45            data_start: 0,
46        }
47    }
48
49    // helper: read 4-byte id
50    fn read_id(&mut self) -> io::Result<[u8; 4]> {
51        let mut id = [0u8; 4];
52        self.file.read_exact(&mut id)?;
53        Ok(id)
54    }
55
56    // helper: read big-endian u64 (DFF/DSDIFF uses big-endian for chunk sizes)
57    fn read_be_u64(&mut self) -> io::Result<u64> {
58        self.file.read_u64::<BigEndian>()
59    }
60}
61
62impl DSDReader for DFFReader {
63    fn open(&mut self, format: &mut DSDFormat) -> io::Result<()> {
64        // --- FRM8 header (big-endian) ---
65        let id = self.read_id()?;
66        if &id != b"FRM8" {
67            return Err(io::Error::new(io::ErrorKind::InvalidData, "not FRM8 / DFF"));
68        }
69
70        // FRM8 size (big-endian u64) - skip/validate
71        let _frm8_size = self.read_be_u64()?; // we don't strictly need it here
72
73        // Next 4 bytes: format id - should be "DSD "
74        let fmt_id = self.read_id()?;
75        if &fmt_id != b"DSD " {
76            return Err(io::Error::new(io::ErrorKind::InvalidData, "not DSD container"));
77        }
78
79        // We'll parse chunks until we find the "DSD " audio chunk.
80        // PROP chunk contains SND subchunks (FS, CHNL, CMPR).
81        let found_dsd;
82        let dsd_chunk_size: u64;
83
84        // metadata we will fill from PROP/SND
85        let mut sample_rate_hz: Option<u32> = None;
86        let mut channels: Option<u16> = None;
87        // `lsbitfirst` in original C++ was configurable; default false here
88        format.is_lsb_first = false;
89
90        loop {
91            // read chunk header: 4-byte id + 8-byte BE size
92            let mut chunk_id = [0u8; 4];
93            // If we reach EOF unexpectedly, error
94            if let Err(e) = self.file.read_exact(&mut chunk_id) {
95                return Err(io::Error::new(io::ErrorKind::InvalidData, format!("unexpected EOF reading chunk id: {}", e)));
96            }
97            let chunk_size = self.read_be_u64()?;
98            let chunk_payload_start = self.file.seek(SeekFrom::Current(0))?;
99
100            match &chunk_id {
101                b"PROP" => {
102                    // PROP payload starts with a 4-byte prop id (e.g. "SND ")
103                    let mut prop_id = [0u8; 4];
104                    self.file.read_exact(&mut prop_id)?;
105                    if &prop_id == b"SND " {
106                        // Parse subchunks inside SND until end of PROP
107                        let prop_end = chunk_payload_start + chunk_size;
108                        while self.file.seek(SeekFrom::Current(0))? < prop_end {
109                            // subchunk header: 4-byte id + 8-byte BE size
110                            let mut sub_id = [0u8; 4];
111                            if let Err(e) = self.file.read_exact(&mut sub_id) {
112                                return Err(io::Error::new(io::ErrorKind::InvalidData, format!("unexpected EOF in SND subchunk id: {}", e)));
113                            }
114                            let sub_size = self.read_be_u64()?;
115                            let sub_payload_start = self.file.seek(SeekFrom::Current(0))?;
116
117                            match &sub_id {
118                                b"FS  " => {
119                                    // sample rate (big-endian u32)
120                                    if sub_size >= 4 {
121                                        let sr = self.file.read_u32::<BigEndian>()?;
122                                        sample_rate_hz = Some(sr);
123                                    } else {
124                                        // invalid FS subchunk: skip
125                                        self.file.seek(SeekFrom::Start(sub_payload_start + sub_size))?;
126                                    }
127                                }
128                                b"CHNL" => {
129                                    // channels (big-endian u16)
130                                    if sub_size >= 2 {
131                                        let ch = self.file.read_u16::<BigEndian>()?;
132                                        channels = Some(ch);
133                                    } else {
134                                        self.file.seek(SeekFrom::Start(sub_payload_start + sub_size))?;
135                                    }
136                                }
137                                b"CMPR" => {
138                                    // compression id (4 bytes), we accept only "DSD "
139                                    if sub_size >= 4 {
140                                        let mut cmp = [0u8; 4];
141                                        self.file.read_exact(&mut cmp)?;
142                                        if &cmp != b"DSD " {
143                                            return Err(io::Error::new(io::ErrorKind::InvalidData, "unsupported CMPR (not DSD)"));
144                                        }
145                                    } else {
146                                        return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid CMPR chunk"));
147                                    }
148                                }
149                                _ => {
150                                    // ignore unknown subchunk
151                                }
152                            }
153
154                            // subchunk payloads are padded to even length per spec:
155                            // padded = (sub_size + 1) & ~1
156                            let padded = (sub_size + 1) & !1u64;
157                            self.file.seek(SeekFrom::Start(sub_payload_start + padded))?;
158                        }
159                    } else {
160                        // not a SND PROP variant - skip PROP payload (padded)
161                        let padded = (chunk_size + 1) & !1u64;
162                        self.file.seek(SeekFrom::Start(chunk_payload_start + padded))?;
163                    }
164                }
165
166                b"DSD " => {
167                    // Found audio chunk
168                    found_dsd = true;
169                    dsd_chunk_size = chunk_size;
170                    // data_start points at the beginning of the audio payload
171                    self.data_start = self.file.seek(SeekFrom::Current(0))?;
172                    // break: we have what we need to prepare decoding
173                    break;
174                }
175
176                _ => {
177                    // skip unknown chunk payload (padded to even)
178                    let padded = (chunk_size + 1) & !1u64;
179                    self.file.seek(SeekFrom::Start(chunk_payload_start + padded))?;
180                }
181            }
182        }
183
184        if !found_dsd {
185            return Err(io::Error::new(io::ErrorKind::InvalidData, "DSD chunk not found"));
186        }
187
188        // require CHNL and FS
189        let channels = channels.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "CHNL missing"))?;
190        let fs = sample_rate_hz.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "FS missing"))?;
191
192        // Fill format and internal fields
193        format.num_channels = channels as u32;
194        self.ch = channels as usize;
195        format.sampling_rate = fs;
196
197        // total_frames = chunk_size / channels (1 byte per channel per frame)
198        let total_frames = dsd_chunk_size / (self.ch as u64);
199        format.total_samples = total_frames; // samples == frames per channel
200        self.total_frames = total_frames;
201
202        // allocate buffer: block_frames * channels bytes
203        self.buf.resize(self.block_frames * self.ch, 0);
204
205        Ok(())
206    }
207
208    fn read(&mut self, data: &mut [&mut [u8]], bytes_per_channel: usize) -> io::Result<usize> {
209        // Mirror C++ decode loop semantics: produce bytes_per_channel bytes per channel
210        if self.ch == 0 {
211            return Ok(0);
212        }
213        if data.len() < self.ch {
214            return Err(io::Error::new(io::ErrorKind::InvalidInput, "not enough channel buffers"));
215        }
216
217        // `written` is how many bytes per channel we've written so far
218        let mut written = 0usize;
219
220        while written < bytes_per_channel {
221            // refill internal buffer if exhausted
222            if self.pos_frames == self.filled_frames {
223                // frames to read in this pass (try to fill block_frames but at least enough to fulfill request)
224                let frames_to_read = (bytes_per_channel - written).min(self.block_frames);
225                let bytes_to_read = frames_to_read * self.ch;
226                self.buf.resize(bytes_to_read, 0);
227                let n = self.file.read(&mut self.buf)?;
228                if n == 0 {
229                    // EOF
230                    return Ok(written);
231                }
232                // n should be multiple of channels; compute frames
233                self.filled_frames = n / self.ch;
234                self.pos_frames = 0;
235            }
236
237            let available_frames = self.filled_frames - self.pos_frames;
238            let need_frames = bytes_per_channel - written;
239            let take_frames = available_frames.min(need_frames);
240
241            // deinterleave `take_frames` frames from buf into channel slices
242            // frames are interleaved: for each frame f: byte0..byte(ch-1)
243            for ch_idx in 0..self.ch {
244                let dst = &mut data[ch_idx][written..written + take_frames];
245                // copy bytes for this channel from each frame with stride
246                let mut dst_i = 0usize;
247                let mut src_offset = self.pos_frames * self.ch + ch_idx;
248                for _ in 0..take_frames {
249                    dst[dst_i] = self.buf[src_offset];
250                    dst_i += 1;
251                    src_offset += self.ch;
252                }
253            }
254
255            self.pos_frames += take_frames;
256            written += take_frames;
257            self.read_frames = self.read_frames.saturating_add(take_frames as u64);
258        }
259
260        Ok(written)
261    }
262
263    fn seek_percent(&mut self, percent: f64) -> io::Result<()> {
264        if !(0.0..=1.0).contains(&percent) {
265            return Err(io::Error::new(io::ErrorKind::InvalidInput, "percent out of range"));
266        }
267        let target_frame = (self.total_frames as f64 * percent) as u64;
268        self.seek_samples(target_frame)
269    }
270
271    fn seek_samples(&mut self, sample_index: u64) -> io::Result<()> {
272        // In DFF: 1 frame => 1 byte per channel; byte offset = sample_index * channels
273        let byte_offset = sample_index.checked_mul(self.ch as u64)
274            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "seek overflow"))?;
275        let offset = self.data_start + byte_offset;
276        self.file.seek(SeekFrom::Start(offset))?;
277        self.read_frames = sample_index;
278        self.pos_frames = 0;
279        self.filled_frames = 0;
280        Ok(())
281    }
282
283    fn get_position_frames(&self) -> u64 {
284        self.read_frames
285    }
286
287    fn get_position_percent(&self) -> f64 {
288        if self.total_frames == 0 {
289            0.0
290        } else {
291            (self.get_position_frames() as f64 / self.total_frames as f64).min(1.0)
292        }
293    }
294}