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::audio::SampleBuffer;
8use symphonia::core::codecs::{
9 CODEC_TYPE_AAC, CODEC_TYPE_ALAC, CODEC_TYPE_FLAC, CODEC_TYPE_MP3, CODEC_TYPE_OPUS,
10 CODEC_TYPE_PCM_F32LE, CODEC_TYPE_PCM_S16LE, CODEC_TYPE_PCM_S24LE, CODEC_TYPE_PCM_S32LE,
11 CODEC_TYPE_VORBIS, CODEC_TYPE_WAVPACK, CodecType, DecoderOptions,
12};
13use symphonia::core::formats::{FormatOptions, SeekMode, SeekTo};
14use symphonia::core::io::MediaSourceStream;
15use symphonia::core::meta::MetadataOptions;
16use symphonia::core::probe::Hint;
17use symphonia::core::units::Time;
18use thiserror::Error;
19
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: u16,
43 pub duration_ms: u64,
44}
45
46pub struct DecodeHandle {
48 stop: Arc<AtomicBool>,
49 thread: Option<thread::JoinHandle<()>>,
50}
51
52impl DecodeHandle {
53 pub fn signal_stop(&self) {
55 self.stop.store(true, Ordering::Relaxed);
56 }
57
58 #[cfg(test)]
60 pub fn new_for_test(stop: Arc<AtomicBool>) -> Self {
61 Self { stop, thread: None }
62 }
63
64 pub fn stop(&mut self) {
66 self.signal_stop();
67 if let Some(handle) = self.thread.take()
68 && let Err(payload) = handle.join()
69 {
70 let msg = payload
71 .downcast_ref::<String>()
72 .map(|s| s.as_str())
73 .or_else(|| payload.downcast_ref::<&str>().copied())
74 .unwrap_or("unknown");
75 log::error!("decode thread panicked: {}", msg);
76 }
77 }
78}
79
80impl Drop for DecodeHandle {
81 fn drop(&mut self) {
82 self.stop();
83 }
84}
85
86#[derive(Debug, Clone)]
91pub struct TrackBoundary {
92 pub id: QueueItemId,
93 pub path: PathBuf,
94 pub info: StreamInfo,
95 pub sample_offset: u64,
99 pub samples_written: u64,
102 pub seek_samples: u64,
104}
105
106pub struct PlaybackTimeline {
110 boundaries: parking_lot::RwLock<Vec<TrackBoundary>>,
111 samples_written: AtomicU64,
113 pub samples_played: Arc<AtomicU64>,
116}
117
118impl PlaybackTimeline {
119 pub fn new() -> Arc<Self> {
120 Arc::new(Self {
121 boundaries: parking_lot::RwLock::new(Vec::new()),
122 samples_written: AtomicU64::new(0),
123 samples_played: Arc::new(AtomicU64::new(0)),
124 })
125 }
126
127 fn push_boundary(&self, boundary: TrackBoundary) {
129 self.boundaries.write().push(boundary);
130 }
131
132 fn add_written(&self, count: u64) {
134 self.samples_written.fetch_add(count, Ordering::Relaxed);
135 let mut bounds = self.boundaries.write();
137 if let Some(last) = bounds.last_mut() {
138 last.samples_written += count;
139 }
140 }
141
142 pub fn reset(&self) {
144 self.boundaries.write().clear();
145 self.samples_written.store(0, Ordering::Relaxed);
146 self.samples_played.store(0, Ordering::Relaxed);
147 }
148
149 pub fn samples_played_counter(&self) -> Arc<AtomicU64> {
151 self.samples_played.clone()
152 }
153
154 pub fn current_playback(&self) -> Option<(QueueItemId, PathBuf, StreamInfo, u64)> {
163 let bounds = self.boundaries.read();
165
166 if bounds.is_empty() {
167 return None;
168 }
169
170 let played = self.samples_played.load(Ordering::Acquire);
174
175 let idx = bounds.partition_point(|b| b.sample_offset <= played);
179 let current = if idx > 0 {
180 &bounds[idx - 1]
181 } else {
182 return None;
183 };
184
185 let ch = current.info.channels as u64;
186 let rate = current.info.sample_rate as u64;
187 if ch == 0 || rate == 0 {
188 return None;
189 }
190
191 let track_samples = played.saturating_sub(current.sample_offset);
194 let position_ms =
195 (track_samples / ch) * 1000 / rate + (current.seek_samples / ch) * 1000 / rate;
196
197 Some((
198 current.id,
199 current.path.clone(),
200 current.info.clone(),
201 position_ms,
202 ))
203 }
204}
205
206pub struct SourceEntry {
215 pub id: QueueItemId,
216 pub path: PathBuf,
218 pub hint: Hint,
220 pub make_mss: Box<dyn FnOnce() -> std::io::Result<MediaSourceStream> + Send>,
222}
223
224impl SourceEntry {
225 pub fn from_file(id: QueueItemId, path: PathBuf) -> Self {
227 let ext = path
228 .extension()
229 .and_then(|e| e.to_str())
230 .unwrap_or("")
231 .to_string();
232 let path_clone = path.clone();
233 let mut hint = Hint::new();
234 if !ext.is_empty() {
235 hint.with_extension(&ext);
236 }
237 Self {
238 id,
239 path,
240 hint,
241 make_mss: Box::new(move || {
242 let file = File::open(&path_clone)?;
243 Ok(MediaSourceStream::new(Box::new(file), Default::default()))
244 }),
245 }
246 }
247}
248
249pub fn probe_source(mss: MediaSourceStream, hint: &Hint) -> Result<StreamInfo, DecodeError> {
255 probe_mss(mss, hint)
256}
257
258pub fn probe_file(path: &Path) -> Result<StreamInfo, DecodeError> {
260 let file = File::open(path)?;
261 let mss = MediaSourceStream::new(Box::new(file), Default::default());
262 let mut hint = Hint::new();
263 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
264 hint.with_extension(ext);
265 }
266 probe_mss(mss, &hint)
267}
268
269fn probe_mss(mss: MediaSourceStream, hint: &Hint) -> Result<StreamInfo, DecodeError> {
271 let probed = symphonia::default::get_probe()
272 .format(
273 hint,
274 mss,
275 &FormatOptions::default(),
276 &MetadataOptions::default(),
277 )
278 .map_err(|e| DecodeError::Decode(e.to_string()))?;
279
280 let reader = probed.format;
281 let track = reader.default_track().ok_or(DecodeError::NoTrack)?;
282 let codec_params = &track.codec_params;
283 let sample_rate = codec_params.sample_rate.unwrap_or(44100);
284 let channels = codec_params.channels.map(|c| c.count() as u16).unwrap_or(2);
285 let bit_depth = codec_params.bits_per_sample.unwrap_or(16) as u16;
286 let duration_ms = track
287 .codec_params
288 .n_frames
289 .map(|frames| frames * 1000 / sample_rate as u64)
290 .unwrap_or(0);
291 let codec = codec_name(codec_params.codec);
292
293 Ok(StreamInfo {
294 codec,
295 sample_rate,
296 channels,
297 bit_depth,
298 duration_ms,
299 })
300}
301
302#[allow(clippy::too_many_arguments)]
313pub fn start_decode<N, F>(
314 first: SourceEntry,
315 producer: rtrb::Producer<f32>,
316 seek_ms: u64,
317 next_track: N,
318 timeline: Arc<PlaybackTimeline>,
319 viz_buffer: Option<Arc<VizBuffer>>,
320 rg_mode: ReplayGainMode,
321 pre_amp_db: f64,
322 on_finished: F,
323) -> Result<(StreamInfo, DecodeHandle), DecodeError>
324where
325 N: Fn() -> Option<SourceEntry> + Send + 'static,
326 F: FnOnce() + Send + 'static,
327{
328 let stop = Arc::new(AtomicBool::new(false));
329 let stop_clone = stop.clone();
330
331 let thread = thread::Builder::new()
332 .name("koan-decode".into())
333 .spawn(move || {
334 decode_queue_loop(
335 first,
336 producer,
337 &stop_clone,
338 seek_ms,
339 &next_track,
340 &timeline,
341 viz_buffer.as_deref(),
342 rg_mode,
343 pre_amp_db,
344 );
345 if !stop_clone.load(Ordering::Relaxed) {
349 on_finished();
350 }
351 })
352 .map_err(DecodeError::Io)?;
353
354 let placeholder = StreamInfo {
357 codec: String::from("?"),
358 sample_rate: 44100,
359 channels: 2,
360 bit_depth: 16,
361 duration_ms: 0,
362 };
363
364 Ok((
365 placeholder,
366 DecodeHandle {
367 stop,
368 thread: Some(thread),
369 },
370 ))
371}
372
373#[allow(clippy::too_many_arguments)]
383pub fn start_decode_file<N, F>(
384 initial_id: QueueItemId,
385 path: &Path,
386 producer: rtrb::Producer<f32>,
387 seek_ms: u64,
388 next_track: N,
389 timeline: Arc<PlaybackTimeline>,
390 viz_buffer: Option<Arc<VizBuffer>>,
391 rg_mode: ReplayGainMode,
392 pre_amp_db: f64,
393 on_finished: F,
394) -> Result<(StreamInfo, DecodeHandle), DecodeError>
395where
396 N: Fn() -> Option<(QueueItemId, PathBuf)> + Send + 'static,
397 F: FnOnce() + Send + 'static,
398{
399 let info = probe_file(path)?;
400 let first = SourceEntry::from_file(initial_id, path.to_path_buf());
401 let (_, handle) = start_decode(
402 first,
403 producer,
404 seek_ms,
405 move || {
406 let (id, p) = next_track()?;
407 Some(SourceEntry::from_file(id, p))
408 },
409 timeline,
410 viz_buffer,
411 rg_mode,
412 pre_amp_db,
413 on_finished,
414 )?;
415 Ok((info, handle))
416}
417
418#[allow(clippy::too_many_arguments)]
424fn decode_queue_loop<N>(
425 first: SourceEntry,
426 mut producer: rtrb::Producer<f32>,
427 stop: &AtomicBool,
428 initial_seek_ms: u64,
429 next_track: &N,
430 timeline: &PlaybackTimeline,
431 viz_buffer: Option<&VizBuffer>,
432 rg_mode: ReplayGainMode,
433 pre_amp_db: f64,
434) where
435 N: Fn() -> Option<SourceEntry>,
436{
437 let path = first.path.clone();
438 let hint = first.hint.clone();
439 let mss = match (first.make_mss)() {
440 Ok(mss) => mss,
441 Err(e) => {
442 if !stop.load(Ordering::Relaxed) {
443 log::error!("failed to open {}: {}", path.display(), e);
444 }
445 return;
446 }
447 };
448
449 if let Err(e) = decode_single(
450 first.id,
451 &path,
452 &hint,
453 mss,
454 &mut producer,
455 stop,
456 initial_seek_ms,
457 timeline,
458 viz_buffer,
459 rg_mode,
460 pre_amp_db,
461 ) {
462 if !stop.load(Ordering::Relaxed) {
463 log::error!("decode error on {}: {}", path.display(), e);
464 }
465 return;
466 }
467
468 while !stop.load(Ordering::Relaxed) {
469 let Some(entry) = (next_track)() else {
470 log::info!("playlist exhausted, decode thread finishing");
471 break;
472 };
473
474 log::info!("gapless transition → {}", entry.path.display());
475 let next_path = entry.path.clone();
476 let next_hint = entry.hint.clone();
477 let next_mss = match (entry.make_mss)() {
478 Ok(mss) => mss,
479 Err(e) => {
480 if !stop.load(Ordering::Relaxed) {
481 log::error!("failed to open {}: {}", next_path.display(), e);
482 }
483 break;
484 }
485 };
486
487 if let Err(e) = decode_single(
488 entry.id,
489 &next_path,
490 &next_hint,
491 next_mss,
492 &mut producer,
493 stop,
494 0,
495 timeline,
496 viz_buffer,
497 rg_mode,
498 pre_amp_db,
499 ) {
500 if !stop.load(Ordering::Relaxed) {
501 log::error!("decode error on {}: {}", next_path.display(), e);
502 }
503 break;
504 }
505 }
506}
507
508#[allow(clippy::too_many_arguments)]
514fn decode_single(
515 queue_item_id: QueueItemId,
516 path: &Path,
517 hint: &Hint,
518 mss: MediaSourceStream,
519 producer: &mut rtrb::Producer<f32>,
520 stop: &AtomicBool,
521 seek_ms: u64,
522 timeline: &PlaybackTimeline,
523 viz_buffer: Option<&VizBuffer>,
524 rg_mode: ReplayGainMode,
525 pre_amp_db: f64,
526) -> Result<(), DecodeError> {
527 let format_opts = FormatOptions {
528 enable_gapless: true,
529 ..Default::default()
530 };
531
532 let probed = symphonia::default::get_probe()
533 .format(hint, mss, &format_opts, &MetadataOptions::default())
534 .map_err(|e| DecodeError::Decode(e.to_string()))?;
535
536 let mut reader = probed.format;
537 let track = reader.default_track().ok_or(DecodeError::NoTrack)?;
538 let track_id = track.id;
539 let codec_params = &track.codec_params;
540 let sample_rate = codec_params.sample_rate.unwrap_or(44100);
541 let channels = codec_params.channels.map(|c| c.count() as u16).unwrap_or(2);
542
543 let info = StreamInfo {
544 codec: codec_name(codec_params.codec),
545 sample_rate,
546 channels,
547 bit_depth: codec_params.bits_per_sample.unwrap_or(16) as u16,
548 duration_ms: codec_params
549 .n_frames
550 .map(|f| f * 1000 / sample_rate as u64)
551 .unwrap_or(0),
552 };
553
554 let seek_samples = seek_ms * sample_rate as u64 * channels as u64 / 1000;
555
556 let write_offset = timeline.samples_written.load(Ordering::Relaxed);
558 timeline.push_boundary(TrackBoundary {
559 id: queue_item_id,
560 path: path.to_path_buf(),
561 info,
562 sample_offset: write_offset,
563 samples_written: 0,
564 seek_samples,
565 });
566
567 let mut decoder = symphonia::default::get_codecs()
568 .make(&track.codec_params, &DecoderOptions::default())
569 .map_err(|_| DecodeError::UnsupportedCodec)?;
570
571 if seek_ms > 0 {
573 let secs = seek_ms / 1000;
574 let frac = (seek_ms % 1000) as f64 / 1000.0;
575 reader
576 .seek(
577 SeekMode::Coarse,
578 SeekTo::Time {
579 time: Time::new(secs, frac),
580 track_id: Some(track_id),
581 },
582 )
583 .map_err(|e| DecodeError::Decode(format!("seek failed: {}", e)))?;
584 decoder.reset();
585 }
586
587 let rg_gain = if rg_mode != ReplayGainMode::Off {
589 match crate::audio::replaygain::read_tags(path) {
590 Ok(rg_info) => {
591 let selected = crate::audio::replaygain::select_gain(&rg_info, rg_mode);
592 if let Some((gain_db, _)) = selected {
593 log::info!(
594 "replaygain: applying {:.2} dB ({:?}) to {}",
595 gain_db,
596 rg_mode,
597 path.display()
598 );
599 }
600 selected
601 }
602 Err(e) => {
603 log::debug!("replaygain: no tags for {}: {}", path.display(), e);
604 None
605 }
606 }
607 } else {
608 None
609 };
610 let mut rg_scratch: Vec<f32> = Vec::new();
611
612 let mut sample_buf: Option<SampleBuffer<f32>> = None;
613
614 loop {
615 if stop.load(Ordering::Relaxed) {
616 return Ok(());
617 }
618
619 let packet = match reader.next_packet() {
620 Ok(p) => p,
621 Err(symphonia::core::errors::Error::IoError(ref e))
622 if e.kind() == std::io::ErrorKind::UnexpectedEof =>
623 {
624 return Ok(());
625 }
626 Err(e) => return Err(DecodeError::Decode(e.to_string())),
627 };
628
629 if packet.track_id() != track_id {
630 continue;
631 }
632
633 let decoded = match decoder.decode(&packet) {
634 Ok(d) => d,
635 Err(symphonia::core::errors::Error::DecodeError(e)) => {
636 log::warn!("decode error (skipping packet): {}", e);
637 continue;
638 }
639 Err(e) => return Err(DecodeError::Decode(e.to_string())),
640 };
641
642 let spec = *decoded.spec();
643 let duration = decoded.capacity();
644
645 let sbuf = sample_buf.get_or_insert_with(|| SampleBuffer::new(duration as u64, spec));
646
647 sbuf.copy_interleaved_ref(decoded);
648
649 let samples = if let Some((gain_db, peak)) = rg_gain {
652 rg_scratch.clear();
653 rg_scratch.extend_from_slice(sbuf.samples());
654 crate::audio::replaygain::apply_gain(&mut rg_scratch, gain_db, peak, pre_amp_db);
655 &rg_scratch[..]
656 } else {
657 sbuf.samples()
658 };
659
660 let mut offset = 0;
667 while offset < samples.len() {
668 if stop.load(Ordering::Relaxed) {
669 return Ok(());
670 }
671
672 let slots = producer.slots();
673 if slots == 0 {
674 thread::sleep(std::time::Duration::from_micros(500));
675 continue;
676 }
677
678 let chunk_size = slots.min(samples.len() - offset);
679 if let Ok(mut chunk) = producer.write_chunk_uninit(chunk_size) {
680 let to_write = &samples[offset..offset + chunk_size];
681 let (first, second) = chunk.as_mut_slices();
682 let first_len = first.len().min(to_write.len());
683 for (slot, &val) in first.iter_mut().zip(&to_write[..first_len]) {
684 slot.write(val);
685 }
686 if first_len < to_write.len() {
687 for (slot, &val) in second.iter_mut().zip(&to_write[first_len..]) {
688 slot.write(val);
689 }
690 }
691 unsafe { chunk.commit_all() };
695
696 if let Some(viz) = viz_buffer {
698 viz.push_samples(to_write, channels, sample_rate);
699 }
700
701 offset += chunk_size;
702 }
703 }
704
705 timeline.add_written(samples.len() as u64);
706 }
707}
708
709pub fn codec_name(codec: CodecType) -> String {
710 match codec {
711 CODEC_TYPE_FLAC => "FLAC",
712 CODEC_TYPE_MP3 => "MP3",
713 CODEC_TYPE_AAC => "AAC",
714 CODEC_TYPE_VORBIS => "Vorbis",
715 CODEC_TYPE_OPUS => "Opus",
716 CODEC_TYPE_ALAC => "ALAC",
717 CODEC_TYPE_WAVPACK => "WavPack",
718 CODEC_TYPE_PCM_S16LE => "PCM/16",
719 CODEC_TYPE_PCM_S24LE => "PCM/24",
720 CODEC_TYPE_PCM_S32LE => "PCM/32",
721 CODEC_TYPE_PCM_F32LE => "PCM/f32",
722 other => return format!("Unknown({:?})", other),
723 }
724 .to_string()
725}
726
727#[cfg(test)]
728mod tests {
729 use std::path::PathBuf;
730 use std::sync::atomic::Ordering;
731
732 use super::*;
733 use crate::player::state::QueueItemId;
734
735 fn make_info(sample_rate: u32, channels: u16) -> StreamInfo {
736 StreamInfo {
737 codec: "FLAC".to_string(),
738 sample_rate,
739 channels,
740 bit_depth: 16,
741 duration_ms: 10_000,
742 }
743 }
744
745 fn make_boundary(
746 id: QueueItemId,
747 sample_offset: u64,
748 seek_samples: u64,
749 channels: u16,
750 sample_rate: u32,
751 ) -> TrackBoundary {
752 TrackBoundary {
753 id,
754 path: PathBuf::from("/music/track.flac"),
755 info: make_info(sample_rate, channels),
756 sample_offset,
757 samples_written: 0,
758 seek_samples,
759 }
760 }
761
762 #[test]
765 fn test_timeline_single_track() {
766 let timeline = PlaybackTimeline::new();
770 let id = QueueItemId::new();
771 timeline.push_boundary(make_boundary(id, 0, 0, 2, 44100));
773 timeline.add_written(88200); timeline.samples_played.store(88200, Ordering::Relaxed);
777
778 let result = timeline.current_playback();
779 assert!(
780 result.is_some(),
781 "expected Some for single track with samples played"
782 );
783 let (result_id, _path, _info, position_ms) = result.unwrap();
784 assert_eq!(result_id, id);
785 assert_eq!(
786 position_ms, 1000,
787 "1 second of 44100 Hz stereo should be 1000 ms"
788 );
789 }
790
791 #[test]
792 fn test_timeline_gapless_transition() {
793 let timeline = PlaybackTimeline::new();
797 let id1 = QueueItemId::new();
798 let id2 = QueueItemId::new();
799
800 timeline.push_boundary(make_boundary(id1, 0, 0, 2, 44100));
802 timeline.add_written(88200);
803
804 timeline.push_boundary(make_boundary(id2, 88200, 0, 2, 44100));
806 timeline.add_written(44100); timeline.samples_played.store(90000, Ordering::Relaxed);
810
811 let result = timeline.current_playback();
812 assert!(result.is_some());
813 let (result_id, _path, _info, position_ms) = result.unwrap();
814 assert_eq!(
815 result_id, id2,
816 "playback head past boundary should report second track"
817 );
818 assert_eq!(position_ms, 20, "position within track 2 should be ~20 ms");
820 }
821
822 #[test]
823 fn test_timeline_zero_samples() {
824 let timeline = PlaybackTimeline::new();
827 let id = QueueItemId::new();
828 timeline.push_boundary(make_boundary(id, 0, 0, 2, 44100));
829 timeline.add_written(1000);
830 timeline.samples_played.store(0, Ordering::Relaxed);
831
832 let result = timeline.current_playback();
833 assert!(
834 result.is_some(),
835 "expected Some at 0 samples played with a boundary at offset 0"
836 );
837 let (result_id, _path, _info, position_ms) = result.unwrap();
838 assert_eq!(result_id, id);
839 assert_eq!(position_ms, 0);
840 }
841
842 #[test]
843 fn test_timeline_past_all_boundaries() {
844 let timeline = PlaybackTimeline::new();
847 let id1 = QueueItemId::new();
848 let id2 = QueueItemId::new();
849
850 timeline.push_boundary(make_boundary(id1, 0, 0, 2, 44100));
851 timeline.add_written(88200);
852 timeline.push_boundary(make_boundary(id2, 88200, 0, 2, 44100));
853 timeline.add_written(88200);
854
855 timeline
857 .samples_played
858 .store(999_999_999, Ordering::Relaxed);
859
860 let result = timeline.current_playback();
861 assert!(result.is_some());
862 let (result_id, _path, _info, _position_ms) = result.unwrap();
863 assert_eq!(
864 result_id, id2,
865 "samples past all boundaries should report the last track"
866 );
867 }
868
869 #[test]
870 fn test_timeline_seek_offset() {
871 let timeline = PlaybackTimeline::new();
875 let id = QueueItemId::new();
876 let seek_samples = 88200u64; timeline.push_boundary(make_boundary(id, 0, seek_samples, 2, 44100));
878 timeline.add_written(44100); timeline.samples_played.store(0, Ordering::Relaxed);
881
882 let result = timeline.current_playback();
883 assert!(result.is_some());
884 let (_result_id, _path, _info, position_ms) = result.unwrap();
885 assert_eq!(
887 position_ms, 1000,
888 "position should include seek offset of 1000 ms"
889 );
890 }
891
892 #[test]
893 fn test_timeline_reset() {
894 let timeline = PlaybackTimeline::new();
896 let id = QueueItemId::new();
897 timeline.push_boundary(make_boundary(id, 0, 0, 2, 44100));
898 timeline.add_written(88200);
899 timeline.samples_played.store(44100, Ordering::Relaxed);
900
901 assert!(timeline.current_playback().is_some());
903
904 timeline.reset();
905
906 assert!(
907 timeline.current_playback().is_none(),
908 "after reset, current_playback should return None"
909 );
910 assert_eq!(
911 timeline.samples_played.load(Ordering::Relaxed),
912 0,
913 "samples_played should be 0 after reset"
914 );
915 assert_eq!(
916 timeline.samples_written.load(Ordering::Relaxed),
917 0,
918 "samples_written should be 0 after reset"
919 );
920 }
921}