songbird/input/codecs/dca/
mod.rs

1mod metadata;
2pub use self::metadata::*;
3
4use crate::constants::{SAMPLE_RATE, SAMPLE_RATE_RAW};
5
6use std::io::{Seek, SeekFrom};
7use symphonia::core::{
8    codecs::{CodecParameters, CODEC_TYPE_OPUS},
9    errors::{self as symph_err, Error as SymphError, Result as SymphResult, SeekErrorKind},
10    formats::prelude::*,
11    io::{MediaSource, MediaSourceStream, ReadBytes, SeekBuffered},
12    meta::{Metadata as SymphMetadata, MetadataBuilder, MetadataLog, StandardTagKey, Tag, Value},
13    probe::{Descriptor, Instantiate, QueryDescriptor},
14    sample::SampleFormat,
15};
16
17impl QueryDescriptor for DcaReader {
18    fn query() -> &'static [Descriptor] {
19        &[symphonia_core::support_format!(
20            "dca",
21            "DCA[0/1] Opus Wrapper",
22            &["dca"],
23            &[],
24            &[b"DCA1"]
25        )]
26    }
27
28    fn score(_context: &[u8]) -> u8 {
29        255
30    }
31}
32
33struct SeekAccel {
34    frame_offsets: Vec<(TimeStamp, u64)>,
35    seek_index_fill_rate: u16,
36    next_ts: TimeStamp,
37}
38
39impl SeekAccel {
40    fn new(options: FormatOptions, first_frame_byte_pos: u64) -> Self {
41        let per_s = options.seek_index_fill_rate;
42        let next_ts = (per_s as u64) * (SAMPLE_RATE_RAW as u64);
43
44        Self {
45            frame_offsets: vec![(0, first_frame_byte_pos)],
46            seek_index_fill_rate: per_s,
47            next_ts,
48        }
49    }
50
51    fn update(&mut self, ts: TimeStamp, pos: u64) {
52        if ts >= self.next_ts {
53            self.next_ts += (self.seek_index_fill_rate as u64) * (SAMPLE_RATE_RAW as u64);
54            self.frame_offsets.push((ts, pos));
55        }
56    }
57
58    fn get_seek_pos(&self, ts: TimeStamp) -> (TimeStamp, u64) {
59        let index = self.frame_offsets.partition_point(|&(o_ts, _)| o_ts <= ts) - 1;
60        self.frame_offsets[index]
61    }
62}
63
64/// [DCA\[0/1\]](https://github.com/bwmarrin/dca) Format reader for Symphonia.
65pub struct DcaReader {
66    source: MediaSourceStream,
67    track: Option<Track>,
68    metas: MetadataLog,
69    seek_accel: SeekAccel,
70    curr_ts: TimeStamp,
71    max_ts: Option<TimeStamp>,
72    held_packet: Option<Packet>,
73}
74
75impl FormatReader for DcaReader {
76    fn try_new(mut source: MediaSourceStream, options: &FormatOptions) -> SymphResult<Self> {
77        // Read in the magic number to verify it's a DCA file.
78        let magic = source.read_quad_bytes()?;
79
80        // FIXME: make use of the new options.enable_gapless to apply the opus coder delay.
81
82        let read_meta = match &magic {
83            b"DCA1" => true,
84            _ if &magic[..3] == b"DCA" => {
85                return symph_err::unsupported_error("unsupported DCA version");
86            },
87            _ => {
88                source.seek_buffered_rel(-4);
89                false
90            },
91        };
92
93        let mut codec_params = CodecParameters::new();
94
95        codec_params
96            .for_codec(CODEC_TYPE_OPUS)
97            .with_max_frames_per_packet(1)
98            .with_sample_rate(SAMPLE_RATE_RAW as u32)
99            .with_time_base(TimeBase::new(1, SAMPLE_RATE_RAW as u32))
100            .with_sample_format(SampleFormat::F32);
101
102        let mut metas = MetadataLog::default();
103
104        if read_meta {
105            let size = source.read_u32()?;
106
107            // Sanity check
108            if (size as i32) < 2 {
109                return symph_err::decode_error("missing DCA1 metadata block");
110            }
111
112            let raw_json = source.read_boxed_slice_exact(size as usize)?;
113
114            let metadata: DcaMetadata = serde_json::from_slice::<DcaMetadata>(&raw_json)
115                .map_err(|_| SymphError::DecodeError("malformed DCA1 metadata block"))?;
116
117            let mut revision = MetadataBuilder::new();
118
119            if let Some(info) = metadata.info {
120                if let Some(t) = info.title {
121                    revision.add_tag(Tag::new(
122                        Some(StandardTagKey::TrackTitle),
123                        "title",
124                        Value::String(t),
125                    ));
126                }
127                if let Some(t) = info.album {
128                    revision.add_tag(Tag::new(
129                        Some(StandardTagKey::Album),
130                        "album",
131                        Value::String(t),
132                    ));
133                }
134                if let Some(t) = info.artist {
135                    revision.add_tag(Tag::new(
136                        Some(StandardTagKey::Artist),
137                        "artist",
138                        Value::String(t),
139                    ));
140                }
141                if let Some(t) = info.genre {
142                    revision.add_tag(Tag::new(
143                        Some(StandardTagKey::Genre),
144                        "genre",
145                        Value::String(t),
146                    ));
147                }
148                if let Some(t) = info.comments {
149                    revision.add_tag(Tag::new(
150                        Some(StandardTagKey::Comment),
151                        "comments",
152                        Value::String(t),
153                    ));
154                }
155                if let Some(_t) = info.cover {
156                    // TODO: Add visual, figure out MIME types.
157                }
158            }
159
160            if let Some(origin) = metadata.origin {
161                if let Some(t) = origin.url {
162                    revision.add_tag(Tag::new(Some(StandardTagKey::Url), "url", Value::String(t)));
163                }
164            }
165
166            metas.push(revision.metadata());
167        }
168
169        let bytes_read = source.pos();
170
171        Ok(Self {
172            source,
173            track: Some(Track {
174                id: 0,
175                language: None,
176                codec_params,
177            }),
178            metas,
179            seek_accel: SeekAccel::new(*options, bytes_read),
180            curr_ts: 0,
181            max_ts: None,
182            held_packet: None,
183        })
184    }
185
186    fn cues(&self) -> &[Cue] {
187        // No cues in DCA...
188        &[]
189    }
190
191    fn metadata(&mut self) -> SymphMetadata<'_> {
192        self.metas.metadata()
193    }
194
195    fn seek(&mut self, _mode: SeekMode, to: SeekTo) -> SymphResult<SeekedTo> {
196        let can_backseek = self.source.is_seekable();
197
198        let track = if self.track.is_none() {
199            return symph_err::seek_error(SeekErrorKind::Unseekable);
200        } else {
201            self.track.as_ref().unwrap()
202        };
203
204        let rate = track.codec_params.sample_rate;
205        let ts = match to {
206            SeekTo::Time { time, .. } =>
207                if let Some(rate) = rate {
208                    TimeBase::new(1, rate).calc_timestamp(time)
209                } else {
210                    return symph_err::seek_error(SeekErrorKind::Unseekable);
211                },
212            SeekTo::TimeStamp { ts, .. } => ts,
213        };
214
215        if let Some(max_ts) = self.max_ts {
216            if ts > max_ts {
217                return symph_err::seek_error(SeekErrorKind::OutOfRange);
218            }
219        }
220
221        let backseek_needed = self.curr_ts > ts;
222
223        if backseek_needed && !can_backseek {
224            return symph_err::seek_error(SeekErrorKind::ForwardOnly);
225        }
226
227        let (accel_seek_ts, accel_seek_pos) = self.seek_accel.get_seek_pos(ts);
228
229        if backseek_needed || accel_seek_pos > self.source.pos() {
230            self.source.seek(SeekFrom::Start(accel_seek_pos))?;
231            self.curr_ts = accel_seek_ts;
232        }
233
234        while let Ok(pkt) = self.next_packet() {
235            let pts = pkt.ts;
236            let dur = pkt.dur;
237            let track_id = pkt.track_id();
238
239            if (pts..pts + dur).contains(&ts) {
240                self.held_packet = Some(pkt);
241                return Ok(SeekedTo {
242                    track_id,
243                    required_ts: ts,
244                    actual_ts: pts,
245                });
246            }
247        }
248
249        symph_err::seek_error(SeekErrorKind::OutOfRange)
250    }
251
252    fn tracks(&self) -> &[Track] {
253        // DCA tracks can hold only one track by design.
254        // Of course, a zero-length file is technically allowed,
255        // in which case no track.
256        if let Some(track) = self.track.as_ref() {
257            std::slice::from_ref(track)
258        } else {
259            &[]
260        }
261    }
262
263    fn default_track(&self) -> Option<&Track> {
264        self.track.as_ref()
265    }
266
267    fn next_packet(&mut self) -> SymphResult<Packet> {
268        if let Some(pkt) = self.held_packet.take() {
269            return Ok(pkt);
270        }
271
272        let frame_pos = self.source.pos();
273
274        let p_len = match self.source.read_u16() {
275            Ok(len) => len as i16,
276            Err(eof) => {
277                self.max_ts = Some(self.curr_ts);
278                return Err(eof.into());
279            },
280        };
281
282        if p_len < 0 {
283            return symph_err::decode_error("DCA frame header had a negative length.");
284        }
285
286        let buf = self.source.read_boxed_slice_exact(p_len as usize)?;
287
288        let checked_buf = buf[..].try_into().or_else(|_| {
289            symph_err::decode_error("Packet was not a valid Opus Packet: too large for audiopus.")
290        })?;
291
292        let sample_ct = audiopus::packet::nb_samples(checked_buf, SAMPLE_RATE).or_else(|_| {
293            symph_err::decode_error(
294                "Packet was not a valid Opus packet: couldn't read sample count.",
295            )
296        })? as u64;
297
298        let out = Packet::new_from_boxed_slice(0, self.curr_ts, sample_ct, buf);
299
300        self.seek_accel.update(self.curr_ts, frame_pos);
301
302        self.curr_ts += sample_ct;
303
304        Ok(out)
305    }
306
307    fn into_inner(self: Box<Self>) -> MediaSourceStream {
308        self.source
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use crate::input::input_tests::*;
315    use crate::{constants::test_data::FILE_DCA_TARGET, input::File};
316
317    // NOTE: this covers youtube audio in a non-copyright-violating way, since
318    // those depend on an HttpRequest internally anyhow.
319    #[tokio::test]
320    #[ntest::timeout(10_000)]
321    async fn dca_track_plays() {
322        track_plays_passthrough(|| File::new(FILE_DCA_TARGET)).await;
323    }
324
325    #[tokio::test]
326    #[ntest::timeout(10_000)]
327    async fn dca_forward_seek_correct() {
328        forward_seek_correct(|| File::new(FILE_DCA_TARGET)).await;
329    }
330
331    #[tokio::test]
332    #[ntest::timeout(10_000)]
333    async fn dca_backward_seek_correct() {
334        backward_seek_correct(|| File::new(FILE_DCA_TARGET)).await;
335    }
336
337    #[tokio::test]
338    #[ntest::timeout(10_000)]
339    async fn opus_passthrough_when_other_tracks_paused() {
340        track_plays_passthrough_when_is_only_active(|| File::new(FILE_DCA_TARGET)).await;
341    }
342}