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}
168
169impl std::fmt::Debug for PlaybackTimeline {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 f.debug_struct("PlaybackTimeline")
172 .field("samples_played", &self.samples_played)
173 .finish_non_exhaustive()
174 }
175}
176
177impl PlaybackTimeline {
178 pub fn new() -> Arc<Self> {
179 Arc::new(Self {
180 boundaries: parking_lot::RwLock::new(Vec::new()),
181 samples_written: AtomicU64::new(0),
182 samples_played: Arc::new(AtomicU64::new(0)),
183 queued: parking_lot::Mutex::new(None),
184 clock: parking_lot::Mutex::new(None),
185 })
186 }
187
188 fn played(&self, bounds: &[TrackBoundary]) -> u64 {
190 let Some(clock) = *self.clock.lock() else {
191 return self.samples_played.load(Ordering::Acquire);
192 };
193 let Some(first) = bounds.first() else {
194 return 0;
195 };
196 let per_second = first.output_rate as u64 * first.info.channels as u64;
197 let at = clock.now_ms() * per_second / 1000;
198 (at - at % (first.info.channels as u64).max(1))
200 .min(self.samples_written.load(Ordering::Acquire))
201 }
202
203 pub fn clock(&self) -> Option<RendererClock> {
205 *self.clock.lock()
206 }
207
208 pub fn set_clock(&self, clock: Option<RendererClock>) {
209 *self.clock.lock() = clock;
210 }
211
212 pub fn track_at(&self, samples: u64) -> Option<QueueItemId> {
214 let bounds = self.boundaries.read();
215 let idx = bounds.partition_point(|b| b.sample_offset <= samples);
216 Some(bounds.get(idx.checked_sub(1)?)?.id)
217 }
218
219 pub fn until_end(&self) -> Option<std::time::Duration> {
221 let bounds = self.boundaries.read();
222 let played = self.played(&bounds);
223 let last = bounds.last()?;
224 let per_second = last.output_rate as u64 * last.info.channels as u64;
225 if per_second == 0 {
226 return None;
227 }
228 let left = self
229 .samples_written
230 .load(Ordering::Acquire)
231 .saturating_sub(played);
232 Some(std::time::Duration::from_micros(
233 left.saturating_mul(1_000_000) / per_second,
234 ))
235 }
236
237 pub fn on_queued(&self, f: impl Fn() + Send + Sync + 'static) {
239 *self.queued.lock() = Some(Box::new(f));
240 }
241
242 pub fn playhead(&self) -> Option<Playhead> {
245 let bounds = self.boundaries.read();
246 let played = self.played(&bounds);
247 let boundary = bounds
248 .partition_point(|b| b.sample_offset <= played)
249 .checked_sub(1)?;
250 let current = bounds.get(boundary)?;
251 Some(Playhead {
252 boundary,
253 id: current.id,
254 position_ms: current.position_ms(played)?,
255 })
256 }
257
258 pub fn boundary_count(&self) -> usize {
260 self.boundaries.read().len()
261 }
262
263 pub fn position_in(&self, boundary: usize) -> Option<u64> {
266 let bounds = self.boundaries.read();
267 let played = self.played(&bounds);
268 let at = bounds
269 .get(boundary + 1)
270 .map_or(played, |next| played.min(next.sample_offset));
271 bounds.get(boundary)?.position_ms(at)
272 }
273
274 pub fn until_next_track(&self) -> Option<std::time::Duration> {
277 let bounds = self.boundaries.read();
278 let played = self.played(&bounds);
279 let idx = bounds.partition_point(|b| b.sample_offset <= played);
280 let next = bounds.get(idx)?;
281 let current = bounds.get(idx.checked_sub(1)?)?;
282 let per_second = current.output_rate as u64 * current.info.channels as u64;
283 if per_second == 0 {
284 return None;
285 }
286 let left = next.sample_offset - played;
287 Some(std::time::Duration::from_micros(
288 left.saturating_mul(1_000_000) / per_second,
289 ))
290 }
291
292 pub fn queued_after_playhead(&self) -> Vec<QueueItemId> {
296 let bounds = self.boundaries.read();
297 let played = self.played(&bounds);
298 let idx = bounds.partition_point(|b| b.sample_offset <= played);
299 bounds[idx..].iter().map(|b| b.id).collect()
300 }
301
302 pub fn writer(&self) -> TimelineWriter<'_> {
304 TimelineWriter { timeline: self }
305 }
306
307 pub fn reset(&self) {
309 self.boundaries.write().clear();
310 self.samples_written.store(0, Ordering::Relaxed);
311 self.samples_played.store(0, Ordering::Relaxed);
312 *self.clock.lock() = None;
313 }
314
315 pub fn samples_played_counter(&self) -> Arc<AtomicU64> {
317 self.samples_played.clone()
318 }
319
320 pub fn current_playback(&self) -> Option<(QueueItemId, PathBuf, StreamInfo, u64)> {
328 let bounds = self.boundaries.read();
330
331 if bounds.is_empty() {
332 return None;
333 }
334
335 let played = self.played(&bounds);
339
340 let idx = bounds.partition_point(|b| b.sample_offset <= played);
344 let current = if idx > 0 {
345 &bounds[idx - 1]
346 } else {
347 return None;
348 };
349
350 let position_ms = current.position_ms(played)?;
351
352 Some((
353 current.id,
354 current.path.clone(),
355 current.info.clone(),
356 position_ms,
357 ))
358 }
359}
360
361pub struct TimelineWriter<'a> {
367 timeline: &'a PlaybackTimeline,
368}
369
370impl TimelineWriter<'_> {
371 fn samples_written(&self) -> u64 {
373 self.timeline.samples_written.load(Ordering::Relaxed)
374 }
375
376 fn push_boundary(&self, boundary: TrackBoundary) {
378 self.timeline.boundaries.write().push(boundary);
379 if let Some(queued) = self.timeline.queued.lock().as_ref() {
380 queued();
381 }
382 }
383
384 fn add_written(&self, count: u64) {
386 let mut bounds = self.timeline.boundaries.write();
387 self.timeline
388 .samples_written
389 .fetch_add(count, Ordering::Relaxed);
390 if let Some(last) = bounds.last_mut() {
392 last.samples_written += count;
393 }
394 }
395}
396
397pub struct SourceEntry {
406 pub id: QueueItemId,
407 pub path: PathBuf,
409 pub hint: Hint,
411 pub make_mss: Box<dyn FnOnce() -> std::io::Result<MediaSourceStream<'static>> + Send>,
413}
414
415impl SourceEntry {
416 pub fn from_file(id: QueueItemId, path: PathBuf) -> Self {
418 let ext = path
419 .extension()
420 .and_then(|e| e.to_str())
421 .unwrap_or("")
422 .to_string();
423 let path_clone = path.clone();
424 let mut hint = Hint::new();
425 if !ext.is_empty() {
426 hint.with_extension(&ext);
427 }
428 Self {
429 id,
430 path,
431 hint,
432 make_mss: Box::new(move || {
433 let file = File::open(&path_clone)?;
434 Ok(MediaSourceStream::new(Box::new(file), Default::default()))
435 }),
436 }
437 }
438}
439
440pub fn probe_source(mss: MediaSourceStream<'_>, hint: &Hint) -> Result<StreamInfo, DecodeError> {
446 probe_mss(mss, hint)
447}
448
449pub fn probe_file(path: &Path) -> Result<StreamInfo, DecodeError> {
451 let file_size = std::fs::metadata(path).ok().map(|m| m.len());
452 let file = File::open(path)?;
453 let mss = MediaSourceStream::new(Box::new(file), Default::default());
454 let mut hint = Hint::new();
455 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
456 hint.with_extension(ext);
457 }
458 let mut info = probe_mss(mss, &hint)?;
459 if info.bitrate_kbps.is_none()
462 && info.bit_depth.is_none()
463 && let Some(size) = file_size
464 && info.duration_ms > 0
465 {
466 info.bitrate_kbps = Some((size * 8 / info.duration_ms) as u32);
467 }
468 Ok(info)
469}
470
471fn probe_mss(mss: MediaSourceStream<'_>, hint: &Hint) -> Result<StreamInfo, DecodeError> {
473 let reader = symphonia::default::get_probe()
474 .probe(
475 hint,
476 mss,
477 FormatOptions::default(),
478 MetadataOptions::default(),
479 )
480 .map_err(|e| match e {
481 symphonia::core::errors::Error::IoError(io) => DecodeError::Io(io),
485 other => DecodeError::Decode(other.to_string()),
486 })?;
487
488 let track = reader
489 .default_track(TrackType::Audio)
490 .ok_or(DecodeError::NoTrack)?;
491 let codec_params = track
492 .codec_params
493 .as_ref()
494 .and_then(|p| p.audio())
495 .ok_or(DecodeError::NoTrack)?;
496 let is_opus = codec_params.codec == CODEC_ID_OPUS;
497 let sample_rate = if is_opus {
499 48000
500 } else {
501 codec_params.sample_rate.unwrap_or(44100)
502 };
503 let channels = codec_params
504 .channels
505 .as_ref()
506 .map(|c| c.count() as u16)
507 .unwrap_or(2);
508 let bit_depth = source_bit_depth(codec_params);
509 let duration_ms = track_duration_ms(&*reader, track, sample_rate);
510 let codec = codec_name(codec_params.codec);
511
512 let bitrate_kbps = estimate_bitrate_from_codec_params(codec_params);
517
518 Ok(StreamInfo {
519 codec,
520 sample_rate,
521 channels,
522 bit_depth,
523 bitrate_kbps,
524 duration_ms,
525 })
526}
527
528#[derive(Clone)]
534pub struct Processing {
535 pub rg_mode: ReplayGainMode,
536 pub pre_amp_db: f64,
537 pub dsp: Option<Arc<Setup>>,
539}
540
541impl Default for Processing {
542 fn default() -> Self {
543 Self {
544 rg_mode: ReplayGainMode::Off,
545 pre_amp_db: 0.0,
546 dsp: None,
547 }
548 }
549}
550
551impl Processing {
552 pub fn output_rate(&self, source: u32) -> u32 {
554 self.dsp.as_ref().map_or(source, |d| d.output_rate(source))
555 }
556}
557
558#[allow(clippy::too_many_arguments)]
568pub fn start_decode<N, F>(
569 first: SourceEntry,
570 producer: rtrb::Producer<f32>,
571 seek_ms: u64,
572 next_track: N,
573 timeline: Arc<PlaybackTimeline>,
574 viz_buffer: Option<Arc<VizBuffer>>,
575 processing: Processing,
576 on_finished: F,
577) -> Result<DecodeHandle, DecodeError>
578where
579 N: Fn() -> Option<SourceEntry> + Send + 'static,
580 F: FnOnce(&AtomicBool) + Send + 'static,
581{
582 let stop = Arc::new(AtomicBool::new(false));
583 let stop_clone = stop.clone();
584 let thread = thread::Builder::new()
585 .name("koan-decode".into())
586 .spawn(move || {
587 decode_queue_loop(
588 first,
589 producer,
590 &stop_clone,
591 seek_ms,
592 &next_track,
593 &timeline.writer(),
594 viz_buffer.as_deref(),
595 &processing,
596 );
597 if !stop_clone.load(Ordering::Relaxed) {
601 on_finished(&stop_clone);
602 }
603 })
604 .map_err(DecodeError::Io)?;
605
606 Ok(DecodeHandle {
607 stop,
608 thread: Some(thread),
609 })
610}
611
612const MAX_CONSECUTIVE_FAILURES: u32 = 32;
620
621#[allow(clippy::too_many_arguments)]
632fn decode_queue_loop<N>(
633 first: SourceEntry,
634 mut producer: rtrb::Producer<f32>,
635 stop: &AtomicBool,
636 initial_seek_ms: u64,
637 next_track: &N,
638 timeline: &TimelineWriter<'_>,
639 viz_buffer: Option<&VizBuffer>,
640 processing: &Processing,
641) where
642 N: Fn() -> Option<SourceEntry>,
643{
644 if let Some(viz) = viz_buffer {
647 viz.reset();
648 }
649
650 let mut pending = Some(first);
651 let mut seek_ms = initial_seek_ms;
652 let mut format: Option<PcmFormat> = None;
653 let mut failures: u32 = 0;
654 let mut chain: Option<Chain> = None;
655
656 while let Some(entry) = pending.take() {
657 if stop.load(Ordering::Relaxed) {
658 break;
659 }
660
661 let SourceEntry {
662 id,
663 path,
664 hint,
665 make_mss,
666 } = entry;
667
668 let outcome = make_mss().map_err(DecodeError::Io).and_then(|mss| {
669 decode_single(
670 id,
671 &path,
672 &hint,
673 mss,
674 &mut producer,
675 stop,
676 seek_ms,
677 timeline,
678 viz_buffer,
679 processing,
680 &mut chain,
681 format,
682 )
683 });
684
685 match outcome {
686 Ok(Decoded::Complete(decoded_format)) => {
687 format = Some(decoded_format);
688 failures = 0;
689 }
690 Ok(Decoded::FormatMismatch) => break,
691 Err(e) => {
692 if stop.load(Ordering::Relaxed) {
693 break;
694 }
695 failures += 1;
696 log::error!("skipping {}: {}", path.display(), e);
697 if failures >= MAX_CONSECUTIVE_FAILURES {
698 log::error!(
699 "{} sources failed in a row, decode thread giving up",
700 failures
701 );
702 break;
703 }
704 }
705 }
706 if stop.load(Ordering::Relaxed) {
709 break;
710 }
711
712 seek_ms = 0;
713 pending = (next_track)();
714 match pending {
715 Some(ref next) => log::info!("gapless transition → {}", next.path.display()),
716 None => log::info!("playlist exhausted, decode thread finishing"),
717 }
718 }
719
720 if let (Some(chain), Some(format)) = (chain.as_mut(), format)
722 && !stop.load(Ordering::Relaxed)
723 {
724 write_ring(&mut producer, chain.flush(), stop, viz_buffer, format);
725 }
726
727 wait_for_drain(&producer, stop, format);
728}
729
730fn write_ring(
738 producer: &mut rtrb::Producer<f32>,
739 samples: &[f32],
740 stop: &AtomicBool,
741 viz_buffer: Option<&VizBuffer>,
742 (sample_rate, channels): PcmFormat,
743) -> bool {
744 let mut offset = 0;
745 while offset < samples.len() {
746 if stop.load(Ordering::Relaxed) {
747 return false;
748 }
749
750 let slots = producer.slots();
751 if slots == 0 {
752 let half = producer.buffer().capacity() / 2;
759 thread::park_timeout(time_to_play(half, Some((sample_rate, channels))));
760 continue;
761 }
762
763 let chunk_size = slots.min(samples.len() - offset);
764 if let Ok(mut chunk) = producer.write_chunk_uninit(chunk_size) {
765 let to_write = &samples[offset..offset + chunk_size];
766 let (first, second) = chunk.as_mut_slices();
767 let first_len = first.len().min(to_write.len());
768 for (slot, &val) in first.iter_mut().zip(&to_write[..first_len]) {
769 slot.write(val);
770 }
771 if first_len < to_write.len() {
772 for (slot, &val) in second.iter_mut().zip(&to_write[first_len..]) {
773 slot.write(val);
774 }
775 }
776 unsafe { chunk.commit_all() };
780
781 if let Some(viz) = viz_buffer {
782 viz.push_samples(to_write, channels, sample_rate);
783 }
784
785 offset += chunk_size;
786 }
787 }
788 true
789}
790
791fn time_to_play(samples: usize, format: Option<PcmFormat>) -> std::time::Duration {
794 match format {
795 Some((rate, channels)) if rate > 0 && channels > 0 => std::time::Duration::from_micros(
796 samples as u64 * 1_000_000 / (rate as u64 * channels as u64),
797 ),
798 _ => std::time::Duration::from_millis(2),
799 }
800}
801
802fn wait_for_drain(producer: &rtrb::Producer<f32>, stop: &AtomicBool, format: Option<PcmFormat>) {
808 let capacity = producer.buffer().capacity();
809 while !stop.load(Ordering::Relaxed) && !producer.is_abandoned() {
810 let left = capacity.saturating_sub(producer.slots());
811 if left == 0 {
812 return;
813 }
814 thread::park_timeout(time_to_play(left, format));
815 }
816}
817
818type PcmFormat = (u32, u16);
826
827enum Decoded {
829 Complete(PcmFormat),
831 FormatMismatch,
834}
835
836#[allow(clippy::too_many_arguments)]
841fn decode_single(
842 queue_item_id: QueueItemId,
843 path: &Path,
844 hint: &Hint,
845 mss: MediaSourceStream<'_>,
846 producer: &mut rtrb::Producer<f32>,
847 stop: &AtomicBool,
848 seek_ms: u64,
849 timeline: &TimelineWriter<'_>,
850 viz_buffer: Option<&VizBuffer>,
851 processing: &Processing,
852 chain: &mut Option<Chain>,
853 expected: Option<PcmFormat>,
854) -> Result<Decoded, DecodeError> {
855 let mut reader = symphonia::default::get_probe()
856 .probe(
857 hint,
858 mss,
859 FormatOptions::default(),
860 MetadataOptions::default(),
861 )
862 .map_err(|e| DecodeError::Decode(e.to_string()))?;
863
864 let track = reader
865 .default_track(TrackType::Audio)
866 .ok_or(DecodeError::NoTrack)?;
867 let track_id = track.id;
868 let time_base = track.time_base;
869 let codec_params = track
870 .codec_params
871 .as_ref()
872 .and_then(|p| p.audio())
873 .ok_or(DecodeError::NoTrack)?;
874 let is_opus_codec = codec_params.codec == CODEC_ID_OPUS;
875
876 let sample_rate = if is_opus_codec {
878 48000
879 } else {
880 codec_params.sample_rate.unwrap_or(44100)
881 };
882 let channels = codec_params
883 .channels
884 .as_ref()
885 .map(|c| c.count() as u16)
886 .unwrap_or(2);
887
888 let duration_ms = track_duration_ms(&*reader, track, sample_rate);
889
890 let mut bitrate_kbps = estimate_bitrate_from_codec_params(codec_params);
892 if bitrate_kbps.is_none()
893 && is_opus_codec
894 && let Ok(meta) = std::fs::metadata(path)
895 && duration_ms > 0
896 {
897 bitrate_kbps = Some((meta.len() * 8 / duration_ms) as u32);
898 }
899
900 let info = StreamInfo {
901 codec: codec_name(codec_params.codec),
902 sample_rate,
903 channels,
904 bit_depth: source_bit_depth(codec_params),
905 bitrate_kbps,
906 duration_ms,
907 };
908
909 let out_rate = processing.output_rate(sample_rate);
913 let format = (out_rate, channels);
914 if let Some(expected) = expected
915 && expected != format
916 {
917 log::info!(
918 "format change at {}: {}Hz/{}ch → {}Hz/{}ch, restarting audio engine",
919 path.display(),
920 expected.0,
921 expected.1,
922 out_rate,
923 channels
924 );
925 return Ok(Decoded::FormatMismatch);
926 }
927
928 let mut symphonia_decoder = if is_opus_codec {
930 None
931 } else {
932 Some(
933 symphonia::default::get_codecs()
934 .make_audio_decoder(codec_params, &AudioDecoderOptions::default())
935 .map_err(|_| DecodeError::UnsupportedCodec)?,
936 )
937 };
938 let mut opus_bridge = if is_opus_codec {
939 Some(OpusBridge::new(codec_params).map_err(|e| DecodeError::Decode(e.to_string()))?)
940 } else {
941 None
942 };
943
944 let mut seek_samples = 0;
959 let mut discard = 0usize;
960 if seek_ms > 0 {
961 let preroll = if is_opus_codec { OPUS_PREROLL_MS } else { 0 };
962 let seeked = reader
963 .seek(
964 SeekMode::Accurate,
965 SeekTo::Time {
966 time: Time::from_millis_u64(seek_ms.saturating_sub(preroll)),
967 track_id: Some(track_id),
968 },
969 )
970 .map_err(|e| DecodeError::Decode(format!("seek failed: {}", e)))?;
971 seek_samples = landing_samples(time_base, seeked.actual_ts, sample_rate, channels)
972 .unwrap_or(
973 seek_ms.saturating_sub(preroll) * sample_rate as u64 * channels as u64 / 1000,
974 );
975 let landed_at_start = seek_samples == 0;
976 if preroll > 0 {
977 let wanted = seek_ms * sample_rate as u64 / 1000 * channels as u64;
978 discard = wanted.saturating_sub(seek_samples) as usize;
979 seek_samples += discard as u64;
980 }
981 if let Some(ref mut dec) = symphonia_decoder {
982 dec.reset();
983 }
984 if let Some(ref mut opus) = opus_bridge {
985 opus.reset(landed_at_start);
986 }
987 }
988
989 if let Some(setup) = &processing.dsp {
993 match chain {
994 Some(c) => {
995 let tail = c.set_source_rate(sample_rate);
996 if !write_ring(producer, tail, stop, viz_buffer, format) {
997 return Ok(Decoded::Complete(format));
998 }
999 }
1000 None => *chain = Some(Chain::new(setup, sample_rate, channels)),
1001 }
1002 }
1003
1004 let write_offset = timeline.samples_written();
1006 timeline.push_boundary(TrackBoundary {
1007 id: queue_item_id,
1008 path: path.to_path_buf(),
1009 info,
1010 sample_offset: write_offset,
1011 samples_written: 0,
1012 seek_samples,
1013 output_rate: out_rate,
1014 });
1015
1016 let rg_mode = processing.rg_mode;
1018 let pre_amp_db = processing.pre_amp_db;
1019 let rg_gain = if rg_mode != ReplayGainMode::Off {
1020 match crate::audio::replaygain::read_tags(path) {
1021 Ok(rg_info) => {
1022 let selected = crate::audio::replaygain::select_gain(&rg_info, rg_mode);
1023 if let Some((gain_db, _)) = selected {
1024 log::info!(
1025 "replaygain: applying {:.2} dB ({:?}) to {}",
1026 gain_db,
1027 rg_mode,
1028 path.display()
1029 );
1030 }
1031 selected
1032 }
1033 Err(e) => {
1034 log::debug!("replaygain: no tags for {}: {}", path.display(), e);
1035 None
1036 }
1037 }
1038 } else {
1039 None
1040 };
1041 let mut rg_scratch: Vec<f32> = Vec::new();
1042
1043 let mut sample_buf: Vec<f32> = Vec::new();
1044
1045 loop {
1046 if stop.load(Ordering::Relaxed) {
1047 return Ok(Decoded::Complete(format));
1048 }
1049
1050 let packet = match reader.next_packet() {
1051 Ok(Some(p)) => p,
1052 Ok(None) => return Ok(Decoded::Complete(format)),
1053 Err(e) => return Err(DecodeError::Decode(e.to_string())),
1054 };
1055
1056 if packet.track_id != track_id {
1057 continue;
1058 }
1059
1060 let samples: &[f32] = if let Some(ref mut opus) = opus_bridge {
1062 match opus.decode_packet(&packet.data) {
1063 Ok(s) => s,
1064 Err(e) => {
1065 log::warn!("opus decode error (skipping packet): {}", e);
1066 continue;
1067 }
1068 }
1069 } else {
1070 let decoder = symphonia_decoder.as_mut().unwrap();
1071 let decoded = match decoder.decode(&packet) {
1072 Ok(d) => d,
1073 Err(symphonia::core::errors::Error::DecodeError(e)) => {
1074 log::warn!("decode error (skipping packet): {}", e);
1075 continue;
1076 }
1077 Err(e) => return Err(DecodeError::Decode(e.to_string())),
1078 };
1079
1080 let spec = decoded.spec();
1081 let (decoded_rate, decoded_channels) = (spec.rate(), spec.channels().count() as u16);
1082 if (decoded_rate, decoded_channels) != (sample_rate, channels) {
1086 log::warn!(
1087 "{}: decoded {}Hz/{}ch but stream declares {}Hz/{}ch, restarting audio engine",
1088 path.display(),
1089 decoded_rate,
1090 decoded_channels,
1091 sample_rate,
1092 channels
1093 );
1094 return Ok(Decoded::FormatMismatch);
1095 }
1096 decoded.copy_to_vec_interleaved(&mut sample_buf);
1097 &sample_buf[..]
1098 };
1099
1100 let samples = if discard > 0 {
1101 let dropped = discard.min(samples.len());
1102 discard -= dropped;
1103 &samples[dropped..]
1104 } else {
1105 samples
1106 };
1107 if samples.is_empty() {
1108 continue;
1109 }
1110
1111 let samples = if let Some((gain_db, peak)) = rg_gain {
1114 rg_scratch.clear();
1115 rg_scratch.extend_from_slice(samples);
1116 crate::audio::replaygain::apply_gain(&mut rg_scratch, gain_db, peak, pre_amp_db);
1117 &rg_scratch[..]
1118 } else {
1119 samples
1120 };
1121
1122 let (samples, length) = match chain.as_mut() {
1123 Some(c) => c.process(samples),
1124 None => (samples, samples.len() as u64),
1125 };
1126 if !write_ring(producer, samples, stop, viz_buffer, format) {
1127 return Ok(Decoded::Complete(format));
1128 }
1129 timeline.add_written(length);
1130 }
1131}
1132
1133const OPUS_PREROLL_MS: u64 = 80;
1136
1137fn landing_samples(
1143 time_base: Option<TimeBase>,
1144 actual_ts: Timestamp,
1145 sample_rate: u32,
1146 channels: u16,
1147) -> Option<u64> {
1148 let (seconds, nanos) = time_base?.calc_time(actual_ts)?.parts();
1149 let rate = sample_rate as u64;
1150 let frames = seconds.max(0) as u64 * rate + (nanos as u64 * rate) / 1_000_000_000;
1151 Some(frames * channels as u64)
1152}
1153
1154pub(crate) fn track_duration_ms(
1161 reader: &(impl FormatReader + ?Sized),
1162 track: &Track,
1163 sample_rate: u32,
1164) -> u64 {
1165 fn to_ms(time_base: Option<TimeBase>, duration: Option<Duration>) -> Option<u64> {
1166 let time = time_base?.calc_duration(duration?)?;
1167 Some(time.as_millis().max(0) as u64)
1168 }
1169
1170 let media = reader.media_info();
1171 to_ms(track.time_base, track.duration)
1172 .or_else(|| to_ms(media.time_base, media.duration))
1173 .or_else(|| {
1174 track
1175 .num_frames
1176 .map(|frames| frames * 1000 / sample_rate as u64)
1177 })
1178 .unwrap_or(0)
1179}
1180
1181fn estimate_bitrate_from_codec_params(params: &AudioCodecParameters) -> Option<u32> {
1187 let is_lossy = matches!(
1188 params.codec,
1189 CODEC_ID_MP3 | CODEC_ID_AAC | CODEC_ID_VORBIS | CODEC_ID_OPUS
1190 );
1191 if !is_lossy {
1192 return None;
1193 }
1194
1195 let bpcs = params.bits_per_coded_sample?;
1198 let sr = params.sample_rate?;
1199 let channels = params
1200 .channels
1201 .as_ref()
1202 .map(|c| c.count() as u32)
1203 .unwrap_or(2);
1204 Some(bpcs * sr * channels / 1000)
1205}
1206
1207pub fn source_bit_depth(params: &AudioCodecParameters) -> Option<u16> {
1211 if params.codec == CODEC_ID_OPUS {
1212 return None;
1213 }
1214 if let Some(bits) = params.bits_per_sample {
1215 return Some(bits as u16);
1216 }
1217 if params.codec == CODEC_ID_ALAC {
1218 let cookie = params.extra_data.as_deref()?;
1219 return MagicCookie::read(cookie)
1220 .ok()
1221 .map(|c| u16::from(c.bit_depth));
1222 }
1223 None
1224}
1225
1226pub fn codec_name(codec: AudioCodecId) -> String {
1227 match codec {
1228 CODEC_ID_FLAC => "FLAC",
1229 CODEC_ID_MP3 => "MP3",
1230 CODEC_ID_AAC => "AAC",
1231 CODEC_ID_VORBIS => "Vorbis",
1232 CODEC_ID_OPUS => "Opus",
1233 CODEC_ID_ALAC => "ALAC",
1234 CODEC_ID_PCM_S16LE => "PCM/16",
1235 CODEC_ID_PCM_S24LE => "PCM/24",
1236 CODEC_ID_PCM_S32LE => "PCM/32",
1237 CODEC_ID_PCM_F32LE => "PCM/f32",
1238 other => return format!("Unknown({:?})", other),
1239 }
1240 .to_string()
1241}
1242
1243#[cfg(test)]
1244mod tests {
1245 use std::path::PathBuf;
1246 use std::sync::atomic::Ordering;
1247
1248 use super::*;
1249 use crate::player::state::QueueItemId;
1250
1251 fn make_info(sample_rate: u32, channels: u16) -> StreamInfo {
1252 StreamInfo {
1253 codec: "FLAC".to_string(),
1254 sample_rate,
1255 channels,
1256 bit_depth: Some(16),
1257 bitrate_kbps: None,
1258 duration_ms: 10_000,
1259 }
1260 }
1261
1262 fn make_boundary(
1263 id: QueueItemId,
1264 sample_offset: u64,
1265 seek_samples: u64,
1266 channels: u16,
1267 sample_rate: u32,
1268 ) -> TrackBoundary {
1269 TrackBoundary {
1270 id,
1271 path: PathBuf::from("/music/track.flac"),
1272 info: make_info(sample_rate, channels),
1273 sample_offset,
1274 samples_written: 0,
1275 seek_samples,
1276 output_rate: sample_rate,
1277 }
1278 }
1279
1280 fn writer(timeline: &PlaybackTimeline) -> TimelineWriter<'_> {
1281 timeline.writer()
1282 }
1283
1284 #[test]
1287 fn test_timeline_single_track() {
1288 let timeline = PlaybackTimeline::new();
1292 let tl = writer(&timeline);
1293 let id = QueueItemId::new();
1294 tl.push_boundary(make_boundary(id, 0, 0, 2, 44100));
1296 tl.add_written(88200); timeline.samples_played.store(88200, Ordering::Relaxed);
1300
1301 let result = timeline.current_playback();
1302 assert!(
1303 result.is_some(),
1304 "expected Some for single track with samples played"
1305 );
1306 let (result_id, _path, _info, position_ms) = result.unwrap();
1307 assert_eq!(result_id, id);
1308 assert_eq!(
1309 position_ms, 1000,
1310 "1 second of 44100 Hz stereo should be 1000 ms"
1311 );
1312 }
1313
1314 #[test]
1315 fn test_timeline_gapless_transition() {
1316 let timeline = PlaybackTimeline::new();
1320 let tl = writer(&timeline);
1321 let id1 = QueueItemId::new();
1322 let id2 = QueueItemId::new();
1323
1324 tl.push_boundary(make_boundary(id1, 0, 0, 2, 44100));
1326 tl.add_written(88200);
1327
1328 tl.push_boundary(make_boundary(id2, 88200, 0, 2, 44100));
1330 tl.add_written(44100); timeline.samples_played.store(90000, Ordering::Relaxed);
1334
1335 let result = timeline.current_playback();
1336 assert!(result.is_some());
1337 let (result_id, _path, _info, position_ms) = result.unwrap();
1338 assert_eq!(
1339 result_id, id2,
1340 "playback head past boundary should report second track"
1341 );
1342 assert_eq!(position_ms, 20, "position within track 2 should be ~20 ms");
1344 }
1345
1346 #[test]
1347 fn test_timeline_zero_samples() {
1348 let timeline = PlaybackTimeline::new();
1351 let tl = writer(&timeline);
1352 let id = QueueItemId::new();
1353 tl.push_boundary(make_boundary(id, 0, 0, 2, 44100));
1354 tl.add_written(1000);
1355 timeline.samples_played.store(0, Ordering::Relaxed);
1356
1357 let result = timeline.current_playback();
1358 assert!(
1359 result.is_some(),
1360 "expected Some at 0 samples played with a boundary at offset 0"
1361 );
1362 let (result_id, _path, _info, position_ms) = result.unwrap();
1363 assert_eq!(result_id, id);
1364 assert_eq!(position_ms, 0);
1365 }
1366
1367 #[test]
1368 fn test_timeline_past_all_boundaries() {
1369 let timeline = PlaybackTimeline::new();
1372 let tl = writer(&timeline);
1373 let id1 = QueueItemId::new();
1374 let id2 = QueueItemId::new();
1375
1376 tl.push_boundary(make_boundary(id1, 0, 0, 2, 44100));
1377 tl.add_written(88200);
1378 tl.push_boundary(make_boundary(id2, 88200, 0, 2, 44100));
1379 tl.add_written(88200);
1380
1381 timeline
1383 .samples_played
1384 .store(999_999_999, Ordering::Relaxed);
1385
1386 let result = timeline.current_playback();
1387 assert!(result.is_some());
1388 let (result_id, _path, _info, _position_ms) = result.unwrap();
1389 assert_eq!(
1390 result_id, id2,
1391 "samples past all boundaries should report the last track"
1392 );
1393 }
1394
1395 #[test]
1396 fn test_timeline_seek_offset() {
1397 let timeline = PlaybackTimeline::new();
1401 let tl = writer(&timeline);
1402 let id = QueueItemId::new();
1403 let seek_samples = 88200u64; tl.push_boundary(make_boundary(id, 0, seek_samples, 2, 44100));
1405 tl.add_written(44100); timeline.samples_played.store(0, Ordering::Relaxed);
1408
1409 let result = timeline.current_playback();
1410 assert!(result.is_some());
1411 let (_result_id, _path, _info, position_ms) = result.unwrap();
1412 assert_eq!(
1414 position_ms, 1000,
1415 "position should include seek offset of 1000 ms"
1416 );
1417 }
1418
1419 #[test]
1420 fn the_next_track_is_due_when_the_current_one_has_played_out() {
1421 let timeline = PlaybackTimeline::new();
1422 let tl = writer(&timeline);
1423 let (a, b) = (QueueItemId::new(), QueueItemId::new());
1424 tl.push_boundary(make_boundary(a, 0, 0, 2, 44100));
1426 assert_eq!(timeline.until_next_track(), None, "nothing queued after it");
1427 tl.push_boundary(make_boundary(b, 88200, 0, 2, 44100));
1428
1429 timeline.samples_played.store(44100, Ordering::Relaxed);
1430 assert_eq!(
1431 timeline.until_next_track(),
1432 Some(std::time::Duration::from_millis(500))
1433 );
1434 timeline.samples_played.store(88200, Ordering::Relaxed);
1435 assert_eq!(timeline.until_next_track(), None, "b is playing now");
1436 }
1437
1438 #[test]
1439 fn the_queued_callback_hears_each_track() {
1440 let timeline = PlaybackTimeline::new();
1441 let heard = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1442 let counter = heard.clone();
1443 timeline.on_queued(move || {
1444 counter.fetch_add(1, Ordering::Relaxed);
1445 });
1446 let tl = writer(&timeline);
1447 tl.push_boundary(make_boundary(QueueItemId::new(), 0, 0, 2, 44100));
1448 tl.push_boundary(make_boundary(QueueItemId::new(), 88200, 0, 2, 44100));
1449 assert_eq!(heard.load(Ordering::Relaxed), 2);
1450 }
1451
1452 #[test]
1453 fn a_track_played_past_reads_as_played_to_its_end() {
1454 let timeline = PlaybackTimeline::new();
1455 let tl = writer(&timeline);
1456 let (a, b) = (QueueItemId::new(), QueueItemId::new());
1457 tl.push_boundary(make_boundary(a, 0, 0, 2, 44100));
1458 tl.push_boundary(make_boundary(b, 88200, 0, 2, 44100));
1459 timeline
1460 .samples_played
1461 .store(88200 + 44100, Ordering::Relaxed);
1462
1463 assert_eq!(timeline.position_in(0), Some(1000));
1464 assert_eq!(timeline.position_in(1), Some(500));
1465 assert_eq!(
1466 timeline.playhead(),
1467 Some(Playhead {
1468 boundary: 1,
1469 id: b,
1470 position_ms: 500
1471 })
1472 );
1473 }
1474
1475 #[test]
1476 fn an_item_queued_twice_is_two_plays() {
1477 let timeline = PlaybackTimeline::new();
1478 let tl = writer(&timeline);
1479 let a = QueueItemId::new();
1480 tl.push_boundary(make_boundary(a, 0, 0, 2, 44100));
1481 tl.push_boundary(make_boundary(a, 88200, 0, 2, 44100));
1482 timeline
1483 .samples_played
1484 .store(88200 + 44100, Ordering::Relaxed);
1485
1486 assert_eq!(timeline.position_in(0), Some(1000), "the first pass ended");
1487 assert_eq!(timeline.position_in(1), Some(500));
1488 assert_eq!(timeline.playhead().map(|p| p.boundary), Some(1));
1489 }
1490
1491 #[test]
1492 fn test_timeline_reset() {
1493 let timeline = PlaybackTimeline::new();
1495 let tl = writer(&timeline);
1496 let id = QueueItemId::new();
1497 tl.push_boundary(make_boundary(id, 0, 0, 2, 44100));
1498 tl.add_written(88200);
1499 timeline.samples_played.store(44100, Ordering::Relaxed);
1500
1501 assert!(timeline.current_playback().is_some());
1503
1504 timeline.reset();
1505
1506 assert!(
1507 timeline.current_playback().is_none(),
1508 "after reset, current_playback should return None"
1509 );
1510 assert_eq!(
1511 timeline.samples_played.load(Ordering::Relaxed),
1512 0,
1513 "samples_played should be 0 after reset"
1514 );
1515 assert_eq!(
1516 timeline.samples_written.load(Ordering::Relaxed),
1517 0,
1518 "samples_written should be 0 after reset"
1519 );
1520 }
1521
1522 #[test]
1525 fn probe_file_extracts_stream_info() {
1526 let dir = tempfile::tempdir().unwrap();
1527 let wav_path = dir.path().join("probe_test.wav");
1528 crate::test_utils::generate_wav(&wav_path, 44100, 2, 1.0, 16);
1529
1530 let info = probe_file(&wav_path).expect("probe_file should succeed on a valid WAV");
1531 assert_eq!(info.sample_rate, 44100, "sample rate mismatch");
1532 assert_eq!(info.channels, 2, "channel count mismatch");
1533 assert_eq!(info.bit_depth, Some(16), "bit depth mismatch");
1534 assert!(
1535 info.duration_ms > 900 && info.duration_ms < 1100,
1536 "duration should be ~1000ms, got {}",
1537 info.duration_ms
1538 );
1539 assert!(
1540 info.codec.contains("PCM"),
1541 "codec should be PCM variant, got {}",
1542 info.codec
1543 );
1544 }
1545
1546 #[test]
1547 fn probe_file_reads_alac_bit_depth_from_the_magic_cookie() {
1548 let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/audio/testdata/24-bit-96k.m4a");
1549 let info = probe_file(&path).expect("probe_file should succeed on ALAC in MP4");
1550 assert_eq!(info.codec, "ALAC");
1551 assert_eq!(info.sample_rate, 96000);
1552 assert_eq!(info.bit_depth, Some(24));
1553 }
1554
1555 #[test]
1556 fn decode_single_produces_samples() {
1557 let dir = tempfile::tempdir().unwrap();
1558 let wav_path = dir.path().join("tone.wav");
1559 crate::test_utils::generate_wav_tone(&wav_path, 44100, 440.0, 0.1);
1561
1562 let (mut producer, mut consumer) = rtrb::RingBuffer::new(44100 * 2);
1564
1565 let timeline = PlaybackTimeline::new();
1566 let tl = writer(&timeline);
1567 let stop = Arc::new(AtomicBool::new(false));
1568
1569 let id = QueueItemId::new();
1570 let entry = SourceEntry::from_file(id, wav_path.clone());
1571 let hint = entry.hint.clone();
1572 let mss = (entry.make_mss)().expect("should open WAV file");
1573
1574 let result = decode_single(
1575 id,
1576 &wav_path,
1577 &hint,
1578 mss,
1579 &mut producer,
1580 &stop,
1581 0,
1582 &tl,
1583 None,
1584 &Processing::default(),
1585 &mut None,
1586 None,
1587 );
1588 assert!(
1589 matches!(result, Ok(Decoded::Complete((44100, 1)))),
1590 "decode_single should complete at the source format"
1591 );
1592
1593 let available = consumer.slots();
1595 assert!(available > 0, "expected samples in ring buffer, got 0");
1596
1597 let mut found_nonzero = false;
1599 while consumer.slots() > 0 {
1600 if let Ok(chunk) = consumer.read_chunk(consumer.slots().min(1024)) {
1601 let (first, second) = chunk.as_slices();
1602 for &s in first.iter().chain(second.iter()) {
1603 if s.abs() > 0.001 {
1604 found_nonzero = true;
1605 break;
1606 }
1607 }
1608 chunk.commit_all();
1609 }
1610 if found_nonzero {
1611 break;
1612 }
1613 }
1614 assert!(
1615 found_nonzero,
1616 "expected non-zero samples from 440Hz sine decode"
1617 );
1618 }
1619
1620 fn run_queue(paths: &[PathBuf]) -> Vec<TrackBoundary> {
1626 run_queue_with(paths, &Processing::default()).0
1627 }
1628
1629 fn run_queue_with(
1632 paths: &[PathBuf],
1633 processing: &Processing,
1634 ) -> (Vec<TrackBoundary>, u64, u64) {
1635 let (producer, mut consumer) = rtrb::RingBuffer::new(1 << 16);
1636 let timeline = PlaybackTimeline::new();
1637 let tl = writer(&timeline);
1638 let stop = Arc::new(AtomicBool::new(false));
1639
1640 let drain_stop = Arc::new(AtomicBool::new(false));
1641 let drain_flag = drain_stop.clone();
1642 let drainer = std::thread::spawn(move || {
1643 let mut read = 0u64;
1644 while !drain_flag.load(Ordering::Relaxed) {
1645 let n = consumer.slots();
1646 if n > 0
1647 && let Ok(chunk) = consumer.read_chunk(n)
1648 {
1649 read += n as u64;
1650 chunk.commit_all();
1651 }
1652 std::thread::sleep(std::time::Duration::from_micros(200));
1653 }
1654 read
1655 });
1656
1657 let rest: std::sync::Mutex<Vec<PathBuf>> = std::sync::Mutex::new(paths[1..].to_vec());
1658 let next_track = move || {
1659 let mut rest = rest.lock().ok()?;
1660 if rest.is_empty() {
1661 return None;
1662 }
1663 Some(SourceEntry::from_file(QueueItemId::new(), rest.remove(0)))
1664 };
1665
1666 decode_queue_loop(
1667 SourceEntry::from_file(QueueItemId::new(), paths[0].clone()),
1668 producer,
1669 &stop,
1670 0,
1671 &next_track,
1672 &tl,
1673 None,
1674 processing,
1675 );
1676
1677 drain_stop.store(true, Ordering::Relaxed);
1678 let read = drainer.join().unwrap();
1679
1680 let written = timeline.samples_written.load(Ordering::Relaxed);
1681 (timeline.boundaries.read().clone(), read, written)
1682 }
1683
1684 #[test]
1685 fn convolution_at_one_rate_plays_two_source_rates_gaplessly() {
1686 let dir = tempfile::tempdir().unwrap();
1687 let a = dir.path().join("a.wav");
1688 let b = dir.path().join("b.wav");
1689 crate::test_utils::generate_wav(&a, 44100, 2, 0.5, 16);
1690 crate::test_utils::generate_wav(&b, 96000, 2, 0.5, 16);
1691 let mut ir = vec![0.0; 129];
1692 ir[64] = 1.0;
1693 let processing = Processing {
1694 dsp: Some(Arc::new(Setup::new(
1695 vec![],
1696 vec![crate::audio::dsp::Impulse::from_channels(48000, vec![ir])],
1697 ))),
1698 ..Processing::default()
1699 };
1700
1701 let (bounds, read, written) = run_queue_with(&[a, b], &processing);
1702 assert_eq!(bounds.len(), 2, "both resample to 48 kHz, so one session");
1703 assert!(bounds.iter().all(|b| b.output_rate == 48000));
1704 assert_eq!(bounds[0].info.sample_rate, 44100);
1705 assert_eq!(bounds[1].info.sample_rate, 96000);
1706 assert_eq!(written, 2 * 24000 * 2);
1709 assert_eq!(read, written);
1710 assert_eq!(bounds[1].sample_offset, 24000 * 2);
1711 }
1712
1713 #[test]
1714 fn gapless_continues_when_format_matches() {
1715 let dir = tempfile::tempdir().unwrap();
1716 let a = dir.path().join("a.wav");
1717 let b = dir.path().join("b.wav");
1718 crate::test_utils::generate_wav(&a, 44100, 2, 0.1, 16);
1719 crate::test_utils::generate_wav(&b, 44100, 2, 0.1, 16);
1720
1721 let bounds = run_queue(&[a, b]);
1722 assert_eq!(
1723 bounds.len(),
1724 2,
1725 "same-format tracks should decode gaplessly"
1726 );
1727 }
1728
1729 #[test]
1730 fn gapless_stops_at_sample_rate_change() {
1731 let dir = tempfile::tempdir().unwrap();
1732 let a = dir.path().join("a.wav");
1733 let b = dir.path().join("b.wav");
1734 crate::test_utils::generate_wav(&a, 44100, 2, 0.1, 16);
1735 crate::test_utils::generate_wav(&b, 48000, 2, 0.1, 16);
1736
1737 let bounds = run_queue(&[a, b]);
1738 assert_eq!(
1739 bounds.len(),
1740 1,
1741 "a 48kHz track must not join a 44.1kHz ring buffer"
1742 );
1743 assert_eq!(bounds[0].info.sample_rate, 44100);
1744 }
1745
1746 #[test]
1747 fn gapless_stops_at_channel_change() {
1748 let dir = tempfile::tempdir().unwrap();
1749 let a = dir.path().join("a.wav");
1750 let b = dir.path().join("b.wav");
1751 crate::test_utils::generate_wav(&a, 44100, 2, 0.1, 16);
1752 crate::test_utils::generate_wav(&b, 44100, 1, 0.1, 16);
1753
1754 let bounds = run_queue(&[a, b]);
1755 assert_eq!(
1756 bounds.len(),
1757 1,
1758 "a mono track must not join a stereo ring buffer"
1759 );
1760 assert_eq!(bounds[0].info.channels, 2);
1761 }
1762
1763 #[test]
1764 fn drain_waits_for_the_consumer() {
1765 let (mut producer, mut consumer) = rtrb::RingBuffer::new(64);
1766 for _ in 0..64 {
1767 producer.push(0.0).unwrap();
1768 }
1769 let stop = Arc::new(AtomicBool::new(false));
1770
1771 let reader = std::thread::spawn(move || {
1772 std::thread::sleep(std::time::Duration::from_millis(20));
1773 let chunk = consumer.read_chunk(64).unwrap();
1774 chunk.commit_all();
1775 consumer
1776 });
1777
1778 wait_for_drain(&producer, &stop, None);
1779 assert_eq!(producer.slots(), 64, "drain must wait for an empty buffer");
1780 drop(reader.join().unwrap());
1781 }
1782
1783 #[test]
1784 fn drain_returns_when_playback_is_torn_down() {
1785 let (producer, consumer) = rtrb::RingBuffer::<f32>::new(64);
1786 let stop = Arc::new(AtomicBool::new(true));
1787 wait_for_drain(&producer, &stop, None);
1788 drop(consumer);
1789 }
1790
1791 fn write_garbage(path: &Path) {
1795 std::fs::write(path, b"this is not a wav file").unwrap();
1796 }
1797
1798 #[test]
1799 fn an_unreadable_track_is_skipped_and_the_queue_continues() {
1800 let dir = tempfile::tempdir().unwrap();
1801 let a = dir.path().join("a.wav");
1802 let bad = dir.path().join("bad.wav");
1803 let c = dir.path().join("c.wav");
1804 crate::test_utils::generate_wav(&a, 44100, 2, 0.1, 16);
1805 write_garbage(&bad);
1806 crate::test_utils::generate_wav(&c, 44100, 2, 0.1, 16);
1807
1808 let bounds = run_queue(&[a.clone(), bad, c.clone()]);
1809 let decoded: Vec<_> = bounds.iter().map(|b| b.path.clone()).collect();
1810 assert_eq!(
1811 decoded,
1812 vec![a, c],
1813 "one bad file must not take the rest of the queue with it"
1814 );
1815 }
1816
1817 #[test]
1818 fn a_missing_track_is_skipped_and_the_queue_continues() {
1819 let dir = tempfile::tempdir().unwrap();
1820 let missing = dir.path().join("gone.wav");
1821 let b = dir.path().join("b.wav");
1822 crate::test_utils::generate_wav(&b, 44100, 2, 0.1, 16);
1823
1824 let bounds = run_queue(&[missing, b.clone()]);
1825 assert_eq!(bounds.len(), 1);
1826 assert_eq!(
1827 bounds[0].path, b,
1828 "a bad first track must not end the session"
1829 );
1830 }
1831
1832 #[test]
1833 fn an_entirely_unreadable_queue_terminates() {
1834 let dir = tempfile::tempdir().unwrap();
1835 let bad = dir.path().join("bad.wav");
1836 write_garbage(&bad);
1837
1838 let (producer, _consumer) = rtrb::RingBuffer::<f32>::new(1 << 12);
1841 let timeline = PlaybackTimeline::new();
1842 let tl = writer(&timeline);
1843 let stop = Arc::new(AtomicBool::new(false));
1844
1845 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1846 let counter = calls.clone();
1847 let bad_path = bad.clone();
1848 let next_track = move || {
1849 counter.fetch_add(1, Ordering::Relaxed);
1850 Some(SourceEntry::from_file(QueueItemId::new(), bad_path.clone()))
1851 };
1852
1853 decode_queue_loop(
1854 SourceEntry::from_file(QueueItemId::new(), bad),
1855 producer,
1856 &stop,
1857 0,
1858 &next_track,
1859 &tl,
1860 None,
1861 &Processing::default(),
1862 );
1863
1864 assert_eq!(
1866 calls.load(Ordering::Relaxed) + 1,
1867 MAX_CONSECUTIVE_FAILURES as usize
1868 );
1869 assert!(timeline.boundaries.read().is_empty());
1870 }
1871
1872 #[test]
1875 fn landing_samples_converts_frame_timebases() {
1876 let tb = TimeBase::try_from_recip(44100).unwrap();
1878 assert_eq!(
1879 landing_samples(Some(tb), Timestamp::from(44100u32), 44100, 2),
1880 Some(88_200)
1881 );
1882 }
1883
1884 #[test]
1885 fn landing_samples_converts_millisecond_timebases() {
1886 let tb = TimeBase::try_new(1, 1000).unwrap();
1888 assert_eq!(
1889 landing_samples(Some(tb), Timestamp::from(1500u32), 48000, 2),
1890 Some(48000 * 3 / 2 * 2)
1891 );
1892 }
1893
1894 #[test]
1895 fn landing_samples_needs_a_timebase() {
1896 assert_eq!(
1897 landing_samples(None, Timestamp::from(1000u32), 44100, 2),
1898 None
1899 );
1900 }
1901
1902 #[cfg(test)]
1905 fn make_vbr_mp3(dir: &Path) -> PathBuf {
1906 let wav = dir.join("source.wav");
1907 let mp3 = dir.join("source.mp3");
1908 let ok = std::process::Command::new("sox")
1909 .args(["-n", "-r", "44100", "-c", "2"])
1910 .arg(&wav)
1911 .args([
1912 "synth", "30", "sine", "200", "vol", "0.02", ":", "synth", "270", "sine", "880",
1913 "vol", "0.9",
1914 ])
1915 .status()
1916 .expect("sox not installed")
1917 .success();
1918 assert!(ok, "sox failed");
1919 let ok = std::process::Command::new("lame")
1920 .args(["-V", "2", "--quiet"])
1921 .arg(&wav)
1922 .arg(&mp3)
1923 .status()
1924 .expect("lame not installed")
1925 .success();
1926 assert!(ok, "lame failed");
1927 mp3
1928 }
1929
1930 #[test]
1935 #[ignore = "generates a fixture with sox + lame; run with cargo test -- --ignored"]
1936 fn seek_on_vbr_reports_where_it_landed() {
1937 let dir = tempfile::tempdir().unwrap();
1938 let path = make_vbr_mp3(dir.path());
1939 let info = probe_file(&path).unwrap();
1940 let channels = info.channels as u64;
1941 let rate = info.sample_rate as u64;
1942
1943 let seek_ms = 150_000u64;
1944 let (mut producer, mut consumer) = rtrb::RingBuffer::<f32>::new(1 << 16);
1945 let stop = Arc::new(AtomicBool::new(false));
1946 let timeline = PlaybackTimeline::new();
1947 let tl = writer(&timeline);
1948
1949 let drain_stop = stop.clone();
1950 let drained = std::thread::spawn(move || {
1951 let mut total = 0u64;
1952 while !drain_stop.load(Ordering::Relaxed) {
1953 let slots = consumer.slots();
1954 if slots == 0 {
1955 std::thread::sleep(std::time::Duration::from_micros(200));
1956 continue;
1957 }
1958 let chunk = consumer.read_chunk(slots).unwrap();
1959 total += slots as u64;
1960 chunk.commit_all();
1961 }
1962 total
1963 });
1964
1965 let file = File::open(&path).unwrap();
1966 let mss = MediaSourceStream::new(Box::new(file), Default::default());
1967 let mut hint = Hint::new();
1968 hint.with_extension("mp3");
1969 decode_single(
1970 QueueItemId::new(),
1971 &path,
1972 &hint,
1973 mss,
1974 &mut producer,
1975 &stop,
1976 seek_ms,
1977 &tl,
1978 None,
1979 &Processing::default(),
1980 &mut None,
1981 None,
1982 )
1983 .unwrap();
1984
1985 let written = timeline.samples_written.load(Ordering::Relaxed);
1986 stop.store(true, Ordering::Relaxed);
1987 drained.join().unwrap();
1988
1989 let reported_start_ms = {
1990 let bounds = timeline.boundaries.read();
1991 (bounds[0].seek_samples / channels) * 1000 / rate
1992 };
1993 let decoded_ms = (written / channels) * 1000 / rate;
1994
1995 let total_ms = reported_start_ms + decoded_ms;
1997 assert!(
1998 total_ms.abs_diff(info.duration_ms) < 500,
1999 "reported start {}ms + {}ms decoded = {}ms, but the file is {}ms",
2000 reported_start_ms,
2001 decoded_ms,
2002 total_ms,
2003 info.duration_ms
2004 );
2005 assert!(
2007 reported_start_ms.abs_diff(seek_ms) < 100,
2008 "seek to {}ms reported {}ms",
2009 seek_ms,
2010 reported_start_ms
2011 );
2012 }
2013}