songbird/input/codecs/dca/
mod.rs1mod 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
64pub 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 let magic = source.read_quad_bytes()?;
79
80 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 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 }
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 &[]
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 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 #[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}