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 thiserror::Error;
18
19use crate::audio::opus::OpusBridge;
20use crate::audio::viz::VizBuffer;
21use crate::config::ReplayGainMode;
22use crate::player::state::QueueItemId;
23
24#[derive(Debug, Error)]
25pub enum DecodeError {
26 #[error("failed to open file: {0}")]
27 Io(#[from] std::io::Error),
28 #[error("no supported audio track found")]
29 NoTrack,
30 #[error("unsupported codec")]
31 UnsupportedCodec,
32 #[error("decode error: {0}")]
33 Decode(String),
34}
35
36#[derive(Debug, Clone)]
38pub struct StreamInfo {
39 pub codec: String,
40 pub sample_rate: u32,
41 pub channels: u16,
42 pub bit_depth: Option<u16>,
43 pub bitrate_kbps: Option<u32>,
45 pub duration_ms: u64,
46}
47
48pub struct DecodeHandle {
50 stop: Arc<AtomicBool>,
51 thread: Option<thread::JoinHandle<()>>,
52}
53
54impl DecodeHandle {
55 pub fn signal_stop(&self) {
59 self.stop.store(true, Ordering::Relaxed);
60 if let Some(handle) = &self.thread {
61 handle.thread().unpark();
62 }
63 }
64
65 #[cfg(test)]
67 pub fn new_for_test(stop: Arc<AtomicBool>) -> Self {
68 Self { stop, thread: None }
69 }
70
71 pub fn stop(&mut self) {
73 self.signal_stop();
74 if let Some(handle) = self.thread.take()
75 && let Err(payload) = handle.join()
76 {
77 let msg = payload
78 .downcast_ref::<String>()
79 .map(|s| s.as_str())
80 .or_else(|| payload.downcast_ref::<&str>().copied())
81 .unwrap_or("unknown");
82 log::error!("decode thread panicked: {}", msg);
83 }
84 }
85}
86
87impl Drop for DecodeHandle {
88 fn drop(&mut self) {
89 self.stop();
90 }
91}
92
93#[derive(Debug, Clone)]
98pub struct TrackBoundary {
99 pub id: QueueItemId,
100 pub path: PathBuf,
101 pub info: StreamInfo,
102 pub sample_offset: u64,
106 pub samples_written: u64,
109 pub seek_samples: u64,
111}
112
113impl TrackBoundary {
114 fn position_ms(&self, played: u64) -> Option<u64> {
116 let ch = self.info.channels as u64;
117 let rate = self.info.sample_rate as u64;
118 if ch == 0 || rate == 0 {
119 return None;
120 }
121 let track_samples = played.saturating_sub(self.sample_offset);
123 Some((track_samples / ch) * 1000 / rate + (self.seek_samples / ch) * 1000 / rate)
124 }
125}
126
127pub struct PlaybackTimeline {
131 boundaries: parking_lot::RwLock<Vec<TrackBoundary>>,
132 samples_written: AtomicU64,
134 pub samples_played: Arc<AtomicU64>,
137 generation: AtomicU64,
140 queued: parking_lot::Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
143}
144
145impl std::fmt::Debug for PlaybackTimeline {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 f.debug_struct("PlaybackTimeline")
148 .field("samples_played", &self.samples_played)
149 .field("generation", &self.generation)
150 .finish_non_exhaustive()
151 }
152}
153
154impl PlaybackTimeline {
155 pub fn new() -> Arc<Self> {
156 Arc::new(Self {
157 boundaries: parking_lot::RwLock::new(Vec::new()),
158 samples_written: AtomicU64::new(0),
159 samples_played: Arc::new(AtomicU64::new(0)),
160 generation: AtomicU64::new(0),
161 queued: parking_lot::Mutex::new(None),
162 })
163 }
164
165 pub fn on_queued(&self, f: impl Fn() + Send + Sync + 'static) {
167 *self.queued.lock() = Some(Box::new(f));
168 }
169
170 pub fn playhead(&self) -> Option<(QueueItemId, u64)> {
173 let bounds = self.boundaries.read();
174 let played = self.samples_played.load(Ordering::Acquire);
175 let idx = bounds.partition_point(|b| b.sample_offset <= played);
176 let current = bounds.get(idx.checked_sub(1)?)?;
177 Some((current.id, current.position_ms(played)?))
178 }
179
180 pub fn position_of(&self, id: QueueItemId) -> Option<u64> {
183 let bounds = self.boundaries.read();
184 let played = self.samples_played.load(Ordering::Acquire);
185 let idx = bounds.iter().rposition(|b| b.id == id)?;
186 let at = bounds
187 .get(idx + 1)
188 .map_or(played, |next| played.min(next.sample_offset));
189 bounds[idx].position_ms(at)
190 }
191
192 pub fn until_next_track(&self) -> Option<std::time::Duration> {
195 let bounds = self.boundaries.read();
196 let played = self.samples_played.load(Ordering::Acquire);
197 let idx = bounds.partition_point(|b| b.sample_offset <= played);
198 let next = bounds.get(idx)?;
199 let current = bounds.get(idx.checked_sub(1)?)?;
200 let per_second = current.info.sample_rate as u64 * current.info.channels as u64;
201 if per_second == 0 {
202 return None;
203 }
204 let left = next.sample_offset - played;
205 Some(std::time::Duration::from_micros(
206 left.saturating_mul(1_000_000) / per_second,
207 ))
208 }
209
210 pub fn generation(&self) -> u64 {
215 self.generation.load(Ordering::Acquire)
216 }
217
218 pub fn writer(&self, generation: u64) -> TimelineWriter<'_> {
220 TimelineWriter {
221 timeline: self,
222 generation,
223 }
224 }
225
226 pub fn reset(&self) {
228 let mut bounds = self.boundaries.write();
232 self.generation.fetch_add(1, Ordering::AcqRel);
233 bounds.clear();
234 self.samples_written.store(0, Ordering::Relaxed);
235 self.samples_played.store(0, Ordering::Relaxed);
236 }
237
238 pub fn samples_played_counter(&self) -> Arc<AtomicU64> {
240 self.samples_played.clone()
241 }
242
243 pub fn current_playback(&self) -> Option<(QueueItemId, PathBuf, StreamInfo, u64)> {
251 let bounds = self.boundaries.read();
253
254 if bounds.is_empty() {
255 return None;
256 }
257
258 let played = self.samples_played.load(Ordering::Acquire);
262
263 let idx = bounds.partition_point(|b| b.sample_offset <= played);
267 let current = if idx > 0 {
268 &bounds[idx - 1]
269 } else {
270 return None;
271 };
272
273 let position_ms = current.position_ms(played)?;
274
275 Some((
276 current.id,
277 current.path.clone(),
278 current.info.clone(),
279 position_ms,
280 ))
281 }
282}
283
284pub struct TimelineWriter<'a> {
298 timeline: &'a PlaybackTimeline,
299 generation: u64,
300}
301
302impl TimelineWriter<'_> {
303 pub fn is_current(&self) -> bool {
306 self.timeline.generation.load(Ordering::Acquire) == self.generation
307 }
308
309 fn samples_written(&self) -> u64 {
311 self.timeline.samples_written.load(Ordering::Relaxed)
312 }
313
314 fn push_boundary(&self, boundary: TrackBoundary) {
316 {
317 let mut bounds = self.timeline.boundaries.write();
318 if !self.is_current() {
319 return;
320 }
321 bounds.push(boundary);
322 }
323 if let Some(queued) = self.timeline.queued.lock().as_ref() {
324 queued();
325 }
326 }
327
328 fn add_written(&self, count: u64) {
330 let mut bounds = self.timeline.boundaries.write();
331 if !self.is_current() {
332 return;
333 }
334 self.timeline
335 .samples_written
336 .fetch_add(count, Ordering::Relaxed);
337 if let Some(last) = bounds.last_mut() {
339 last.samples_written += count;
340 }
341 }
342}
343
344pub struct SourceEntry {
353 pub id: QueueItemId,
354 pub path: PathBuf,
356 pub hint: Hint,
358 pub make_mss: Box<dyn FnOnce() -> std::io::Result<MediaSourceStream<'static>> + Send>,
360}
361
362impl SourceEntry {
363 pub fn from_file(id: QueueItemId, path: PathBuf) -> Self {
365 let ext = path
366 .extension()
367 .and_then(|e| e.to_str())
368 .unwrap_or("")
369 .to_string();
370 let path_clone = path.clone();
371 let mut hint = Hint::new();
372 if !ext.is_empty() {
373 hint.with_extension(&ext);
374 }
375 Self {
376 id,
377 path,
378 hint,
379 make_mss: Box::new(move || {
380 let file = File::open(&path_clone)?;
381 Ok(MediaSourceStream::new(Box::new(file), Default::default()))
382 }),
383 }
384 }
385}
386
387pub fn probe_source(mss: MediaSourceStream<'_>, hint: &Hint) -> Result<StreamInfo, DecodeError> {
393 probe_mss(mss, hint)
394}
395
396pub fn probe_file(path: &Path) -> Result<StreamInfo, DecodeError> {
398 let file_size = std::fs::metadata(path).ok().map(|m| m.len());
399 let file = File::open(path)?;
400 let mss = MediaSourceStream::new(Box::new(file), Default::default());
401 let mut hint = Hint::new();
402 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
403 hint.with_extension(ext);
404 }
405 let mut info = probe_mss(mss, &hint)?;
406 if info.bitrate_kbps.is_none()
409 && info.bit_depth.is_none()
410 && let Some(size) = file_size
411 && info.duration_ms > 0
412 {
413 info.bitrate_kbps = Some((size * 8 / info.duration_ms) as u32);
414 }
415 Ok(info)
416}
417
418fn probe_mss(mss: MediaSourceStream<'_>, hint: &Hint) -> Result<StreamInfo, DecodeError> {
420 let reader = symphonia::default::get_probe()
421 .probe(
422 hint,
423 mss,
424 FormatOptions::default(),
425 MetadataOptions::default(),
426 )
427 .map_err(|e| match e {
428 symphonia::core::errors::Error::IoError(io) => DecodeError::Io(io),
432 other => DecodeError::Decode(other.to_string()),
433 })?;
434
435 let track = reader
436 .default_track(TrackType::Audio)
437 .ok_or(DecodeError::NoTrack)?;
438 let codec_params = track
439 .codec_params
440 .as_ref()
441 .and_then(|p| p.audio())
442 .ok_or(DecodeError::NoTrack)?;
443 let is_opus = codec_params.codec == CODEC_ID_OPUS;
444 let sample_rate = if is_opus {
446 48000
447 } else {
448 codec_params.sample_rate.unwrap_or(44100)
449 };
450 let channels = codec_params
451 .channels
452 .as_ref()
453 .map(|c| c.count() as u16)
454 .unwrap_or(2);
455 let bit_depth = if is_opus {
456 None
457 } else {
458 Some(codec_params.bits_per_sample.unwrap_or(16) as u16)
459 };
460 let duration_ms = track_duration_ms(&*reader, track, sample_rate);
461 let codec = codec_name(codec_params.codec);
462
463 let bitrate_kbps = estimate_bitrate_from_codec_params(codec_params);
468
469 Ok(StreamInfo {
470 codec,
471 sample_rate,
472 channels,
473 bit_depth,
474 bitrate_kbps,
475 duration_ms,
476 })
477}
478
479#[allow(clippy::too_many_arguments)]
490pub fn start_decode<N, F>(
491 first: SourceEntry,
492 producer: rtrb::Producer<f32>,
493 seek_ms: u64,
494 next_track: N,
495 timeline: Arc<PlaybackTimeline>,
496 viz_buffer: Option<Arc<VizBuffer>>,
497 rg_mode: ReplayGainMode,
498 pre_amp_db: f64,
499 on_finished: F,
500) -> Result<(StreamInfo, DecodeHandle), DecodeError>
501where
502 N: Fn() -> Option<SourceEntry> + Send + 'static,
503 F: FnOnce() + Send + 'static,
504{
505 let stop = Arc::new(AtomicBool::new(false));
506 let stop_clone = stop.clone();
507 let generation = timeline.generation();
511
512 let thread = thread::Builder::new()
513 .name("koan-decode".into())
514 .spawn(move || {
515 decode_queue_loop(
516 first,
517 producer,
518 &stop_clone,
519 seek_ms,
520 &next_track,
521 &timeline.writer(generation),
522 viz_buffer.as_deref(),
523 rg_mode,
524 pre_amp_db,
525 );
526 if !stop_clone.load(Ordering::Relaxed) {
530 on_finished();
531 }
532 })
533 .map_err(DecodeError::Io)?;
534
535 let placeholder = StreamInfo {
538 codec: String::from("?"),
539 sample_rate: 44100,
540 channels: 2,
541 bit_depth: Some(16),
542 bitrate_kbps: None,
543 duration_ms: 0,
544 };
545
546 Ok((
547 placeholder,
548 DecodeHandle {
549 stop,
550 thread: Some(thread),
551 },
552 ))
553}
554
555#[allow(clippy::too_many_arguments)]
565pub fn start_decode_file<N, F>(
566 initial_id: QueueItemId,
567 path: &Path,
568 producer: rtrb::Producer<f32>,
569 seek_ms: u64,
570 next_track: N,
571 timeline: Arc<PlaybackTimeline>,
572 viz_buffer: Option<Arc<VizBuffer>>,
573 rg_mode: ReplayGainMode,
574 pre_amp_db: f64,
575 on_finished: F,
576) -> Result<(StreamInfo, DecodeHandle), DecodeError>
577where
578 N: Fn() -> Option<(QueueItemId, PathBuf)> + Send + 'static,
579 F: FnOnce() + Send + 'static,
580{
581 let info = probe_file(path)?;
582 let first = SourceEntry::from_file(initial_id, path.to_path_buf());
583 let (_, handle) = start_decode(
584 first,
585 producer,
586 seek_ms,
587 move || {
588 let (id, p) = next_track()?;
589 Some(SourceEntry::from_file(id, p))
590 },
591 timeline,
592 viz_buffer,
593 rg_mode,
594 pre_amp_db,
595 on_finished,
596 )?;
597 Ok((info, handle))
598}
599
600const MAX_CONSECUTIVE_FAILURES: u32 = 32;
608
609#[allow(clippy::too_many_arguments)]
620fn decode_queue_loop<N>(
621 first: SourceEntry,
622 mut producer: rtrb::Producer<f32>,
623 stop: &AtomicBool,
624 initial_seek_ms: u64,
625 next_track: &N,
626 timeline: &TimelineWriter<'_>,
627 viz_buffer: Option<&VizBuffer>,
628 rg_mode: ReplayGainMode,
629 pre_amp_db: f64,
630) where
631 N: Fn() -> Option<SourceEntry>,
632{
633 if let Some(viz) = viz_buffer {
636 viz.reset();
637 }
638
639 let mut pending = Some(first);
640 let mut seek_ms = initial_seek_ms;
641 let mut format: Option<PcmFormat> = None;
642 let mut failures: u32 = 0;
643
644 while let Some(entry) = pending.take() {
645 if stop.load(Ordering::Relaxed) || !timeline.is_current() {
646 break;
647 }
648
649 let SourceEntry {
650 id,
651 path,
652 hint,
653 make_mss,
654 } = entry;
655
656 let outcome = make_mss().map_err(DecodeError::Io).and_then(|mss| {
657 decode_single(
658 id,
659 &path,
660 &hint,
661 mss,
662 &mut producer,
663 stop,
664 seek_ms,
665 timeline,
666 viz_buffer,
667 rg_mode,
668 pre_amp_db,
669 format,
670 )
671 });
672
673 match outcome {
674 Ok(Decoded::Complete(decoded_format)) => {
675 format = Some(decoded_format);
676 failures = 0;
677 }
678 Ok(Decoded::FormatMismatch) => break,
679 Err(e) => {
680 if stop.load(Ordering::Relaxed) {
681 break;
682 }
683 failures += 1;
684 log::error!("skipping {}: {}", path.display(), e);
685 if failures >= MAX_CONSECUTIVE_FAILURES {
686 log::error!(
687 "{} sources failed in a row, decode thread giving up",
688 failures
689 );
690 break;
691 }
692 }
693 }
694 if stop.load(Ordering::Relaxed) || !timeline.is_current() {
697 break;
698 }
699
700 seek_ms = 0;
701 pending = (next_track)();
702 match pending {
703 Some(ref next) => log::info!("gapless transition → {}", next.path.display()),
704 None => log::info!("playlist exhausted, decode thread finishing"),
705 }
706 }
707
708 wait_for_drain(&producer, stop, format);
709}
710
711fn time_to_play(samples: usize, format: Option<PcmFormat>) -> std::time::Duration {
714 match format {
715 Some((rate, channels)) if rate > 0 && channels > 0 => std::time::Duration::from_micros(
716 samples as u64 * 1_000_000 / (rate as u64 * channels as u64),
717 ),
718 _ => std::time::Duration::from_millis(2),
719 }
720}
721
722fn wait_for_drain(producer: &rtrb::Producer<f32>, stop: &AtomicBool, format: Option<PcmFormat>) {
728 let capacity = producer.buffer().capacity();
729 while !stop.load(Ordering::Relaxed) && !producer.is_abandoned() {
730 let left = capacity.saturating_sub(producer.slots());
731 if left == 0 {
732 return;
733 }
734 thread::park_timeout(time_to_play(left, format));
735 }
736}
737
738type PcmFormat = (u32, u16);
746
747enum Decoded {
749 Complete(PcmFormat),
751 FormatMismatch,
754}
755
756#[allow(clippy::too_many_arguments)]
761fn decode_single(
762 queue_item_id: QueueItemId,
763 path: &Path,
764 hint: &Hint,
765 mss: MediaSourceStream<'_>,
766 producer: &mut rtrb::Producer<f32>,
767 stop: &AtomicBool,
768 seek_ms: u64,
769 timeline: &TimelineWriter<'_>,
770 viz_buffer: Option<&VizBuffer>,
771 rg_mode: ReplayGainMode,
772 pre_amp_db: f64,
773 expected: Option<PcmFormat>,
774) -> Result<Decoded, DecodeError> {
775 let mut reader = symphonia::default::get_probe()
776 .probe(
777 hint,
778 mss,
779 FormatOptions::default(),
780 MetadataOptions::default(),
781 )
782 .map_err(|e| DecodeError::Decode(e.to_string()))?;
783
784 let track = reader
785 .default_track(TrackType::Audio)
786 .ok_or(DecodeError::NoTrack)?;
787 let track_id = track.id;
788 let time_base = track.time_base;
789 let codec_params = track
790 .codec_params
791 .as_ref()
792 .and_then(|p| p.audio())
793 .ok_or(DecodeError::NoTrack)?;
794 let is_opus_codec = codec_params.codec == CODEC_ID_OPUS;
795
796 let sample_rate = if is_opus_codec {
798 48000
799 } else {
800 codec_params.sample_rate.unwrap_or(44100)
801 };
802 let channels = codec_params
803 .channels
804 .as_ref()
805 .map(|c| c.count() as u16)
806 .unwrap_or(2);
807
808 let duration_ms = track_duration_ms(&*reader, track, sample_rate);
809
810 let mut bitrate_kbps = estimate_bitrate_from_codec_params(codec_params);
812 if bitrate_kbps.is_none()
813 && is_opus_codec
814 && let Ok(meta) = std::fs::metadata(path)
815 && duration_ms > 0
816 {
817 bitrate_kbps = Some((meta.len() * 8 / duration_ms) as u32);
818 }
819
820 let info = StreamInfo {
821 codec: codec_name(codec_params.codec),
822 sample_rate,
823 channels,
824 bit_depth: if is_opus_codec {
825 None
826 } else {
827 Some(codec_params.bits_per_sample.unwrap_or(16) as u16)
828 },
829 bitrate_kbps,
830 duration_ms,
831 };
832
833 if let Some(expected) = expected
834 && expected != (sample_rate, channels)
835 {
836 log::info!(
837 "format change at {}: {}Hz/{}ch → {}Hz/{}ch, restarting audio engine",
838 path.display(),
839 expected.0,
840 expected.1,
841 sample_rate,
842 channels
843 );
844 return Ok(Decoded::FormatMismatch);
845 }
846
847 let mut symphonia_decoder = if is_opus_codec {
849 None
850 } else {
851 Some(
852 symphonia::default::get_codecs()
853 .make_audio_decoder(codec_params, &AudioDecoderOptions::default())
854 .map_err(|_| DecodeError::UnsupportedCodec)?,
855 )
856 };
857 let mut opus_bridge = if is_opus_codec {
858 Some(OpusBridge::new(codec_params).map_err(|e| DecodeError::Decode(e.to_string()))?)
859 } else {
860 None
861 };
862
863 let mut seek_samples = 0;
873 if seek_ms > 0 {
874 let seeked = reader
875 .seek(
876 SeekMode::Accurate,
877 SeekTo::Time {
878 time: Time::from_millis_u64(seek_ms),
879 track_id: Some(track_id),
880 },
881 )
882 .map_err(|e| DecodeError::Decode(format!("seek failed: {}", e)))?;
883 seek_samples = landing_samples(time_base, seeked.actual_ts, sample_rate, channels)
884 .unwrap_or(seek_ms * sample_rate as u64 * channels as u64 / 1000);
885 if let Some(ref mut dec) = symphonia_decoder {
886 dec.reset();
887 }
888 if let Some(ref mut opus) = opus_bridge {
889 opus.reset();
890 }
891 }
892
893 let write_offset = timeline.samples_written();
895 timeline.push_boundary(TrackBoundary {
896 id: queue_item_id,
897 path: path.to_path_buf(),
898 info,
899 sample_offset: write_offset,
900 samples_written: 0,
901 seek_samples,
902 });
903
904 let rg_gain = if rg_mode != ReplayGainMode::Off {
906 match crate::audio::replaygain::read_tags(path) {
907 Ok(rg_info) => {
908 let selected = crate::audio::replaygain::select_gain(&rg_info, rg_mode);
909 if let Some((gain_db, _)) = selected {
910 log::info!(
911 "replaygain: applying {:.2} dB ({:?}) to {}",
912 gain_db,
913 rg_mode,
914 path.display()
915 );
916 }
917 selected
918 }
919 Err(e) => {
920 log::debug!("replaygain: no tags for {}: {}", path.display(), e);
921 None
922 }
923 }
924 } else {
925 None
926 };
927 let mut rg_scratch: Vec<f32> = Vec::new();
928
929 let mut sample_buf: Vec<f32> = Vec::new();
930
931 loop {
932 if stop.load(Ordering::Relaxed) || !timeline.is_current() {
933 return Ok(Decoded::Complete((sample_rate, channels)));
934 }
935
936 let packet = match reader.next_packet() {
937 Ok(Some(p)) => p,
938 Ok(None) => return Ok(Decoded::Complete((sample_rate, channels))),
939 Err(e) => return Err(DecodeError::Decode(e.to_string())),
940 };
941
942 if packet.track_id != track_id {
943 continue;
944 }
945
946 let samples: &[f32] = if let Some(ref mut opus) = opus_bridge {
948 match opus.decode_packet(&packet.data) {
949 Ok(s) => s,
950 Err(e) => {
951 log::warn!("opus decode error (skipping packet): {}", e);
952 continue;
953 }
954 }
955 } else {
956 let decoder = symphonia_decoder.as_mut().unwrap();
957 let decoded = match decoder.decode(&packet) {
958 Ok(d) => d,
959 Err(symphonia::core::errors::Error::DecodeError(e)) => {
960 log::warn!("decode error (skipping packet): {}", e);
961 continue;
962 }
963 Err(e) => return Err(DecodeError::Decode(e.to_string())),
964 };
965
966 let spec = decoded.spec();
967 let (decoded_rate, decoded_channels) = (spec.rate(), spec.channels().count() as u16);
968 if (decoded_rate, decoded_channels) != (sample_rate, channels) {
972 log::warn!(
973 "{}: decoded {}Hz/{}ch but stream declares {}Hz/{}ch, restarting audio engine",
974 path.display(),
975 decoded_rate,
976 decoded_channels,
977 sample_rate,
978 channels
979 );
980 return Ok(Decoded::FormatMismatch);
981 }
982 decoded.copy_to_vec_interleaved(&mut sample_buf);
983 &sample_buf[..]
984 };
985
986 if samples.is_empty() {
987 continue;
988 }
989
990 let samples = if let Some((gain_db, peak)) = rg_gain {
993 rg_scratch.clear();
994 rg_scratch.extend_from_slice(samples);
995 crate::audio::replaygain::apply_gain(&mut rg_scratch, gain_db, peak, pre_amp_db);
996 &rg_scratch[..]
997 } else {
998 samples
999 };
1000
1001 let mut offset = 0;
1008 while offset < samples.len() {
1009 if stop.load(Ordering::Relaxed) || !timeline.is_current() {
1012 return Ok(Decoded::Complete((sample_rate, channels)));
1013 }
1014
1015 let slots = producer.slots();
1016 if slots == 0 {
1017 let half = producer.buffer().capacity() / 2;
1024 thread::park_timeout(time_to_play(half, Some((sample_rate, channels))));
1025 continue;
1026 }
1027
1028 let chunk_size = slots.min(samples.len() - offset);
1029 if let Ok(mut chunk) = producer.write_chunk_uninit(chunk_size) {
1030 let to_write = &samples[offset..offset + chunk_size];
1031 let (first, second) = chunk.as_mut_slices();
1032 let first_len = first.len().min(to_write.len());
1033 for (slot, &val) in first.iter_mut().zip(&to_write[..first_len]) {
1034 slot.write(val);
1035 }
1036 if first_len < to_write.len() {
1037 for (slot, &val) in second.iter_mut().zip(&to_write[first_len..]) {
1038 slot.write(val);
1039 }
1040 }
1041 unsafe { chunk.commit_all() };
1045
1046 if let Some(viz) = viz_buffer {
1048 viz.push_samples(to_write, channels, sample_rate);
1049 }
1050
1051 offset += chunk_size;
1052 }
1053 }
1054
1055 timeline.add_written(samples.len() as u64);
1056 }
1057}
1058
1059fn landing_samples(
1065 time_base: Option<TimeBase>,
1066 actual_ts: Timestamp,
1067 sample_rate: u32,
1068 channels: u16,
1069) -> Option<u64> {
1070 let (seconds, nanos) = time_base?.calc_time(actual_ts)?.parts();
1071 let rate = sample_rate as u64;
1072 let frames = seconds.max(0) as u64 * rate + (nanos as u64 * rate) / 1_000_000_000;
1073 Some(frames * channels as u64)
1074}
1075
1076pub(crate) fn track_duration_ms(
1083 reader: &(impl FormatReader + ?Sized),
1084 track: &Track,
1085 sample_rate: u32,
1086) -> u64 {
1087 fn to_ms(time_base: Option<TimeBase>, duration: Option<Duration>) -> Option<u64> {
1088 let time = time_base?.calc_duration(duration?)?;
1089 Some(time.as_millis().max(0) as u64)
1090 }
1091
1092 let media = reader.media_info();
1093 to_ms(track.time_base, track.duration)
1094 .or_else(|| to_ms(media.time_base, media.duration))
1095 .or_else(|| {
1096 track
1097 .num_frames
1098 .map(|frames| frames * 1000 / sample_rate as u64)
1099 })
1100 .unwrap_or(0)
1101}
1102
1103fn estimate_bitrate_from_codec_params(params: &AudioCodecParameters) -> Option<u32> {
1109 let is_lossy = matches!(
1110 params.codec,
1111 CODEC_ID_MP3 | CODEC_ID_AAC | CODEC_ID_VORBIS | CODEC_ID_OPUS
1112 );
1113 if !is_lossy {
1114 return None;
1115 }
1116
1117 let bpcs = params.bits_per_coded_sample?;
1120 let sr = params.sample_rate?;
1121 let channels = params
1122 .channels
1123 .as_ref()
1124 .map(|c| c.count() as u32)
1125 .unwrap_or(2);
1126 Some(bpcs * sr * channels / 1000)
1127}
1128
1129pub fn codec_name(codec: AudioCodecId) -> String {
1130 match codec {
1131 CODEC_ID_FLAC => "FLAC",
1132 CODEC_ID_MP3 => "MP3",
1133 CODEC_ID_AAC => "AAC",
1134 CODEC_ID_VORBIS => "Vorbis",
1135 CODEC_ID_OPUS => "Opus",
1136 CODEC_ID_ALAC => "ALAC",
1137 CODEC_ID_PCM_S16LE => "PCM/16",
1138 CODEC_ID_PCM_S24LE => "PCM/24",
1139 CODEC_ID_PCM_S32LE => "PCM/32",
1140 CODEC_ID_PCM_F32LE => "PCM/f32",
1141 other => return format!("Unknown({:?})", other),
1142 }
1143 .to_string()
1144}
1145
1146#[cfg(test)]
1147mod tests {
1148 use std::path::PathBuf;
1149 use std::sync::atomic::Ordering;
1150
1151 use super::*;
1152 use crate::player::state::QueueItemId;
1153
1154 fn make_info(sample_rate: u32, channels: u16) -> StreamInfo {
1155 StreamInfo {
1156 codec: "FLAC".to_string(),
1157 sample_rate,
1158 channels,
1159 bit_depth: Some(16),
1160 bitrate_kbps: None,
1161 duration_ms: 10_000,
1162 }
1163 }
1164
1165 fn make_boundary(
1166 id: QueueItemId,
1167 sample_offset: u64,
1168 seek_samples: u64,
1169 channels: u16,
1170 sample_rate: u32,
1171 ) -> TrackBoundary {
1172 TrackBoundary {
1173 id,
1174 path: PathBuf::from("/music/track.flac"),
1175 info: make_info(sample_rate, channels),
1176 sample_offset,
1177 samples_written: 0,
1178 seek_samples,
1179 }
1180 }
1181
1182 fn writer(timeline: &PlaybackTimeline) -> TimelineWriter<'_> {
1184 timeline.writer(timeline.generation())
1185 }
1186
1187 #[test]
1190 fn test_timeline_single_track() {
1191 let timeline = PlaybackTimeline::new();
1195 let tl = writer(&timeline);
1196 let id = QueueItemId::new();
1197 tl.push_boundary(make_boundary(id, 0, 0, 2, 44100));
1199 tl.add_written(88200); timeline.samples_played.store(88200, Ordering::Relaxed);
1203
1204 let result = timeline.current_playback();
1205 assert!(
1206 result.is_some(),
1207 "expected Some for single track with samples played"
1208 );
1209 let (result_id, _path, _info, position_ms) = result.unwrap();
1210 assert_eq!(result_id, id);
1211 assert_eq!(
1212 position_ms, 1000,
1213 "1 second of 44100 Hz stereo should be 1000 ms"
1214 );
1215 }
1216
1217 #[test]
1218 fn test_timeline_gapless_transition() {
1219 let timeline = PlaybackTimeline::new();
1223 let tl = writer(&timeline);
1224 let id1 = QueueItemId::new();
1225 let id2 = QueueItemId::new();
1226
1227 tl.push_boundary(make_boundary(id1, 0, 0, 2, 44100));
1229 tl.add_written(88200);
1230
1231 tl.push_boundary(make_boundary(id2, 88200, 0, 2, 44100));
1233 tl.add_written(44100); timeline.samples_played.store(90000, Ordering::Relaxed);
1237
1238 let result = timeline.current_playback();
1239 assert!(result.is_some());
1240 let (result_id, _path, _info, position_ms) = result.unwrap();
1241 assert_eq!(
1242 result_id, id2,
1243 "playback head past boundary should report second track"
1244 );
1245 assert_eq!(position_ms, 20, "position within track 2 should be ~20 ms");
1247 }
1248
1249 #[test]
1250 fn test_timeline_zero_samples() {
1251 let timeline = PlaybackTimeline::new();
1254 let tl = writer(&timeline);
1255 let id = QueueItemId::new();
1256 tl.push_boundary(make_boundary(id, 0, 0, 2, 44100));
1257 tl.add_written(1000);
1258 timeline.samples_played.store(0, Ordering::Relaxed);
1259
1260 let result = timeline.current_playback();
1261 assert!(
1262 result.is_some(),
1263 "expected Some at 0 samples played with a boundary at offset 0"
1264 );
1265 let (result_id, _path, _info, position_ms) = result.unwrap();
1266 assert_eq!(result_id, id);
1267 assert_eq!(position_ms, 0);
1268 }
1269
1270 #[test]
1271 fn test_timeline_past_all_boundaries() {
1272 let timeline = PlaybackTimeline::new();
1275 let tl = writer(&timeline);
1276 let id1 = QueueItemId::new();
1277 let id2 = QueueItemId::new();
1278
1279 tl.push_boundary(make_boundary(id1, 0, 0, 2, 44100));
1280 tl.add_written(88200);
1281 tl.push_boundary(make_boundary(id2, 88200, 0, 2, 44100));
1282 tl.add_written(88200);
1283
1284 timeline
1286 .samples_played
1287 .store(999_999_999, Ordering::Relaxed);
1288
1289 let result = timeline.current_playback();
1290 assert!(result.is_some());
1291 let (result_id, _path, _info, _position_ms) = result.unwrap();
1292 assert_eq!(
1293 result_id, id2,
1294 "samples past all boundaries should report the last track"
1295 );
1296 }
1297
1298 #[test]
1299 fn test_timeline_seek_offset() {
1300 let timeline = PlaybackTimeline::new();
1304 let tl = writer(&timeline);
1305 let id = QueueItemId::new();
1306 let seek_samples = 88200u64; tl.push_boundary(make_boundary(id, 0, seek_samples, 2, 44100));
1308 tl.add_written(44100); timeline.samples_played.store(0, Ordering::Relaxed);
1311
1312 let result = timeline.current_playback();
1313 assert!(result.is_some());
1314 let (_result_id, _path, _info, position_ms) = result.unwrap();
1315 assert_eq!(
1317 position_ms, 1000,
1318 "position should include seek offset of 1000 ms"
1319 );
1320 }
1321
1322 #[test]
1323 fn the_next_track_is_due_when_the_current_one_has_played_out() {
1324 let timeline = PlaybackTimeline::new();
1325 let tl = writer(&timeline);
1326 let (a, b) = (QueueItemId::new(), QueueItemId::new());
1327 tl.push_boundary(make_boundary(a, 0, 0, 2, 44100));
1329 assert_eq!(timeline.until_next_track(), None, "nothing queued after it");
1330 tl.push_boundary(make_boundary(b, 88200, 0, 2, 44100));
1331
1332 timeline.samples_played.store(44100, Ordering::Relaxed);
1333 assert_eq!(
1334 timeline.until_next_track(),
1335 Some(std::time::Duration::from_millis(500))
1336 );
1337 timeline.samples_played.store(88200, Ordering::Relaxed);
1338 assert_eq!(timeline.until_next_track(), None, "b is playing now");
1339 }
1340
1341 #[test]
1342 fn the_queued_callback_hears_each_track() {
1343 let timeline = PlaybackTimeline::new();
1344 let heard = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1345 let counter = heard.clone();
1346 timeline.on_queued(move || {
1347 counter.fetch_add(1, Ordering::Relaxed);
1348 });
1349 let tl = writer(&timeline);
1350 tl.push_boundary(make_boundary(QueueItemId::new(), 0, 0, 2, 44100));
1351 tl.push_boundary(make_boundary(QueueItemId::new(), 88200, 0, 2, 44100));
1352 assert_eq!(heard.load(Ordering::Relaxed), 2);
1353 }
1354
1355 #[test]
1356 fn a_track_played_past_reads_as_played_to_its_end() {
1357 let timeline = PlaybackTimeline::new();
1358 let tl = writer(&timeline);
1359 let (a, b) = (QueueItemId::new(), QueueItemId::new());
1360 tl.push_boundary(make_boundary(a, 0, 0, 2, 44100));
1361 tl.push_boundary(make_boundary(b, 88200, 0, 2, 44100));
1362 timeline
1363 .samples_played
1364 .store(88200 + 44100, Ordering::Relaxed);
1365
1366 assert_eq!(timeline.position_of(a), Some(1000));
1367 assert_eq!(timeline.position_of(b), Some(500));
1368 assert_eq!(timeline.playhead(), Some((b, 500)));
1369 }
1370
1371 #[test]
1372 fn test_timeline_reset() {
1373 let timeline = PlaybackTimeline::new();
1375 let tl = writer(&timeline);
1376 let id = QueueItemId::new();
1377 tl.push_boundary(make_boundary(id, 0, 0, 2, 44100));
1378 tl.add_written(88200);
1379 timeline.samples_played.store(44100, Ordering::Relaxed);
1380
1381 assert!(timeline.current_playback().is_some());
1383
1384 timeline.reset();
1385
1386 assert!(
1387 timeline.current_playback().is_none(),
1388 "after reset, current_playback should return None"
1389 );
1390 assert_eq!(
1391 timeline.samples_played.load(Ordering::Relaxed),
1392 0,
1393 "samples_played should be 0 after reset"
1394 );
1395 assert_eq!(
1396 timeline.samples_written.load(Ordering::Relaxed),
1397 0,
1398 "samples_written should be 0 after reset"
1399 );
1400 }
1401
1402 #[test]
1405 fn probe_file_extracts_stream_info() {
1406 let dir = tempfile::tempdir().unwrap();
1407 let wav_path = dir.path().join("probe_test.wav");
1408 crate::test_utils::generate_wav(&wav_path, 44100, 2, 1.0, 16);
1409
1410 let info = probe_file(&wav_path).expect("probe_file should succeed on a valid WAV");
1411 assert_eq!(info.sample_rate, 44100, "sample rate mismatch");
1412 assert_eq!(info.channels, 2, "channel count mismatch");
1413 assert_eq!(info.bit_depth, Some(16), "bit depth mismatch");
1414 assert!(
1415 info.duration_ms > 900 && info.duration_ms < 1100,
1416 "duration should be ~1000ms, got {}",
1417 info.duration_ms
1418 );
1419 assert!(
1420 info.codec.contains("PCM"),
1421 "codec should be PCM variant, got {}",
1422 info.codec
1423 );
1424 }
1425
1426 #[test]
1427 fn decode_single_produces_samples() {
1428 let dir = tempfile::tempdir().unwrap();
1429 let wav_path = dir.path().join("tone.wav");
1430 crate::test_utils::generate_wav_tone(&wav_path, 44100, 440.0, 0.1);
1432
1433 let (mut producer, mut consumer) = rtrb::RingBuffer::new(44100 * 2);
1435
1436 let timeline = PlaybackTimeline::new();
1437 let tl = writer(&timeline);
1438 let stop = Arc::new(AtomicBool::new(false));
1439
1440 let id = QueueItemId::new();
1441 let entry = SourceEntry::from_file(id, wav_path.clone());
1442 let hint = entry.hint.clone();
1443 let mss = (entry.make_mss)().expect("should open WAV file");
1444
1445 let result = decode_single(
1446 id,
1447 &wav_path,
1448 &hint,
1449 mss,
1450 &mut producer,
1451 &stop,
1452 0,
1453 &tl,
1454 None,
1455 crate::config::ReplayGainMode::Off,
1456 0.0,
1457 None,
1458 );
1459 assert!(
1460 matches!(result, Ok(Decoded::Complete((44100, 1)))),
1461 "decode_single should complete at the source format"
1462 );
1463
1464 let available = consumer.slots();
1466 assert!(available > 0, "expected samples in ring buffer, got 0");
1467
1468 let mut found_nonzero = false;
1470 while consumer.slots() > 0 {
1471 if let Ok(chunk) = consumer.read_chunk(consumer.slots().min(1024)) {
1472 let (first, second) = chunk.as_slices();
1473 for &s in first.iter().chain(second.iter()) {
1474 if s.abs() > 0.001 {
1475 found_nonzero = true;
1476 break;
1477 }
1478 }
1479 chunk.commit_all();
1480 }
1481 if found_nonzero {
1482 break;
1483 }
1484 }
1485 assert!(
1486 found_nonzero,
1487 "expected non-zero samples from 440Hz sine decode"
1488 );
1489 }
1490
1491 fn run_queue(paths: &[PathBuf]) -> Vec<TrackBoundary> {
1497 let (producer, mut consumer) = rtrb::RingBuffer::new(1 << 16);
1498 let timeline = PlaybackTimeline::new();
1499 let tl = writer(&timeline);
1500 let stop = Arc::new(AtomicBool::new(false));
1501
1502 let drain_stop = Arc::new(AtomicBool::new(false));
1503 let drain_flag = drain_stop.clone();
1504 let drainer = std::thread::spawn(move || {
1505 while !drain_flag.load(Ordering::Relaxed) {
1506 let n = consumer.slots();
1507 if n > 0
1508 && let Ok(chunk) = consumer.read_chunk(n)
1509 {
1510 chunk.commit_all();
1511 }
1512 std::thread::sleep(std::time::Duration::from_micros(200));
1513 }
1514 });
1515
1516 let rest: std::sync::Mutex<Vec<PathBuf>> = std::sync::Mutex::new(paths[1..].to_vec());
1517 let next_track = move || {
1518 let mut rest = rest.lock().ok()?;
1519 if rest.is_empty() {
1520 return None;
1521 }
1522 Some(SourceEntry::from_file(QueueItemId::new(), rest.remove(0)))
1523 };
1524
1525 decode_queue_loop(
1526 SourceEntry::from_file(QueueItemId::new(), paths[0].clone()),
1527 producer,
1528 &stop,
1529 0,
1530 &next_track,
1531 &tl,
1532 None,
1533 crate::config::ReplayGainMode::Off,
1534 0.0,
1535 );
1536
1537 drain_stop.store(true, Ordering::Relaxed);
1538 drainer.join().unwrap();
1539
1540 timeline.boundaries.read().clone()
1541 }
1542
1543 #[test]
1544 fn gapless_continues_when_format_matches() {
1545 let dir = tempfile::tempdir().unwrap();
1546 let a = dir.path().join("a.wav");
1547 let b = dir.path().join("b.wav");
1548 crate::test_utils::generate_wav(&a, 44100, 2, 0.1, 16);
1549 crate::test_utils::generate_wav(&b, 44100, 2, 0.1, 16);
1550
1551 let bounds = run_queue(&[a, b]);
1552 assert_eq!(
1553 bounds.len(),
1554 2,
1555 "same-format tracks should decode gaplessly"
1556 );
1557 }
1558
1559 #[test]
1560 fn gapless_stops_at_sample_rate_change() {
1561 let dir = tempfile::tempdir().unwrap();
1562 let a = dir.path().join("a.wav");
1563 let b = dir.path().join("b.wav");
1564 crate::test_utils::generate_wav(&a, 44100, 2, 0.1, 16);
1565 crate::test_utils::generate_wav(&b, 48000, 2, 0.1, 16);
1566
1567 let bounds = run_queue(&[a, b]);
1568 assert_eq!(
1569 bounds.len(),
1570 1,
1571 "a 48kHz track must not join a 44.1kHz ring buffer"
1572 );
1573 assert_eq!(bounds[0].info.sample_rate, 44100);
1574 }
1575
1576 #[test]
1577 fn gapless_stops_at_channel_change() {
1578 let dir = tempfile::tempdir().unwrap();
1579 let a = dir.path().join("a.wav");
1580 let b = dir.path().join("b.wav");
1581 crate::test_utils::generate_wav(&a, 44100, 2, 0.1, 16);
1582 crate::test_utils::generate_wav(&b, 44100, 1, 0.1, 16);
1583
1584 let bounds = run_queue(&[a, b]);
1585 assert_eq!(
1586 bounds.len(),
1587 1,
1588 "a mono track must not join a stereo ring buffer"
1589 );
1590 assert_eq!(bounds[0].info.channels, 2);
1591 }
1592
1593 #[test]
1594 fn drain_waits_for_the_consumer() {
1595 let (mut producer, mut consumer) = rtrb::RingBuffer::new(64);
1596 for _ in 0..64 {
1597 producer.push(0.0).unwrap();
1598 }
1599 let stop = Arc::new(AtomicBool::new(false));
1600
1601 let reader = std::thread::spawn(move || {
1602 std::thread::sleep(std::time::Duration::from_millis(20));
1603 let chunk = consumer.read_chunk(64).unwrap();
1604 chunk.commit_all();
1605 consumer
1606 });
1607
1608 wait_for_drain(&producer, &stop, None);
1609 assert_eq!(producer.slots(), 64, "drain must wait for an empty buffer");
1610 drop(reader.join().unwrap());
1611 }
1612
1613 #[test]
1614 fn drain_returns_when_playback_is_torn_down() {
1615 let (producer, consumer) = rtrb::RingBuffer::<f32>::new(64);
1616 let stop = Arc::new(AtomicBool::new(true));
1617 wait_for_drain(&producer, &stop, None);
1618 drop(consumer);
1619 }
1620
1621 #[test]
1624 fn a_writer_knows_when_its_session_has_ended() {
1625 let timeline = PlaybackTimeline::new();
1626 let tl = writer(&timeline);
1627 assert!(tl.is_current());
1628
1629 timeline.reset();
1630 assert!(!tl.is_current(), "reset must retire the outgoing writer");
1631 assert!(writer(&timeline).is_current());
1632 }
1633
1634 #[test]
1635 fn stale_writes_are_dropped_after_reset() {
1636 let timeline = PlaybackTimeline::new();
1637 let dying = writer(&timeline);
1638 dying.push_boundary(make_boundary(QueueItemId::new(), 0, 0, 2, 44100));
1639 dying.add_written(88200);
1640
1641 timeline.reset();
1642
1643 dying.add_written(4608);
1646 dying.push_boundary(make_boundary(QueueItemId::new(), 0, 0, 2, 44100));
1647
1648 assert_eq!(timeline.samples_written.load(Ordering::Relaxed), 0);
1649 assert!(timeline.boundaries.read().is_empty());
1650 }
1651
1652 #[test]
1653 fn a_dying_decode_thread_cannot_blank_the_transport() {
1654 let timeline = PlaybackTimeline::new();
1655 let dying = writer(&timeline);
1656 dying.push_boundary(make_boundary(QueueItemId::new(), 0, 0, 2, 44100));
1657 dying.add_written(88200);
1658
1659 timeline.reset();
1662 dying.add_written(4608);
1663
1664 let fresh = writer(&timeline);
1665 let id = QueueItemId::new();
1666 let write_offset = fresh.samples_written();
1667 fresh.push_boundary(make_boundary(id, write_offset, 0, 2, 44100));
1668
1669 assert_eq!(write_offset, 0, "first boundary must start at 0");
1670 let (playing, _, _, position_ms) = timeline
1671 .current_playback()
1672 .expect("transport must not go blank at samples_played = 0");
1673 assert_eq!(playing, id);
1674 assert_eq!(position_ms, 0);
1675 }
1676
1677 fn write_garbage(path: &Path) {
1681 std::fs::write(path, b"this is not a wav file").unwrap();
1682 }
1683
1684 #[test]
1685 fn an_unreadable_track_is_skipped_and_the_queue_continues() {
1686 let dir = tempfile::tempdir().unwrap();
1687 let a = dir.path().join("a.wav");
1688 let bad = dir.path().join("bad.wav");
1689 let c = dir.path().join("c.wav");
1690 crate::test_utils::generate_wav(&a, 44100, 2, 0.1, 16);
1691 write_garbage(&bad);
1692 crate::test_utils::generate_wav(&c, 44100, 2, 0.1, 16);
1693
1694 let bounds = run_queue(&[a.clone(), bad, c.clone()]);
1695 let decoded: Vec<_> = bounds.iter().map(|b| b.path.clone()).collect();
1696 assert_eq!(
1697 decoded,
1698 vec![a, c],
1699 "one bad file must not take the rest of the queue with it"
1700 );
1701 }
1702
1703 #[test]
1704 fn a_missing_track_is_skipped_and_the_queue_continues() {
1705 let dir = tempfile::tempdir().unwrap();
1706 let missing = dir.path().join("gone.wav");
1707 let b = dir.path().join("b.wav");
1708 crate::test_utils::generate_wav(&b, 44100, 2, 0.1, 16);
1709
1710 let bounds = run_queue(&[missing, b.clone()]);
1711 assert_eq!(bounds.len(), 1);
1712 assert_eq!(
1713 bounds[0].path, b,
1714 "a bad first track must not end the session"
1715 );
1716 }
1717
1718 #[test]
1719 fn an_entirely_unreadable_queue_terminates() {
1720 let dir = tempfile::tempdir().unwrap();
1721 let bad = dir.path().join("bad.wav");
1722 write_garbage(&bad);
1723
1724 let (producer, _consumer) = rtrb::RingBuffer::<f32>::new(1 << 12);
1727 let timeline = PlaybackTimeline::new();
1728 let tl = writer(&timeline);
1729 let stop = Arc::new(AtomicBool::new(false));
1730
1731 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1732 let counter = calls.clone();
1733 let bad_path = bad.clone();
1734 let next_track = move || {
1735 counter.fetch_add(1, Ordering::Relaxed);
1736 Some(SourceEntry::from_file(QueueItemId::new(), bad_path.clone()))
1737 };
1738
1739 decode_queue_loop(
1740 SourceEntry::from_file(QueueItemId::new(), bad),
1741 producer,
1742 &stop,
1743 0,
1744 &next_track,
1745 &tl,
1746 None,
1747 crate::config::ReplayGainMode::Off,
1748 0.0,
1749 );
1750
1751 assert_eq!(
1753 calls.load(Ordering::Relaxed) + 1,
1754 MAX_CONSECUTIVE_FAILURES as usize
1755 );
1756 assert!(timeline.boundaries.read().is_empty());
1757 }
1758
1759 #[test]
1762 fn landing_samples_converts_frame_timebases() {
1763 let tb = TimeBase::try_from_recip(44100).unwrap();
1765 assert_eq!(
1766 landing_samples(Some(tb), Timestamp::from(44100u32), 44100, 2),
1767 Some(88_200)
1768 );
1769 }
1770
1771 #[test]
1772 fn landing_samples_converts_millisecond_timebases() {
1773 let tb = TimeBase::try_new(1, 1000).unwrap();
1775 assert_eq!(
1776 landing_samples(Some(tb), Timestamp::from(1500u32), 48000, 2),
1777 Some(48000 * 3 / 2 * 2)
1778 );
1779 }
1780
1781 #[test]
1782 fn landing_samples_needs_a_timebase() {
1783 assert_eq!(
1784 landing_samples(None, Timestamp::from(1000u32), 44100, 2),
1785 None
1786 );
1787 }
1788
1789 #[cfg(test)]
1792 fn make_vbr_mp3(dir: &Path) -> PathBuf {
1793 let wav = dir.join("source.wav");
1794 let mp3 = dir.join("source.mp3");
1795 let ok = std::process::Command::new("sox")
1796 .args(["-n", "-r", "44100", "-c", "2"])
1797 .arg(&wav)
1798 .args([
1799 "synth", "30", "sine", "200", "vol", "0.02", ":", "synth", "270", "sine", "880",
1800 "vol", "0.9",
1801 ])
1802 .status()
1803 .expect("sox not installed")
1804 .success();
1805 assert!(ok, "sox failed");
1806 let ok = std::process::Command::new("lame")
1807 .args(["-V", "2", "--quiet"])
1808 .arg(&wav)
1809 .arg(&mp3)
1810 .status()
1811 .expect("lame not installed")
1812 .success();
1813 assert!(ok, "lame failed");
1814 mp3
1815 }
1816
1817 #[test]
1822 #[ignore = "generates a fixture with sox + lame; run with cargo test -- --ignored"]
1823 fn seek_on_vbr_reports_where_it_landed() {
1824 let dir = tempfile::tempdir().unwrap();
1825 let path = make_vbr_mp3(dir.path());
1826 let info = probe_file(&path).unwrap();
1827 let channels = info.channels as u64;
1828 let rate = info.sample_rate as u64;
1829
1830 let seek_ms = 150_000u64;
1831 let (mut producer, mut consumer) = rtrb::RingBuffer::<f32>::new(1 << 16);
1832 let stop = Arc::new(AtomicBool::new(false));
1833 let timeline = PlaybackTimeline::new();
1834 let tl = writer(&timeline);
1835
1836 let drain_stop = stop.clone();
1837 let drained = std::thread::spawn(move || {
1838 let mut total = 0u64;
1839 while !drain_stop.load(Ordering::Relaxed) {
1840 let slots = consumer.slots();
1841 if slots == 0 {
1842 std::thread::sleep(std::time::Duration::from_micros(200));
1843 continue;
1844 }
1845 let chunk = consumer.read_chunk(slots).unwrap();
1846 total += slots as u64;
1847 chunk.commit_all();
1848 }
1849 total
1850 });
1851
1852 let file = File::open(&path).unwrap();
1853 let mss = MediaSourceStream::new(Box::new(file), Default::default());
1854 let mut hint = Hint::new();
1855 hint.with_extension("mp3");
1856 decode_single(
1857 QueueItemId::new(),
1858 &path,
1859 &hint,
1860 mss,
1861 &mut producer,
1862 &stop,
1863 seek_ms,
1864 &tl,
1865 None,
1866 ReplayGainMode::Off,
1867 0.0,
1868 None,
1869 )
1870 .unwrap();
1871
1872 let written = timeline.samples_written.load(Ordering::Relaxed);
1873 stop.store(true, Ordering::Relaxed);
1874 drained.join().unwrap();
1875
1876 let reported_start_ms = {
1877 let bounds = timeline.boundaries.read();
1878 (bounds[0].seek_samples / channels) * 1000 / rate
1879 };
1880 let decoded_ms = (written / channels) * 1000 / rate;
1881
1882 let total_ms = reported_start_ms + decoded_ms;
1884 assert!(
1885 total_ms.abs_diff(info.duration_ms) < 500,
1886 "reported start {}ms + {}ms decoded = {}ms, but the file is {}ms",
1887 reported_start_ms,
1888 decoded_ms,
1889 total_ms,
1890 info.duration_ms
1891 );
1892 assert!(
1894 reported_start_ms.abs_diff(seek_ms) < 100,
1895 "seek to {}ms reported {}ms",
1896 seek_ms,
1897 reported_start_ms
1898 );
1899 }
1900}