1use std::fs::File;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
5use std::thread;
6
7use symphonia::core::codecs::audio::well_known::{
8 CODEC_ID_AAC, CODEC_ID_ALAC, CODEC_ID_FLAC, CODEC_ID_MP3, CODEC_ID_OPUS, CODEC_ID_PCM_F32LE,
9 CODEC_ID_PCM_S16LE, CODEC_ID_PCM_S24LE, CODEC_ID_PCM_S32LE, CODEC_ID_VORBIS,
10};
11use symphonia::core::codecs::audio::{AudioCodecId, AudioCodecParameters, AudioDecoderOptions};
12use symphonia::core::formats::probe::Hint;
13use symphonia::core::formats::{FormatOptions, FormatReader, SeekMode, SeekTo, Track, TrackType};
14use symphonia::core::io::MediaSourceStream;
15use symphonia::core::meta::MetadataOptions;
16use symphonia::core::units::{Duration, Time, TimeBase, Timestamp};
17use symphonia_common::apple::audio::alac::MagicCookie;
18use thiserror::Error;
19
20use crate::audio::dsp::{Chain, Setup};
21use crate::audio::opus::OpusBridge;
22use crate::audio::viz::VizBuffer;
23use crate::config::ReplayGainMode;
24use crate::player::state::{QueueItemId, RendererClock};
25
26#[derive(Debug, Error)]
27pub enum DecodeError {
28 #[error("failed to open file: {0}")]
29 Io(#[from] std::io::Error),
30 #[error("no supported audio track found")]
31 NoTrack,
32 #[error("unsupported codec")]
33 UnsupportedCodec,
34 #[error("decode error: {0}")]
35 Decode(String),
36}
37
38#[derive(Debug, Clone)]
40pub struct StreamInfo {
41 pub codec: String,
42 pub sample_rate: u32,
43 pub channels: u16,
44 pub bit_depth: Option<u16>,
45 pub bitrate_kbps: Option<u32>,
47 pub duration_ms: u64,
48}
49
50pub struct DecodeHandle {
52 stop: Arc<AtomicBool>,
53 thread: Option<thread::JoinHandle<()>>,
54}
55
56impl DecodeHandle {
57 pub fn signal_stop(&self) {
61 self.stop.store(true, Ordering::Relaxed);
62 if let Some(handle) = &self.thread {
63 handle.thread().unpark();
64 }
65 }
66
67 pub fn thread(&self) -> Option<thread::Thread> {
71 self.thread.as_ref().map(|t| t.thread().clone())
72 }
73
74 #[cfg(test)]
76 pub fn new_for_test(stop: Arc<AtomicBool>) -> Self {
77 Self { stop, thread: None }
78 }
79
80 pub fn stop(&mut self) {
82 self.signal_stop();
83 if let Some(handle) = self.thread.take()
84 && let Err(payload) = handle.join()
85 {
86 let msg = payload
87 .downcast_ref::<String>()
88 .map(|s| s.as_str())
89 .or_else(|| payload.downcast_ref::<&str>().copied())
90 .unwrap_or("unknown");
91 log::error!("decode thread panicked: {}", msg);
92 }
93 }
94}
95
96impl Drop for DecodeHandle {
97 fn drop(&mut self) {
98 self.stop();
99 }
100}
101
102#[derive(Debug, Clone)]
107pub struct TrackBoundary {
108 pub id: QueueItemId,
109 pub path: PathBuf,
110 pub info: StreamInfo,
111 pub sample_offset: u64,
115 pub samples_written: u64,
118 pub seek_samples: u64,
120 pub output_rate: u32,
123}
124
125impl TrackBoundary {
126 fn position_ms(&self, played: u64) -> Option<u64> {
128 let ch = self.info.channels as u64;
129 let rate = self.info.sample_rate as u64;
130 let out = self.output_rate as u64;
131 if ch == 0 || rate == 0 || out == 0 {
132 return None;
133 }
134 let track_samples = played.saturating_sub(self.sample_offset);
136 Some((track_samples / ch) * 1000 / out + (self.seek_samples / ch) * 1000 / rate)
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub struct Playhead {
144 pub boundary: usize,
146 pub id: QueueItemId,
147 pub position_ms: u64,
148}
149
150pub struct PlaybackTimeline {
154 boundaries: parking_lot::RwLock<Vec<TrackBoundary>>,
155 samples_written: AtomicU64,
157 pub samples_played: Arc<AtomicU64>,
160 queued: parking_lot::Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
163 clock: parking_lot::Mutex<Option<RendererClock>>,
167 presentation: parking_lot::Mutex<Option<super::backend::Presentation>>,
170}
171
172impl std::fmt::Debug for PlaybackTimeline {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 f.debug_struct("PlaybackTimeline")
175 .field("samples_played", &self.samples_played)
176 .finish_non_exhaustive()
177 }
178}
179
180impl PlaybackTimeline {
181 pub fn new() -> Arc<Self> {
182 Arc::new(Self {
183 boundaries: parking_lot::RwLock::new(Vec::new()),
184 samples_written: AtomicU64::new(0),
185 samples_played: Arc::new(AtomicU64::new(0)),
186 queued: parking_lot::Mutex::new(None),
187 clock: parking_lot::Mutex::new(None),
188 presentation: parking_lot::Mutex::new(None),
189 })
190 }
191
192 fn played(&self, bounds: &[TrackBoundary]) -> u64 {
194 let Some(clock) = *self.clock.lock() else {
195 let played = self.samples_played.load(Ordering::Acquire);
196 return match &*self.presentation.lock() {
197 Some(presented) => presented().min(played),
198 None => played,
199 };
200 };
201 let Some(first) = bounds.first() else {
202 return 0;
203 };
204 let per_second = first.output_rate as u64 * first.info.channels as u64;
205 let at = clock.now_ms() * per_second / 1000;
206 (at - at % (first.info.channels as u64).max(1))
208 .min(self.samples_written.load(Ordering::Acquire))
209 }
210
211 pub fn clock(&self) -> Option<RendererClock> {
213 *self.clock.lock()
214 }
215
216 pub fn set_clock(&self, clock: Option<RendererClock>) {
217 *self.clock.lock() = clock;
218 }
219
220 pub fn track_at(&self, samples: u64) -> Option<QueueItemId> {
222 let bounds = self.boundaries.read();
223 let idx = bounds.partition_point(|b| b.sample_offset <= samples);
224 Some(bounds.get(idx.checked_sub(1)?)?.id)
225 }
226
227 pub fn until_end(&self) -> Option<std::time::Duration> {
229 let bounds = self.boundaries.read();
230 let played = self.played(&bounds);
231 let last = bounds.last()?;
232 let per_second = last.output_rate as u64 * last.info.channels as u64;
233 if per_second == 0 {
234 return None;
235 }
236 let left = self
237 .samples_written
238 .load(Ordering::Acquire)
239 .saturating_sub(played);
240 Some(std::time::Duration::from_micros(
241 left.saturating_mul(1_000_000) / per_second,
242 ))
243 }
244
245 pub fn on_queued(&self, f: impl Fn() + Send + Sync + 'static) {
247 *self.queued.lock() = Some(Box::new(f));
248 }
249
250 pub fn playhead(&self) -> Option<Playhead> {
253 let bounds = self.boundaries.read();
254 let played = self.played(&bounds);
255 let boundary = bounds
256 .partition_point(|b| b.sample_offset <= played)
257 .checked_sub(1)?;
258 let current = bounds.get(boundary)?;
259 Some(Playhead {
260 boundary,
261 id: current.id,
262 position_ms: current.position_ms(played)?,
263 })
264 }
265
266 pub fn boundary_count(&self) -> usize {
268 self.boundaries.read().len()
269 }
270
271 pub fn position_in(&self, boundary: usize) -> Option<u64> {
274 let bounds = self.boundaries.read();
275 let played = self.played(&bounds);
276 let at = bounds
277 .get(boundary + 1)
278 .map_or(played, |next| played.min(next.sample_offset));
279 bounds.get(boundary)?.position_ms(at)
280 }
281
282 pub fn until_next_track(&self) -> Option<std::time::Duration> {
285 let bounds = self.boundaries.read();
286 let played = self.played(&bounds);
287 let idx = bounds.partition_point(|b| b.sample_offset <= played);
288 let next = bounds.get(idx)?;
289 let current = bounds.get(idx.checked_sub(1)?)?;
290 let per_second = current.output_rate as u64 * current.info.channels as u64;
291 if per_second == 0 {
292 return None;
293 }
294 let left = next.sample_offset - played;
295 Some(std::time::Duration::from_micros(
296 left.saturating_mul(1_000_000) / per_second,
297 ))
298 }
299
300 pub fn queued_after_playhead(&self) -> Vec<QueueItemId> {
304 let bounds = self.boundaries.read();
305 let played = self.played(&bounds);
306 let idx = bounds.partition_point(|b| b.sample_offset <= played);
307 bounds[idx..].iter().map(|b| b.id).collect()
308 }
309
310 pub fn writer(&self) -> TimelineWriter<'_> {
312 TimelineWriter { timeline: self }
313 }
314
315 pub fn reset(&self) {
317 self.boundaries.write().clear();
318 self.samples_written.store(0, Ordering::Relaxed);
319 self.samples_played.store(0, Ordering::Relaxed);
320 *self.clock.lock() = None;
321 *self.presentation.lock() = None;
322 }
323
324 pub fn set_presentation(&self, presentation: Option<super::backend::Presentation>) {
327 *self.presentation.lock() = presentation;
328 }
329
330 pub fn samples_played_counter(&self) -> Arc<AtomicU64> {
332 self.samples_played.clone()
333 }
334
335 pub fn current_playback(&self) -> Option<(QueueItemId, PathBuf, StreamInfo, u64)> {
343 let bounds = self.boundaries.read();
345
346 if bounds.is_empty() {
347 return None;
348 }
349
350 let played = self.played(&bounds);
354
355 let idx = bounds.partition_point(|b| b.sample_offset <= played);
359 let current = if idx > 0 {
360 &bounds[idx - 1]
361 } else {
362 return None;
363 };
364
365 let position_ms = current.position_ms(played)?;
366
367 Some((
368 current.id,
369 current.path.clone(),
370 current.info.clone(),
371 position_ms,
372 ))
373 }
374}
375
376pub struct TimelineWriter<'a> {
382 timeline: &'a PlaybackTimeline,
383}
384
385impl TimelineWriter<'_> {
386 fn samples_written(&self) -> u64 {
388 self.timeline.samples_written.load(Ordering::Relaxed)
389 }
390
391 fn push_boundary(&self, boundary: TrackBoundary) {
393 self.timeline.boundaries.write().push(boundary);
394 if let Some(queued) = self.timeline.queued.lock().as_ref() {
395 queued();
396 }
397 }
398
399 fn add_written(&self, count: u64) {
401 let mut bounds = self.timeline.boundaries.write();
402 self.timeline
403 .samples_written
404 .fetch_add(count, Ordering::Relaxed);
405 if let Some(last) = bounds.last_mut() {
407 last.samples_written += count;
408 }
409 }
410}
411
412pub struct SourceEntry {
421 pub id: QueueItemId,
422 pub path: PathBuf,
424 pub hint: Hint,
426 pub make_mss: Box<dyn FnOnce() -> std::io::Result<MediaSourceStream<'static>> + Send>,
428}
429
430impl SourceEntry {
431 pub fn from_file(id: QueueItemId, path: PathBuf) -> Self {
433 let ext = path
434 .extension()
435 .and_then(|e| e.to_str())
436 .unwrap_or("")
437 .to_string();
438 let path_clone = path.clone();
439 let mut hint = Hint::new();
440 if !ext.is_empty() {
441 hint.with_extension(&ext);
442 }
443 Self {
444 id,
445 path,
446 hint,
447 make_mss: Box::new(move || {
448 let file = File::open(&path_clone)?;
449 Ok(MediaSourceStream::new(Box::new(file), Default::default()))
450 }),
451 }
452 }
453}
454
455pub fn probe_source(mss: MediaSourceStream<'_>, hint: &Hint) -> Result<StreamInfo, DecodeError> {
461 probe_mss(mss, hint)
462}
463
464pub fn probe_file(path: &Path) -> Result<StreamInfo, DecodeError> {
466 let file_size = std::fs::metadata(path).ok().map(|m| m.len());
467 let file = File::open(path)?;
468 let mss = MediaSourceStream::new(Box::new(file), Default::default());
469 let mut hint = Hint::new();
470 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
471 hint.with_extension(ext);
472 }
473 let mut info = probe_mss(mss, &hint)?;
474 if info.bitrate_kbps.is_none()
477 && info.bit_depth.is_none()
478 && let Some(size) = file_size
479 && info.duration_ms > 0
480 {
481 info.bitrate_kbps = Some((size * 8 / info.duration_ms) as u32);
482 }
483 Ok(info)
484}
485
486fn probe_mss(mss: MediaSourceStream<'_>, hint: &Hint) -> Result<StreamInfo, DecodeError> {
488 let reader = symphonia::default::get_probe()
489 .probe(
490 hint,
491 mss,
492 FormatOptions::default(),
493 MetadataOptions::default(),
494 )
495 .map_err(|e| match e {
496 symphonia::core::errors::Error::IoError(io) => DecodeError::Io(io),
500 other => DecodeError::Decode(other.to_string()),
501 })?;
502
503 let track = reader
504 .default_track(TrackType::Audio)
505 .ok_or(DecodeError::NoTrack)?;
506 let codec_params = track
507 .codec_params
508 .as_ref()
509 .and_then(|p| p.audio())
510 .ok_or(DecodeError::NoTrack)?;
511 let is_opus = codec_params.codec == CODEC_ID_OPUS;
512 let sample_rate = if is_opus {
514 48000
515 } else {
516 codec_params.sample_rate.unwrap_or(44100)
517 };
518 let channels = codec_params
519 .channels
520 .as_ref()
521 .map(|c| c.count() as u16)
522 .unwrap_or(2);
523 let bit_depth = source_bit_depth(codec_params);
524 let duration_ms = track_duration_ms(&*reader, track, sample_rate);
525 let codec = codec_name(codec_params.codec);
526
527 let bitrate_kbps = estimate_bitrate_from_codec_params(codec_params);
532
533 Ok(StreamInfo {
534 codec,
535 sample_rate,
536 channels,
537 bit_depth,
538 bitrate_kbps,
539 duration_ms,
540 })
541}
542
543#[derive(Clone)]
549pub struct Processing {
550 pub rg_mode: ReplayGainMode,
551 pub pre_amp_db: f64,
552 pub dsp: Option<Arc<Setup>>,
554 pub depth_limit: Arc<std::sync::atomic::AtomicU16>,
557}
558
559impl Default for Processing {
560 fn default() -> Self {
561 Self {
562 rg_mode: ReplayGainMode::Off,
563 pre_amp_db: 0.0,
564 dsp: None,
565 depth_limit: Arc::default(),
566 }
567 }
568}
569
570impl Processing {
571 pub fn output_rate(&self, source: u32) -> u32 {
573 self.dsp.as_ref().map_or(source, |d| d.output_rate(source))
574 }
575}
576
577#[allow(clippy::too_many_arguments)]
587pub fn start_decode<N, F>(
588 first: SourceEntry,
589 producer: rtrb::Producer<f32>,
590 seek_ms: u64,
591 next_track: N,
592 timeline: Arc<PlaybackTimeline>,
593 viz_buffer: Option<Arc<VizBuffer>>,
594 processing: Processing,
595 on_finished: F,
596) -> Result<DecodeHandle, DecodeError>
597where
598 N: Fn() -> Option<SourceEntry> + Send + 'static,
599 F: FnOnce(&AtomicBool) + Send + 'static,
600{
601 let stop = Arc::new(AtomicBool::new(false));
602 let stop_clone = stop.clone();
603 let thread = thread::Builder::new()
604 .name("koan-decode".into())
605 .spawn(move || {
606 decode_queue_loop(
607 first,
608 producer,
609 &stop_clone,
610 seek_ms,
611 &next_track,
612 &timeline.writer(),
613 viz_buffer.as_deref(),
614 &processing,
615 );
616 if !stop_clone.load(Ordering::Relaxed) {
620 on_finished(&stop_clone);
621 }
622 })
623 .map_err(DecodeError::Io)?;
624
625 Ok(DecodeHandle {
626 stop,
627 thread: Some(thread),
628 })
629}
630
631const MAX_CONSECUTIVE_FAILURES: u32 = 32;
639
640#[allow(clippy::too_many_arguments)]
651fn decode_queue_loop<N>(
652 first: SourceEntry,
653 mut producer: rtrb::Producer<f32>,
654 stop: &AtomicBool,
655 initial_seek_ms: u64,
656 next_track: &N,
657 timeline: &TimelineWriter<'_>,
658 viz_buffer: Option<&VizBuffer>,
659 processing: &Processing,
660) where
661 N: Fn() -> Option<SourceEntry>,
662{
663 if let Some(viz) = viz_buffer {
666 viz.reset();
667 }
668
669 let mut pending = Some(first);
670 let mut seek_ms = initial_seek_ms;
671 let mut format: Option<PcmFormat> = None;
672 let mut failures: u32 = 0;
673 let mut chain: Option<Chain> = None;
674
675 while let Some(entry) = pending.take() {
676 if stop.load(Ordering::Relaxed) {
677 break;
678 }
679
680 let SourceEntry {
681 id,
682 path,
683 hint,
684 make_mss,
685 } = entry;
686
687 let outcome = make_mss().map_err(DecodeError::Io).and_then(|mss| {
688 decode_single(
689 id,
690 &path,
691 &hint,
692 mss,
693 &mut producer,
694 stop,
695 seek_ms,
696 timeline,
697 viz_buffer,
698 processing,
699 &mut chain,
700 format,
701 )
702 });
703
704 match outcome {
705 Ok(Decoded::Complete(decoded_format)) => {
706 format = Some(decoded_format);
707 failures = 0;
708 }
709 Ok(Decoded::FormatMismatch) => break,
710 Err(e) => {
711 if stop.load(Ordering::Relaxed) {
712 break;
713 }
714 failures += 1;
715 log::error!("skipping {}: {}", path.display(), e);
716 if failures >= MAX_CONSECUTIVE_FAILURES {
717 log::error!(
718 "{} sources failed in a row, decode thread giving up",
719 failures
720 );
721 break;
722 }
723 }
724 }
725 if stop.load(Ordering::Relaxed) {
728 break;
729 }
730
731 seek_ms = 0;
732 pending = (next_track)();
733 match pending {
734 Some(ref next) => log::info!("gapless transition → {}", next.path.display()),
735 None => log::info!("playlist exhausted, decode thread finishing"),
736 }
737 }
738
739 if let (Some(chain), Some(format)) = (chain.as_mut(), format)
741 && !stop.load(Ordering::Relaxed)
742 {
743 write_ring(&mut producer, chain.flush(), stop, viz_buffer, format);
744 }
745
746 wait_for_drain(&producer, stop, format);
747}
748
749fn write_ring(
757 producer: &mut rtrb::Producer<f32>,
758 samples: &[f32],
759 stop: &AtomicBool,
760 viz_buffer: Option<&VizBuffer>,
761 (sample_rate, channels): PcmFormat,
762) -> bool {
763 let mut offset = 0;
764 while offset < samples.len() {
765 if stop.load(Ordering::Relaxed) {
766 return false;
767 }
768
769 let slots = producer.slots();
770 if slots == 0 {
771 let half = producer.buffer().capacity() / 2;
778 thread::park_timeout(time_to_play(half, Some((sample_rate, channels))));
779 continue;
780 }
781
782 let chunk_size = slots.min(samples.len() - offset);
783 if let Ok(mut chunk) = producer.write_chunk_uninit(chunk_size) {
784 let to_write = &samples[offset..offset + chunk_size];
785 let (first, second) = chunk.as_mut_slices();
786 let first_len = first.len().min(to_write.len());
787 for (slot, &val) in first.iter_mut().zip(&to_write[..first_len]) {
788 slot.write(val);
789 }
790 if first_len < to_write.len() {
791 for (slot, &val) in second.iter_mut().zip(&to_write[first_len..]) {
792 slot.write(val);
793 }
794 }
795 unsafe { chunk.commit_all() };
799
800 if let Some(viz) = viz_buffer {
801 viz.push_samples(to_write, channels, sample_rate);
802 }
803
804 offset += chunk_size;
805 }
806 }
807 true
808}
809
810fn time_to_play(samples: usize, format: Option<PcmFormat>) -> std::time::Duration {
813 match format {
814 Some((rate, channels)) if rate > 0 && channels > 0 => std::time::Duration::from_micros(
815 samples as u64 * 1_000_000 / (rate as u64 * channels as u64),
816 ),
817 _ => std::time::Duration::from_millis(2),
818 }
819}
820
821fn wait_for_drain(producer: &rtrb::Producer<f32>, stop: &AtomicBool, format: Option<PcmFormat>) {
827 let capacity = producer.buffer().capacity();
828 while !stop.load(Ordering::Relaxed) && !producer.is_abandoned() {
829 let left = capacity.saturating_sub(producer.slots());
830 if left == 0 {
831 return;
832 }
833 thread::park_timeout(time_to_play(left, format));
834 }
835}
836
837type PcmFormat = (u32, u16);
845
846enum Decoded {
848 Complete(PcmFormat),
850 FormatMismatch,
853}
854
855#[allow(clippy::too_many_arguments)]
860fn decode_single(
861 queue_item_id: QueueItemId,
862 path: &Path,
863 hint: &Hint,
864 mss: MediaSourceStream<'_>,
865 producer: &mut rtrb::Producer<f32>,
866 stop: &AtomicBool,
867 seek_ms: u64,
868 timeline: &TimelineWriter<'_>,
869 viz_buffer: Option<&VizBuffer>,
870 processing: &Processing,
871 chain: &mut Option<Chain>,
872 expected: Option<PcmFormat>,
873) -> Result<Decoded, DecodeError> {
874 let mut reader = symphonia::default::get_probe()
875 .probe(
876 hint,
877 mss,
878 FormatOptions::default(),
879 MetadataOptions::default(),
880 )
881 .map_err(|e| DecodeError::Decode(e.to_string()))?;
882
883 let track = reader
884 .default_track(TrackType::Audio)
885 .ok_or(DecodeError::NoTrack)?;
886 let track_id = track.id;
887 let time_base = track.time_base;
888 let codec_params = track
889 .codec_params
890 .as_ref()
891 .and_then(|p| p.audio())
892 .ok_or(DecodeError::NoTrack)?;
893 let is_opus_codec = codec_params.codec == CODEC_ID_OPUS;
894
895 let sample_rate = if is_opus_codec {
897 48000
898 } else {
899 codec_params.sample_rate.unwrap_or(44100)
900 };
901 let channels = codec_params
902 .channels
903 .as_ref()
904 .map(|c| c.count() as u16)
905 .unwrap_or(2);
906
907 let duration_ms = track_duration_ms(&*reader, track, sample_rate);
908
909 let mut bitrate_kbps = estimate_bitrate_from_codec_params(codec_params);
911 if bitrate_kbps.is_none()
912 && is_opus_codec
913 && let Ok(meta) = std::fs::metadata(path)
914 && duration_ms > 0
915 {
916 bitrate_kbps = Some((meta.len() * 8 / duration_ms) as u32);
917 }
918
919 let info = StreamInfo {
920 codec: codec_name(codec_params.codec),
921 sample_rate,
922 channels,
923 bit_depth: source_bit_depth(codec_params),
924 bitrate_kbps,
925 duration_ms,
926 };
927
928 let out_rate = processing.output_rate(sample_rate);
932 let format = (out_rate, channels);
933 if let Some(expected) = expected
934 && expected != format
935 {
936 log::info!(
937 "format change at {}: {}Hz/{}ch → {}Hz/{}ch, restarting audio engine",
938 path.display(),
939 expected.0,
940 expected.1,
941 out_rate,
942 channels
943 );
944 return Ok(Decoded::FormatMismatch);
945 }
946 let limit = processing.depth_limit.load(Ordering::Relaxed);
947 if expected.is_some() && limit > 0 && info.bit_depth.unwrap_or(24) > limit {
948 log::info!(
949 "bit depth change at {}: {} bits through an output of {limit}, restarting audio engine",
950 path.display(),
951 info.bit_depth.unwrap_or(24)
952 );
953 return Ok(Decoded::FormatMismatch);
954 }
955
956 let mut symphonia_decoder = if is_opus_codec {
958 None
959 } else {
960 Some(
961 symphonia::default::get_codecs()
962 .make_audio_decoder(codec_params, &AudioDecoderOptions::default())
963 .map_err(|_| DecodeError::UnsupportedCodec)?,
964 )
965 };
966 let mut opus_bridge = if is_opus_codec {
967 Some(OpusBridge::new(codec_params).map_err(|e| DecodeError::Decode(e.to_string()))?)
968 } else {
969 None
970 };
971
972 let mut seek_samples = 0;
987 let mut discard = 0usize;
988 if seek_ms > 0 {
989 let preroll = if is_opus_codec { OPUS_PREROLL_MS } else { 0 };
990 let seeked = reader
991 .seek(
992 SeekMode::Accurate,
993 SeekTo::Time {
994 time: Time::from_millis_u64(seek_ms.saturating_sub(preroll)),
995 track_id: Some(track_id),
996 },
997 )
998 .map_err(|e| DecodeError::Decode(format!("seek failed: {}", e)))?;
999 seek_samples = landing_samples(time_base, seeked.actual_ts, sample_rate, channels)
1000 .unwrap_or(
1001 seek_ms.saturating_sub(preroll) * sample_rate as u64 * channels as u64 / 1000,
1002 );
1003 let landed_at_start = seek_samples == 0;
1004 if preroll > 0 {
1005 let wanted = seek_ms * sample_rate as u64 / 1000 * channels as u64;
1006 discard = wanted.saturating_sub(seek_samples) as usize;
1007 seek_samples += discard as u64;
1008 }
1009 if let Some(ref mut dec) = symphonia_decoder {
1010 dec.reset();
1011 }
1012 if let Some(ref mut opus) = opus_bridge {
1013 opus.reset(landed_at_start);
1014 }
1015 }
1016
1017 if let Some(setup) = &processing.dsp {
1021 match chain {
1022 Some(c) => {
1023 let tail = c.set_source_rate(sample_rate);
1024 if !write_ring(producer, tail, stop, viz_buffer, format) {
1025 return Ok(Decoded::Complete(format));
1026 }
1027 }
1028 None => *chain = Some(Chain::new(setup, sample_rate, channels)),
1029 }
1030 }
1031
1032 let write_offset = timeline.samples_written();
1034 timeline.push_boundary(TrackBoundary {
1035 id: queue_item_id,
1036 path: path.to_path_buf(),
1037 info,
1038 sample_offset: write_offset,
1039 samples_written: 0,
1040 seek_samples,
1041 output_rate: out_rate,
1042 });
1043
1044 let rg_mode = processing.rg_mode;
1046 let pre_amp_db = processing.pre_amp_db;
1047 let rg_gain = if rg_mode != ReplayGainMode::Off {
1048 match crate::audio::replaygain::read_tags(path) {
1049 Ok(rg_info) => {
1050 let selected = crate::audio::replaygain::select_gain(&rg_info, rg_mode);
1051 if let Some((gain_db, _)) = selected {
1052 log::info!(
1053 "replaygain: applying {:.2} dB ({:?}) to {}",
1054 gain_db,
1055 rg_mode,
1056 path.display()
1057 );
1058 }
1059 selected
1060 }
1061 Err(e) => {
1062 log::debug!("replaygain: no tags for {}: {}", path.display(), e);
1063 None
1064 }
1065 }
1066 } else {
1067 None
1068 };
1069 let mut rg_scratch: Vec<f32> = Vec::new();
1070
1071 let mut sample_buf: Vec<f32> = Vec::new();
1072
1073 loop {
1074 if stop.load(Ordering::Relaxed) {
1075 return Ok(Decoded::Complete(format));
1076 }
1077
1078 let packet = match reader.next_packet() {
1079 Ok(Some(p)) => p,
1080 Ok(None) => return Ok(Decoded::Complete(format)),
1081 Err(e) => return Err(DecodeError::Decode(e.to_string())),
1082 };
1083
1084 if packet.track_id != track_id {
1085 continue;
1086 }
1087
1088 let samples: &[f32] = if let Some(ref mut opus) = opus_bridge {
1090 match opus.decode_packet(&packet.data) {
1091 Ok(s) => s,
1092 Err(e) => {
1093 log::warn!("opus decode error (skipping packet): {}", e);
1094 continue;
1095 }
1096 }
1097 } else {
1098 let decoder = symphonia_decoder.as_mut().unwrap();
1099 let decoded = match decoder.decode(&packet) {
1100 Ok(d) => d,
1101 Err(symphonia::core::errors::Error::DecodeError(e)) => {
1102 log::warn!("decode error (skipping packet): {}", e);
1103 continue;
1104 }
1105 Err(e) => return Err(DecodeError::Decode(e.to_string())),
1106 };
1107
1108 let spec = decoded.spec();
1109 let (decoded_rate, decoded_channels) = (spec.rate(), spec.channels().count() as u16);
1110 if (decoded_rate, decoded_channels) != (sample_rate, channels) {
1114 log::warn!(
1115 "{}: decoded {}Hz/{}ch but stream declares {}Hz/{}ch, restarting audio engine",
1116 path.display(),
1117 decoded_rate,
1118 decoded_channels,
1119 sample_rate,
1120 channels
1121 );
1122 return Ok(Decoded::FormatMismatch);
1123 }
1124 decoded.copy_to_vec_interleaved(&mut sample_buf);
1125 &sample_buf[..]
1126 };
1127
1128 let samples = if discard > 0 {
1129 let dropped = discard.min(samples.len());
1130 discard -= dropped;
1131 &samples[dropped..]
1132 } else {
1133 samples
1134 };
1135 if samples.is_empty() {
1136 continue;
1137 }
1138
1139 let samples = if let Some((gain_db, peak)) = rg_gain {
1142 rg_scratch.clear();
1143 rg_scratch.extend_from_slice(samples);
1144 crate::audio::replaygain::apply_gain(&mut rg_scratch, gain_db, peak, pre_amp_db);
1145 &rg_scratch[..]
1146 } else {
1147 samples
1148 };
1149
1150 let (samples, length) = match chain.as_mut() {
1151 Some(c) => c.process(samples),
1152 None => (samples, samples.len() as u64),
1153 };
1154 if !write_ring(producer, samples, stop, viz_buffer, format) {
1155 return Ok(Decoded::Complete(format));
1156 }
1157 timeline.add_written(length);
1158 }
1159}
1160
1161const OPUS_PREROLL_MS: u64 = 80;
1164
1165fn landing_samples(
1171 time_base: Option<TimeBase>,
1172 actual_ts: Timestamp,
1173 sample_rate: u32,
1174 channels: u16,
1175) -> Option<u64> {
1176 let (seconds, nanos) = time_base?.calc_time(actual_ts)?.parts();
1177 let rate = sample_rate as u64;
1178 let frames = seconds.max(0) as u64 * rate + (nanos as u64 * rate) / 1_000_000_000;
1179 Some(frames * channels as u64)
1180}
1181
1182pub(crate) fn track_duration_ms(
1189 reader: &(impl FormatReader + ?Sized),
1190 track: &Track,
1191 sample_rate: u32,
1192) -> u64 {
1193 fn to_ms(time_base: Option<TimeBase>, duration: Option<Duration>) -> Option<u64> {
1194 let time = time_base?.calc_duration(duration?)?;
1195 Some(time.as_millis().max(0) as u64)
1196 }
1197
1198 let media = reader.media_info();
1199 to_ms(track.time_base, track.duration)
1200 .or_else(|| to_ms(media.time_base, media.duration))
1201 .or_else(|| {
1202 track
1203 .num_frames
1204 .map(|frames| frames * 1000 / sample_rate as u64)
1205 })
1206 .unwrap_or(0)
1207}
1208
1209fn estimate_bitrate_from_codec_params(params: &AudioCodecParameters) -> Option<u32> {
1215 let is_lossy = matches!(
1216 params.codec,
1217 CODEC_ID_MP3 | CODEC_ID_AAC | CODEC_ID_VORBIS | CODEC_ID_OPUS
1218 );
1219 if !is_lossy {
1220 return None;
1221 }
1222
1223 let bpcs = params.bits_per_coded_sample?;
1226 let sr = params.sample_rate?;
1227 let channels = params
1228 .channels
1229 .as_ref()
1230 .map(|c| c.count() as u32)
1231 .unwrap_or(2);
1232 Some(bpcs * sr * channels / 1000)
1233}
1234
1235pub fn source_bit_depth(params: &AudioCodecParameters) -> Option<u16> {
1239 if params.codec == CODEC_ID_OPUS {
1240 return None;
1241 }
1242 if let Some(bits) = params.bits_per_sample {
1243 return Some(bits as u16);
1244 }
1245 if params.codec == CODEC_ID_ALAC {
1246 let cookie = params.extra_data.as_deref()?;
1247 return MagicCookie::read(cookie)
1248 .ok()
1249 .map(|c| u16::from(c.bit_depth));
1250 }
1251 None
1252}
1253
1254pub fn codec_name(codec: AudioCodecId) -> String {
1255 match codec {
1256 CODEC_ID_FLAC => "FLAC",
1257 CODEC_ID_MP3 => "MP3",
1258 CODEC_ID_AAC => "AAC",
1259 CODEC_ID_VORBIS => "Vorbis",
1260 CODEC_ID_OPUS => "Opus",
1261 CODEC_ID_ALAC => "ALAC",
1262 CODEC_ID_PCM_S16LE => "PCM/16",
1263 CODEC_ID_PCM_S24LE => "PCM/24",
1264 CODEC_ID_PCM_S32LE => "PCM/32",
1265 CODEC_ID_PCM_F32LE => "PCM/f32",
1266 other => return format!("Unknown({:?})", other),
1267 }
1268 .to_string()
1269}
1270
1271#[cfg(test)]
1272mod tests {
1273 use std::path::PathBuf;
1274 use std::sync::atomic::Ordering;
1275
1276 use super::*;
1277 use crate::player::state::QueueItemId;
1278
1279 fn make_info(sample_rate: u32, channels: u16) -> StreamInfo {
1280 StreamInfo {
1281 codec: "FLAC".to_string(),
1282 sample_rate,
1283 channels,
1284 bit_depth: Some(16),
1285 bitrate_kbps: None,
1286 duration_ms: 10_000,
1287 }
1288 }
1289
1290 fn make_boundary(
1291 id: QueueItemId,
1292 sample_offset: u64,
1293 seek_samples: u64,
1294 channels: u16,
1295 sample_rate: u32,
1296 ) -> TrackBoundary {
1297 TrackBoundary {
1298 id,
1299 path: PathBuf::from("/music/track.flac"),
1300 info: make_info(sample_rate, channels),
1301 sample_offset,
1302 samples_written: 0,
1303 seek_samples,
1304 output_rate: sample_rate,
1305 }
1306 }
1307
1308 fn writer(timeline: &PlaybackTimeline) -> TimelineWriter<'_> {
1309 timeline.writer()
1310 }
1311
1312 #[test]
1315 fn test_timeline_single_track() {
1316 let timeline = PlaybackTimeline::new();
1320 let tl = writer(&timeline);
1321 let id = QueueItemId::new();
1322 tl.push_boundary(make_boundary(id, 0, 0, 2, 44100));
1324 tl.add_written(88200); timeline.samples_played.store(88200, Ordering::Relaxed);
1328
1329 let result = timeline.current_playback();
1330 assert!(
1331 result.is_some(),
1332 "expected Some for single track with samples played"
1333 );
1334 let (result_id, _path, _info, position_ms) = result.unwrap();
1335 assert_eq!(result_id, id);
1336 assert_eq!(
1337 position_ms, 1000,
1338 "1 second of 44100 Hz stereo should be 1000 ms"
1339 );
1340 }
1341
1342 #[test]
1343 fn test_timeline_gapless_transition() {
1344 let timeline = PlaybackTimeline::new();
1348 let tl = writer(&timeline);
1349 let id1 = QueueItemId::new();
1350 let id2 = QueueItemId::new();
1351
1352 tl.push_boundary(make_boundary(id1, 0, 0, 2, 44100));
1354 tl.add_written(88200);
1355
1356 tl.push_boundary(make_boundary(id2, 88200, 0, 2, 44100));
1358 tl.add_written(44100); timeline.samples_played.store(90000, Ordering::Relaxed);
1362
1363 let result = timeline.current_playback();
1364 assert!(result.is_some());
1365 let (result_id, _path, _info, position_ms) = result.unwrap();
1366 assert_eq!(
1367 result_id, id2,
1368 "playback head past boundary should report second track"
1369 );
1370 assert_eq!(position_ms, 20, "position within track 2 should be ~20 ms");
1372 }
1373
1374 #[test]
1375 fn test_timeline_zero_samples() {
1376 let timeline = PlaybackTimeline::new();
1379 let tl = writer(&timeline);
1380 let id = QueueItemId::new();
1381 tl.push_boundary(make_boundary(id, 0, 0, 2, 44100));
1382 tl.add_written(1000);
1383 timeline.samples_played.store(0, Ordering::Relaxed);
1384
1385 let result = timeline.current_playback();
1386 assert!(
1387 result.is_some(),
1388 "expected Some at 0 samples played with a boundary at offset 0"
1389 );
1390 let (result_id, _path, _info, position_ms) = result.unwrap();
1391 assert_eq!(result_id, id);
1392 assert_eq!(position_ms, 0);
1393 }
1394
1395 #[test]
1396 fn test_timeline_past_all_boundaries() {
1397 let timeline = PlaybackTimeline::new();
1400 let tl = writer(&timeline);
1401 let id1 = QueueItemId::new();
1402 let id2 = QueueItemId::new();
1403
1404 tl.push_boundary(make_boundary(id1, 0, 0, 2, 44100));
1405 tl.add_written(88200);
1406 tl.push_boundary(make_boundary(id2, 88200, 0, 2, 44100));
1407 tl.add_written(88200);
1408
1409 timeline
1411 .samples_played
1412 .store(999_999_999, Ordering::Relaxed);
1413
1414 let result = timeline.current_playback();
1415 assert!(result.is_some());
1416 let (result_id, _path, _info, _position_ms) = result.unwrap();
1417 assert_eq!(
1418 result_id, id2,
1419 "samples past all boundaries should report the last track"
1420 );
1421 }
1422
1423 #[test]
1424 fn test_timeline_seek_offset() {
1425 let timeline = PlaybackTimeline::new();
1429 let tl = writer(&timeline);
1430 let id = QueueItemId::new();
1431 let seek_samples = 88200u64; tl.push_boundary(make_boundary(id, 0, seek_samples, 2, 44100));
1433 tl.add_written(44100); timeline.samples_played.store(0, Ordering::Relaxed);
1436
1437 let result = timeline.current_playback();
1438 assert!(result.is_some());
1439 let (_result_id, _path, _info, position_ms) = result.unwrap();
1440 assert_eq!(
1442 position_ms, 1000,
1443 "position should include seek offset of 1000 ms"
1444 );
1445 }
1446
1447 #[test]
1448 fn the_next_track_is_due_when_the_current_one_has_played_out() {
1449 let timeline = PlaybackTimeline::new();
1450 let tl = writer(&timeline);
1451 let (a, b) = (QueueItemId::new(), QueueItemId::new());
1452 tl.push_boundary(make_boundary(a, 0, 0, 2, 44100));
1454 assert_eq!(timeline.until_next_track(), None, "nothing queued after it");
1455 tl.push_boundary(make_boundary(b, 88200, 0, 2, 44100));
1456
1457 timeline.samples_played.store(44100, Ordering::Relaxed);
1458 assert_eq!(
1459 timeline.until_next_track(),
1460 Some(std::time::Duration::from_millis(500))
1461 );
1462 timeline.samples_played.store(88200, Ordering::Relaxed);
1463 assert_eq!(timeline.until_next_track(), None, "b is playing now");
1464 }
1465
1466 #[test]
1467 fn the_queued_callback_hears_each_track() {
1468 let timeline = PlaybackTimeline::new();
1469 let heard = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1470 let counter = heard.clone();
1471 timeline.on_queued(move || {
1472 counter.fetch_add(1, Ordering::Relaxed);
1473 });
1474 let tl = writer(&timeline);
1475 tl.push_boundary(make_boundary(QueueItemId::new(), 0, 0, 2, 44100));
1476 tl.push_boundary(make_boundary(QueueItemId::new(), 88200, 0, 2, 44100));
1477 assert_eq!(heard.load(Ordering::Relaxed), 2);
1478 }
1479
1480 #[test]
1481 fn a_track_played_past_reads_as_played_to_its_end() {
1482 let timeline = PlaybackTimeline::new();
1483 let tl = writer(&timeline);
1484 let (a, b) = (QueueItemId::new(), QueueItemId::new());
1485 tl.push_boundary(make_boundary(a, 0, 0, 2, 44100));
1486 tl.push_boundary(make_boundary(b, 88200, 0, 2, 44100));
1487 timeline
1488 .samples_played
1489 .store(88200 + 44100, Ordering::Relaxed);
1490
1491 assert_eq!(timeline.position_in(0), Some(1000));
1492 assert_eq!(timeline.position_in(1), Some(500));
1493 assert_eq!(
1494 timeline.playhead(),
1495 Some(Playhead {
1496 boundary: 1,
1497 id: b,
1498 position_ms: 500
1499 })
1500 );
1501 }
1502
1503 #[test]
1504 fn an_item_queued_twice_is_two_plays() {
1505 let timeline = PlaybackTimeline::new();
1506 let tl = writer(&timeline);
1507 let a = QueueItemId::new();
1508 tl.push_boundary(make_boundary(a, 0, 0, 2, 44100));
1509 tl.push_boundary(make_boundary(a, 88200, 0, 2, 44100));
1510 timeline
1511 .samples_played
1512 .store(88200 + 44100, Ordering::Relaxed);
1513
1514 assert_eq!(timeline.position_in(0), Some(1000), "the first pass ended");
1515 assert_eq!(timeline.position_in(1), Some(500));
1516 assert_eq!(timeline.playhead().map(|p| p.boundary), Some(1));
1517 }
1518
1519 #[test]
1520 fn an_engine_presentation_reads_the_playhead_between_callbacks() {
1521 let timeline = PlaybackTimeline::new();
1522 let tl = writer(&timeline);
1523 tl.push_boundary(make_boundary(QueueItemId::new(), 0, 0, 2, 44100));
1524 tl.add_written(176_400);
1525 timeline.samples_played.store(88_200, Ordering::Relaxed);
1526 timeline.set_presentation(Some(Arc::new(|| 66_150)));
1527 assert_eq!(timeline.playhead().unwrap().position_ms, 750);
1528
1529 timeline.set_presentation(Some(Arc::new(|| 1_000_000)));
1531 assert_eq!(timeline.playhead().unwrap().position_ms, 1000);
1532
1533 timeline.set_presentation(None);
1534 assert_eq!(timeline.playhead().unwrap().position_ms, 1000);
1535 }
1536
1537 #[test]
1538 fn test_timeline_reset() {
1539 let timeline = PlaybackTimeline::new();
1541 let tl = writer(&timeline);
1542 let id = QueueItemId::new();
1543 tl.push_boundary(make_boundary(id, 0, 0, 2, 44100));
1544 tl.add_written(88200);
1545 timeline.samples_played.store(44100, Ordering::Relaxed);
1546
1547 assert!(timeline.current_playback().is_some());
1549
1550 timeline.reset();
1551
1552 assert!(
1553 timeline.current_playback().is_none(),
1554 "after reset, current_playback should return None"
1555 );
1556 assert_eq!(
1557 timeline.samples_played.load(Ordering::Relaxed),
1558 0,
1559 "samples_played should be 0 after reset"
1560 );
1561 assert_eq!(
1562 timeline.samples_written.load(Ordering::Relaxed),
1563 0,
1564 "samples_written should be 0 after reset"
1565 );
1566 }
1567
1568 #[test]
1571 fn probe_file_extracts_stream_info() {
1572 let dir = tempfile::tempdir().unwrap();
1573 let wav_path = dir.path().join("probe_test.wav");
1574 crate::test_utils::generate_wav(&wav_path, 44100, 2, 1.0, 16);
1575
1576 let info = probe_file(&wav_path).expect("probe_file should succeed on a valid WAV");
1577 assert_eq!(info.sample_rate, 44100, "sample rate mismatch");
1578 assert_eq!(info.channels, 2, "channel count mismatch");
1579 assert_eq!(info.bit_depth, Some(16), "bit depth mismatch");
1580 assert!(
1581 info.duration_ms > 900 && info.duration_ms < 1100,
1582 "duration should be ~1000ms, got {}",
1583 info.duration_ms
1584 );
1585 assert!(
1586 info.codec.contains("PCM"),
1587 "codec should be PCM variant, got {}",
1588 info.codec
1589 );
1590 }
1591
1592 #[test]
1593 fn probe_file_reads_alac_bit_depth_from_the_magic_cookie() {
1594 let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/audio/testdata/24-bit-96k.m4a");
1595 let info = probe_file(&path).expect("probe_file should succeed on ALAC in MP4");
1596 assert_eq!(info.codec, "ALAC");
1597 assert_eq!(info.sample_rate, 96000);
1598 assert_eq!(info.bit_depth, Some(24));
1599 }
1600
1601 #[test]
1602 fn decode_single_produces_samples() {
1603 let dir = tempfile::tempdir().unwrap();
1604 let wav_path = dir.path().join("tone.wav");
1605 crate::test_utils::generate_wav_tone(&wav_path, 44100, 440.0, 0.1);
1607
1608 let (mut producer, mut consumer) = rtrb::RingBuffer::new(44100 * 2);
1610
1611 let timeline = PlaybackTimeline::new();
1612 let tl = writer(&timeline);
1613 let stop = Arc::new(AtomicBool::new(false));
1614
1615 let id = QueueItemId::new();
1616 let entry = SourceEntry::from_file(id, wav_path.clone());
1617 let hint = entry.hint.clone();
1618 let mss = (entry.make_mss)().expect("should open WAV file");
1619
1620 let result = decode_single(
1621 id,
1622 &wav_path,
1623 &hint,
1624 mss,
1625 &mut producer,
1626 &stop,
1627 0,
1628 &tl,
1629 None,
1630 &Processing::default(),
1631 &mut None,
1632 None,
1633 );
1634 assert!(
1635 matches!(result, Ok(Decoded::Complete((44100, 1)))),
1636 "decode_single should complete at the source format"
1637 );
1638
1639 let available = consumer.slots();
1641 assert!(available > 0, "expected samples in ring buffer, got 0");
1642
1643 let mut found_nonzero = false;
1645 while consumer.slots() > 0 {
1646 if let Ok(chunk) = consumer.read_chunk(consumer.slots().min(1024)) {
1647 let (first, second) = chunk.as_slices();
1648 for &s in first.iter().chain(second.iter()) {
1649 if s.abs() > 0.001 {
1650 found_nonzero = true;
1651 break;
1652 }
1653 }
1654 chunk.commit_all();
1655 }
1656 if found_nonzero {
1657 break;
1658 }
1659 }
1660 assert!(
1661 found_nonzero,
1662 "expected non-zero samples from 440Hz sine decode"
1663 );
1664 }
1665
1666 fn run_queue(paths: &[PathBuf]) -> Vec<TrackBoundary> {
1672 run_queue_with(paths, &Processing::default()).0
1673 }
1674
1675 fn run_queue_with(
1678 paths: &[PathBuf],
1679 processing: &Processing,
1680 ) -> (Vec<TrackBoundary>, u64, u64) {
1681 let (producer, mut consumer) = rtrb::RingBuffer::new(1 << 16);
1682 let timeline = PlaybackTimeline::new();
1683 let tl = writer(&timeline);
1684 let stop = Arc::new(AtomicBool::new(false));
1685
1686 let drain_stop = Arc::new(AtomicBool::new(false));
1687 let drain_flag = drain_stop.clone();
1688 let drainer = std::thread::spawn(move || {
1689 let mut read = 0u64;
1690 while !drain_flag.load(Ordering::Relaxed) {
1691 let n = consumer.slots();
1692 if n > 0
1693 && let Ok(chunk) = consumer.read_chunk(n)
1694 {
1695 read += n as u64;
1696 chunk.commit_all();
1697 }
1698 std::thread::sleep(std::time::Duration::from_micros(200));
1699 }
1700 read
1701 });
1702
1703 let rest: std::sync::Mutex<Vec<PathBuf>> = std::sync::Mutex::new(paths[1..].to_vec());
1704 let next_track = move || {
1705 let mut rest = rest.lock().ok()?;
1706 if rest.is_empty() {
1707 return None;
1708 }
1709 Some(SourceEntry::from_file(QueueItemId::new(), rest.remove(0)))
1710 };
1711
1712 decode_queue_loop(
1713 SourceEntry::from_file(QueueItemId::new(), paths[0].clone()),
1714 producer,
1715 &stop,
1716 0,
1717 &next_track,
1718 &tl,
1719 None,
1720 processing,
1721 );
1722
1723 drain_stop.store(true, Ordering::Relaxed);
1724 let read = drainer.join().unwrap();
1725
1726 let written = timeline.samples_written.load(Ordering::Relaxed);
1727 (timeline.boundaries.read().clone(), read, written)
1728 }
1729
1730 #[test]
1731 fn convolution_at_one_rate_plays_two_source_rates_gaplessly() {
1732 let dir = tempfile::tempdir().unwrap();
1733 let a = dir.path().join("a.wav");
1734 let b = dir.path().join("b.wav");
1735 crate::test_utils::generate_wav(&a, 44100, 2, 0.5, 16);
1736 crate::test_utils::generate_wav(&b, 96000, 2, 0.5, 16);
1737 let mut ir = vec![0.0; 129];
1738 ir[64] = 1.0;
1739 let processing = Processing {
1740 dsp: Some(Arc::new(Setup::new(
1741 vec![],
1742 vec![crate::audio::dsp::Impulse::from_channels(48000, vec![ir])],
1743 ))),
1744 ..Processing::default()
1745 };
1746
1747 let (bounds, read, written) = run_queue_with(&[a, b], &processing);
1748 assert_eq!(bounds.len(), 2, "both resample to 48 kHz, so one session");
1749 assert!(bounds.iter().all(|b| b.output_rate == 48000));
1750 assert_eq!(bounds[0].info.sample_rate, 44100);
1751 assert_eq!(bounds[1].info.sample_rate, 96000);
1752 assert_eq!(written, 2 * 24000 * 2);
1755 assert_eq!(read, written);
1756 assert_eq!(bounds[1].sample_offset, 24000 * 2);
1757 }
1758
1759 #[test]
1760 fn gapless_continues_when_format_matches() {
1761 let dir = tempfile::tempdir().unwrap();
1762 let a = dir.path().join("a.wav");
1763 let b = dir.path().join("b.wav");
1764 crate::test_utils::generate_wav(&a, 44100, 2, 0.1, 16);
1765 crate::test_utils::generate_wav(&b, 44100, 2, 0.1, 16);
1766
1767 let bounds = run_queue(&[a, b]);
1768 assert_eq!(
1769 bounds.len(),
1770 2,
1771 "same-format tracks should decode gaplessly"
1772 );
1773 }
1774
1775 #[test]
1776 fn gapless_stops_at_sample_rate_change() {
1777 let dir = tempfile::tempdir().unwrap();
1778 let a = dir.path().join("a.wav");
1779 let b = dir.path().join("b.wav");
1780 crate::test_utils::generate_wav(&a, 44100, 2, 0.1, 16);
1781 crate::test_utils::generate_wav(&b, 48000, 2, 0.1, 16);
1782
1783 let bounds = run_queue(&[a, b]);
1784 assert_eq!(
1785 bounds.len(),
1786 1,
1787 "a 48kHz track must not join a 44.1kHz ring buffer"
1788 );
1789 assert_eq!(bounds[0].info.sample_rate, 44100);
1790 }
1791
1792 #[test]
1793 fn a_track_deeper_than_the_output_ends_the_gapless_run() {
1794 let dir = tempfile::tempdir().unwrap();
1795 let a = dir.path().join("a.wav");
1796 let b = dir.path().join("b.wav");
1797 crate::test_utils::generate_wav(&a, 44100, 2, 0.1, 16);
1798 crate::test_utils::generate_wav(&b, 44100, 2, 0.1, 24);
1799
1800 let narrow = Processing::default();
1801 narrow.depth_limit.store(16, Ordering::Relaxed);
1802 let bounds = run_queue_with(&[a.clone(), b.clone()], &narrow).0;
1803 assert_eq!(bounds.len(), 1, "24 bits must not join a 16-bit output");
1804
1805 let wide = Processing::default();
1806 wide.depth_limit.store(24, Ordering::Relaxed);
1807 assert_eq!(run_queue_with(&[a.clone(), b.clone()], &wide).0.len(), 2);
1808
1809 let seeded = Processing::default();
1812 seeded.depth_limit.store(16, Ordering::Relaxed);
1813 let short = dir.path().join("short.wav");
1814 crate::test_utils::generate_wav(&short, 44100, 2, 0.01, 16);
1815 assert_eq!(run_queue_with(&[short, b], &seeded).0.len(), 1);
1816 }
1817
1818 #[test]
1819 fn gapless_stops_at_channel_change() {
1820 let dir = tempfile::tempdir().unwrap();
1821 let a = dir.path().join("a.wav");
1822 let b = dir.path().join("b.wav");
1823 crate::test_utils::generate_wav(&a, 44100, 2, 0.1, 16);
1824 crate::test_utils::generate_wav(&b, 44100, 1, 0.1, 16);
1825
1826 let bounds = run_queue(&[a, b]);
1827 assert_eq!(
1828 bounds.len(),
1829 1,
1830 "a mono track must not join a stereo ring buffer"
1831 );
1832 assert_eq!(bounds[0].info.channels, 2);
1833 }
1834
1835 #[test]
1836 fn drain_waits_for_the_consumer() {
1837 let (mut producer, mut consumer) = rtrb::RingBuffer::new(64);
1838 for _ in 0..64 {
1839 producer.push(0.0).unwrap();
1840 }
1841 let stop = Arc::new(AtomicBool::new(false));
1842
1843 let reader = std::thread::spawn(move || {
1844 std::thread::sleep(std::time::Duration::from_millis(20));
1845 let chunk = consumer.read_chunk(64).unwrap();
1846 chunk.commit_all();
1847 consumer
1848 });
1849
1850 wait_for_drain(&producer, &stop, None);
1851 assert_eq!(producer.slots(), 64, "drain must wait for an empty buffer");
1852 drop(reader.join().unwrap());
1853 }
1854
1855 #[test]
1856 fn drain_returns_when_playback_is_torn_down() {
1857 let (producer, consumer) = rtrb::RingBuffer::<f32>::new(64);
1858 let stop = Arc::new(AtomicBool::new(true));
1859 wait_for_drain(&producer, &stop, None);
1860 drop(consumer);
1861 }
1862
1863 fn write_garbage(path: &Path) {
1867 std::fs::write(path, b"this is not a wav file").unwrap();
1868 }
1869
1870 #[test]
1871 fn an_unreadable_track_is_skipped_and_the_queue_continues() {
1872 let dir = tempfile::tempdir().unwrap();
1873 let a = dir.path().join("a.wav");
1874 let bad = dir.path().join("bad.wav");
1875 let c = dir.path().join("c.wav");
1876 crate::test_utils::generate_wav(&a, 44100, 2, 0.1, 16);
1877 write_garbage(&bad);
1878 crate::test_utils::generate_wav(&c, 44100, 2, 0.1, 16);
1879
1880 let bounds = run_queue(&[a.clone(), bad, c.clone()]);
1881 let decoded: Vec<_> = bounds.iter().map(|b| b.path.clone()).collect();
1882 assert_eq!(
1883 decoded,
1884 vec![a, c],
1885 "one bad file must not take the rest of the queue with it"
1886 );
1887 }
1888
1889 #[test]
1890 fn a_missing_track_is_skipped_and_the_queue_continues() {
1891 let dir = tempfile::tempdir().unwrap();
1892 let missing = dir.path().join("gone.wav");
1893 let b = dir.path().join("b.wav");
1894 crate::test_utils::generate_wav(&b, 44100, 2, 0.1, 16);
1895
1896 let bounds = run_queue(&[missing, b.clone()]);
1897 assert_eq!(bounds.len(), 1);
1898 assert_eq!(
1899 bounds[0].path, b,
1900 "a bad first track must not end the session"
1901 );
1902 }
1903
1904 #[test]
1905 fn an_entirely_unreadable_queue_terminates() {
1906 let dir = tempfile::tempdir().unwrap();
1907 let bad = dir.path().join("bad.wav");
1908 write_garbage(&bad);
1909
1910 let (producer, _consumer) = rtrb::RingBuffer::<f32>::new(1 << 12);
1913 let timeline = PlaybackTimeline::new();
1914 let tl = writer(&timeline);
1915 let stop = Arc::new(AtomicBool::new(false));
1916
1917 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1918 let counter = calls.clone();
1919 let bad_path = bad.clone();
1920 let next_track = move || {
1921 counter.fetch_add(1, Ordering::Relaxed);
1922 Some(SourceEntry::from_file(QueueItemId::new(), bad_path.clone()))
1923 };
1924
1925 decode_queue_loop(
1926 SourceEntry::from_file(QueueItemId::new(), bad),
1927 producer,
1928 &stop,
1929 0,
1930 &next_track,
1931 &tl,
1932 None,
1933 &Processing::default(),
1934 );
1935
1936 assert_eq!(
1938 calls.load(Ordering::Relaxed) + 1,
1939 MAX_CONSECUTIVE_FAILURES as usize
1940 );
1941 assert!(timeline.boundaries.read().is_empty());
1942 }
1943
1944 #[test]
1947 fn landing_samples_converts_frame_timebases() {
1948 let tb = TimeBase::try_from_recip(44100).unwrap();
1950 assert_eq!(
1951 landing_samples(Some(tb), Timestamp::from(44100u32), 44100, 2),
1952 Some(88_200)
1953 );
1954 }
1955
1956 #[test]
1957 fn landing_samples_converts_millisecond_timebases() {
1958 let tb = TimeBase::try_new(1, 1000).unwrap();
1960 assert_eq!(
1961 landing_samples(Some(tb), Timestamp::from(1500u32), 48000, 2),
1962 Some(48000 * 3 / 2 * 2)
1963 );
1964 }
1965
1966 #[test]
1967 fn landing_samples_needs_a_timebase() {
1968 assert_eq!(
1969 landing_samples(None, Timestamp::from(1000u32), 44100, 2),
1970 None
1971 );
1972 }
1973
1974 #[cfg(test)]
1977 fn make_vbr_mp3(dir: &Path) -> PathBuf {
1978 let wav = dir.join("source.wav");
1979 let mp3 = dir.join("source.mp3");
1980 let ok = std::process::Command::new("sox")
1981 .args(["-n", "-r", "44100", "-c", "2"])
1982 .arg(&wav)
1983 .args([
1984 "synth", "30", "sine", "200", "vol", "0.02", ":", "synth", "270", "sine", "880",
1985 "vol", "0.9",
1986 ])
1987 .status()
1988 .expect("sox not installed")
1989 .success();
1990 assert!(ok, "sox failed");
1991 let ok = std::process::Command::new("lame")
1992 .args(["-V", "2", "--quiet"])
1993 .arg(&wav)
1994 .arg(&mp3)
1995 .status()
1996 .expect("lame not installed")
1997 .success();
1998 assert!(ok, "lame failed");
1999 mp3
2000 }
2001
2002 #[test]
2007 #[ignore = "generates a fixture with sox + lame; run with cargo test -- --ignored"]
2008 fn seek_on_vbr_reports_where_it_landed() {
2009 let dir = tempfile::tempdir().unwrap();
2010 let path = make_vbr_mp3(dir.path());
2011 let info = probe_file(&path).unwrap();
2012 let channels = info.channels as u64;
2013 let rate = info.sample_rate as u64;
2014
2015 let seek_ms = 150_000u64;
2016 let (mut producer, mut consumer) = rtrb::RingBuffer::<f32>::new(1 << 16);
2017 let stop = Arc::new(AtomicBool::new(false));
2018 let timeline = PlaybackTimeline::new();
2019 let tl = writer(&timeline);
2020
2021 let drain_stop = stop.clone();
2022 let drained = std::thread::spawn(move || {
2023 let mut total = 0u64;
2024 while !drain_stop.load(Ordering::Relaxed) {
2025 let slots = consumer.slots();
2026 if slots == 0 {
2027 std::thread::sleep(std::time::Duration::from_micros(200));
2028 continue;
2029 }
2030 let chunk = consumer.read_chunk(slots).unwrap();
2031 total += slots as u64;
2032 chunk.commit_all();
2033 }
2034 total
2035 });
2036
2037 let file = File::open(&path).unwrap();
2038 let mss = MediaSourceStream::new(Box::new(file), Default::default());
2039 let mut hint = Hint::new();
2040 hint.with_extension("mp3");
2041 decode_single(
2042 QueueItemId::new(),
2043 &path,
2044 &hint,
2045 mss,
2046 &mut producer,
2047 &stop,
2048 seek_ms,
2049 &tl,
2050 None,
2051 &Processing::default(),
2052 &mut None,
2053 None,
2054 )
2055 .unwrap();
2056
2057 let written = timeline.samples_written.load(Ordering::Relaxed);
2058 stop.store(true, Ordering::Relaxed);
2059 drained.join().unwrap();
2060
2061 let reported_start_ms = {
2062 let bounds = timeline.boundaries.read();
2063 (bounds[0].seek_samples / channels) * 1000 / rate
2064 };
2065 let decoded_ms = (written / channels) * 1000 / rate;
2066
2067 let total_ms = reported_start_ms + decoded_ms;
2069 assert!(
2070 total_ms.abs_diff(info.duration_ms) < 500,
2071 "reported start {}ms + {}ms decoded = {}ms, but the file is {}ms",
2072 reported_start_ms,
2073 decoded_ms,
2074 total_ms,
2075 info.duration_ms
2076 );
2077 assert!(
2079 reported_start_ms.abs_diff(seek_ms) < 100,
2080 "seek to {}ms reported {}ms",
2081 seek_ms,
2082 reported_start_ms
2083 );
2084 }
2085}