1pub mod commands;
2pub mod history;
3pub mod state;
4pub mod undo;
5
6use std::path::Path;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::thread;
10
11use thiserror::Error;
12
13use crate::audio::{
14 analyzer::VizAnalyzer,
15 backend::{self, AudioBackend, AudioEngineHandle, BackendError, SampleRateWatch},
16 buffer, streaming,
17 viz::{VizBuffer, VizSnapshot},
18};
19use buffer::PlaybackTimeline;
20use commands::{CommandChannel, PlayerCommand};
21use history::{InFlight, PlayEvent, PlayRecorder};
22use state::{LoadState, PlaybackSource, PlaybackState, QueueItemId, SharedPlayerState, TrackInfo};
23use undo::{UndoEntry, UndoStack};
24
25pub(crate) const RING_BUFFER_SIZE: usize = 192_000 * 2;
27
28#[derive(Debug, Error)]
29pub enum PlayerError {
30 #[error("backend error: {0}")]
31 Backend(#[from] BackendError),
32 #[error("decode error: {0}")]
33 Decode(#[from] buffer::DecodeError),
34}
35
36pub struct Player {
38 shared_state: Arc<SharedPlayerState>,
39 commands: CommandChannel,
40 active_playback: Option<ActivePlayback>,
41 timeline: Arc<PlaybackTimeline>,
42 viz_buffer: Arc<VizBuffer>,
43 viz_snapshot: Arc<VizSnapshot>,
44 _viz_analyzer: VizAnalyzer,
46 undo_stack: UndoStack,
47 batch_buffer: Option<Vec<UndoEntry>>,
50 output_device_name: Option<String>,
52 backend: Box<dyn AudioBackend>,
54 last_skip: std::time::Instant,
56 history: Option<PlayRecorder>,
59 in_flight: Option<InFlight>,
61 #[cfg(test)]
64 playback_starts: usize,
65}
66
67struct ActivePlayback {
69 engine: Box<dyn AudioEngineHandle>,
70 decode_handle: buffer::DecodeHandle,
71 _rate_watch: Option<Box<dyn SampleRateWatch>>,
74}
75
76impl Default for Player {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82impl Player {
83 pub fn new() -> Self {
84 let viz_buffer = VizBuffer::new();
85 let viz_snapshot = VizSnapshot::new();
86 let timeline = PlaybackTimeline::new();
87 let cfg = crate::config::Config::load_or_default();
88 let viz_analyzer = VizAnalyzer::spawn_with_snapshot(
89 Arc::clone(&viz_buffer),
90 &cfg.visualizer,
91 Arc::clone(&viz_snapshot),
92 timeline.samples_played_counter(),
93 );
94
95 Self {
96 shared_state: SharedPlayerState::new(),
97 commands: CommandChannel::new(),
98 active_playback: None,
99 timeline,
100 viz_buffer,
101 viz_snapshot,
102 _viz_analyzer: viz_analyzer,
103 undo_stack: UndoStack::new(),
104 batch_buffer: None,
105 output_device_name: cfg.playback.output_device.clone(),
106 backend: crate::audio::platform_backend(),
107 last_skip: std::time::Instant::now(),
108 history: None,
109 in_flight: None,
110 #[cfg(test)]
111 playback_starts: 0,
112 }
113 }
114
115 pub fn shared_state(&self) -> Arc<SharedPlayerState> {
117 self.shared_state.clone()
118 }
119
120 pub fn timeline(&self) -> Arc<PlaybackTimeline> {
122 self.timeline.clone()
123 }
124
125 pub fn viz_buffer(&self) -> Arc<VizBuffer> {
127 self.viz_buffer.clone()
128 }
129
130 pub fn viz_snapshot(&self) -> Arc<VizSnapshot> {
133 self.viz_snapshot.clone()
134 }
135
136 pub fn undo_stack(&self) -> &UndoStack {
138 &self.undo_stack
139 }
140
141 #[allow(clippy::type_complexity)]
149 fn create_engine_for(
150 &self,
151 info: &buffer::StreamInfo,
152 consumer: rtrb::Consumer<f32>,
153 ) -> Result<(Box<dyn AudioEngineHandle>, Option<Box<dyn SampleRateWatch>>), PlayerError> {
154 let device = self.resolve_device()?;
155 let device_rate = self.backend.get_device_sample_rate(&device)?;
156 let source_rate = info.sample_rate as f64;
157
158 self.shared_state.clear_output_sample_rate();
162
163 let settled = if (device_rate - source_rate).abs() > 0.1 {
164 log::info!(
165 "switching device sample rate: {}Hz → {}Hz",
166 device_rate,
167 source_rate
168 );
169 match self.backend.set_device_sample_rate(&device, source_rate) {
170 Ok(rate) => rate,
171 Err(e) => {
172 log::warn!("failed to set device sample rate: {}", e);
173 device_rate
174 }
175 }
176 } else {
177 device_rate
178 };
179
180 if (settled - source_rate).abs() > 0.1 {
181 log::warn!(
182 "device stayed at {}Hz (wanted {}Hz) — output is resampled, not bit-perfect",
183 settled,
184 source_rate
185 );
186 }
187
188 self.shared_state
191 .set_output_sample_rate(settled.round() as u32);
192
193 let watch_state = self.shared_state.clone();
197 let watch_name = device.name.clone();
198 let rate_watch = self.backend.watch_device_sample_rate(
199 &device,
200 Box::new(move |rate| {
201 log::info!("device sample rate changed externally: {rate}Hz on '{watch_name}'");
202 watch_state.set_output_sample_rate(rate.round() as u32);
203 }),
204 );
205
206 let engine = self.backend.create_engine(
207 &device,
208 source_rate,
209 info.channels as u32,
210 consumer,
211 self.timeline.samples_played_counter(),
212 )?;
213
214 Ok((engine, rate_watch))
215 }
216
217 fn resolve_device(&self) -> Result<backend::DeviceInfo, PlayerError> {
220 if let Some(ref name) = self.output_device_name {
221 match self.backend.list_devices() {
222 Ok(devices) => {
223 if let Some(dev) = devices.into_iter().find(|d| d.name == *name) {
224 return Ok(dev);
225 }
226 log::warn!(
227 "configured output device '{}' not found, falling back to default",
228 name,
229 );
230 }
231 Err(e) => {
232 log::warn!("failed to list devices while resolving '{}': {}", name, e);
233 }
234 }
235 }
236 Ok(self.backend.default_device()?)
237 }
238
239 pub fn set_output_device(&mut self, name: String) {
242 log::info!("switching output device to: {}", name);
243 self.output_device_name = Some(name.clone());
244
245 if let Err(e) = crate::config::Config::persist(|cfg| {
246 cfg.playback.output_device = Some(name);
247 }) {
248 log::error!("failed to save output device config: {}", e);
249 }
250
251 self.restart_on_current_track();
252 }
253
254 pub fn clear_output_device(&mut self) {
256 log::info!("reverting to system default output device");
257 self.output_device_name = None;
258
259 if let Err(e) = crate::config::Config::persist(|cfg| {
260 cfg.playback.output_device = None;
261 }) {
262 log::error!("failed to save output device config: {}", e);
263 }
264
265 self.restart_on_current_track();
266 }
267
268 fn restart_on_current_track(&mut self) {
271 if let Some(info) = self.shared_state.track_info() {
272 let was_paused = self.shared_state.playback_state() == PlaybackState::Paused;
273 let position_ms = self.shared_state.position_ms();
274 if let Err(e) = self.start_playback(info.id, &info.path, position_ms) {
275 log::error!("failed to restart playback on device switch: {}", e);
276 return;
277 }
278 if was_paused {
279 self.pause();
280 }
281 }
282 }
283
284 pub fn output_device_name(&self) -> Option<&str> {
286 self.output_device_name.as_deref()
287 }
288
289 pub fn command_sender(&self) -> crossbeam_channel::Sender<PlayerCommand> {
291 self.commands.tx.clone()
292 }
293
294 pub fn play(&mut self, id: QueueItemId) {
297 self.shared_state.set_cursor(Some(id));
298
299 match self.shared_state.item_playback_source(id) {
300 Some(PlaybackSource::Ready(path)) => {
301 if let Err(e) = self.start_playback(id, &path, 0) {
302 log::error!("play failed: {}", e);
303 }
304 }
305 Some(PlaybackSource::Streaming {
306 path,
307 bytes_written,
308 total,
309 }) => {
310 if let Err(e) = self.start_streaming_playback(id, &path, bytes_written, total) {
311 log::error!("streaming play failed, waiting for full download: {}", e);
314 }
315 }
316 None => {
317 self.stop_engine();
319 self.shared_state.set_playback_state(PlaybackState::Stopped);
320 log::info!("play: item {:?} not ready, waiting for TrackReady", id);
321 }
322 }
323 }
324
325 fn start_playback(
330 &mut self,
331 id: QueueItemId,
332 path: &Path,
333 seek_ms: u64,
334 ) -> Result<(), PlayerError> {
335 #[cfg(test)]
336 {
337 self.playback_starts += 1;
338 }
339 let result = self.open_playback(id, path, seek_ms);
340 if result.is_err() {
341 self.stop_playback_and_clear_state();
342 }
343 result
344 }
345
346 fn open_playback(
347 &mut self,
348 id: QueueItemId,
349 path: &Path,
350 seek_ms: u64,
351 ) -> Result<(), PlayerError> {
352 self.stop_engine();
353
354 let info = buffer::probe_file(path)?;
355
356 self.shared_state.set_track_info(Some(TrackInfo {
360 id,
361 path: path.to_path_buf(),
362 codec: info.codec.clone(),
363 sample_rate: info.sample_rate,
364 bit_depth: info.bit_depth,
365 bitrate_kbps: info.bitrate_kbps,
366 channels: info.channels,
367 duration_ms: info.duration_ms,
368 }));
369 self.shared_state.set_position_ms(seek_ms);
370 self.on_track_changed(id);
371 log::info!(
372 "playing: {} ({:?}) — {} {}Hz/{}ch, {}ms{}",
373 path.display(),
374 id,
375 info.codec,
376 info.sample_rate,
377 info.channels,
378 info.duration_ms,
379 if seek_ms > 0 {
380 format!(" @{}ms", seek_ms)
381 } else {
382 String::new()
383 }
384 );
385
386 let (producer, consumer) = rtrb::RingBuffer::new(RING_BUFFER_SIZE);
387
388 self.timeline.reset();
390
391 let advance_state = self.shared_state.clone();
395 let decode_cursor = parking_lot::Mutex::new(Some(id));
396 let next_track = move || {
397 let current = decode_cursor.lock().take()?;
398 let next = advance_state.peek_next_ready_after(current);
399 if let Some((next_id, _)) = &next {
400 let mut guard = decode_cursor.lock();
401 *guard = Some(*next_id);
402 }
403 next
404 };
405
406 let cfg = crate::config::Config::load_or_default();
408 let rg_mode = cfg.playback.replaygain;
409 let pre_amp_db = cfg.playback.pre_amp_db;
410
411 let finish_tx = self.commands.tx.clone();
412 let (_stream_info, decode_handle) = buffer::start_decode_file(
413 id,
414 path,
415 producer,
416 seek_ms,
417 next_track,
418 self.timeline.clone(),
419 Some(self.viz_buffer.clone()),
420 rg_mode,
421 pre_amp_db,
422 move || {
423 finish_tx.send(PlayerCommand::DecodeFinished).ok();
424 },
425 )?;
426
427 let (engine, rate_watch) = self.create_engine_for(&info, consumer)?;
428 engine.start()?;
429
430 self.shared_state.set_playback_state(PlaybackState::Playing);
431
432 self.active_playback = Some(ActivePlayback {
433 engine,
434 decode_handle,
435 _rate_watch: rate_watch,
436 });
437
438 Ok(())
439 }
440
441 fn start_streaming_playback(
448 &mut self,
449 id: QueueItemId,
450 path: &Path,
451 bytes_written: Arc<AtomicU64>,
452 total: u64,
453 ) -> Result<(), PlayerError> {
454 let result = self.open_streaming_playback(id, path, bytes_written, total);
455 if result.is_err() {
456 self.stop_playback_and_clear_state();
457 }
458 result
459 }
460
461 fn open_streaming_playback(
462 &mut self,
463 id: QueueItemId,
464 path: &Path,
465 bytes_written: Arc<AtomicU64>,
466 total: u64,
467 ) -> Result<(), PlayerError> {
468 self.stop_engine();
469
470 let stream_buf = streaming::StreamBuffer::new(if total > 0 { Some(total) } else { None });
472
473 let pump_path = path.to_path_buf();
481 let pump_buf = stream_buf.clone();
482 let pump_written = bytes_written.clone();
483 let pump_state = self.shared_state.clone();
484 thread::Builder::new()
485 .name("koan-stream-pump".into())
486 .spawn(move || {
487 use std::fs::File;
488 use std::io::Read;
489 use std::time::{Duration, Instant};
490
491 const STALL_LIMIT: Duration = Duration::from_secs(30);
495
496 let mut file = match File::open(&pump_path) {
497 Ok(f) => f,
498 Err(e) => {
499 log::error!("stream pump: failed to open {}: {}", pump_path.display(), e);
500 pump_buf.fail();
501 return;
502 }
503 };
504 let mut buf = vec![0u8; 65536];
505 let mut offset: u64 = 0;
506 let mut last_progress = Instant::now();
507 loop {
508 if pump_buf.is_abandoned() {
510 return;
511 }
512 match pump_state.item_load_state(id) {
513 Some(LoadState::Failed(e)) => {
514 log::warn!("stream pump: download of {:?} failed: {}", id, e);
515 pump_buf.fail();
516 return;
517 }
518 Some(LoadState::Ready) => match file.read(&mut buf) {
521 Ok(0) => break,
522 Ok(n) => {
523 pump_buf.push(&buf[..n]);
524 offset += n as u64;
525 continue;
526 }
527 Err(e) => {
528 log::warn!("stream pump read error: {}", e);
529 pump_buf.fail();
530 return;
531 }
532 },
533 _ => {}
534 }
535
536 let available = pump_written.load(Ordering::Acquire);
537 if offset >= available {
538 if total > 0 && available >= total {
539 break; }
541 if last_progress.elapsed() >= STALL_LIMIT {
542 log::warn!(
543 "stream pump: no data for {}s, abandoning {}",
544 STALL_LIMIT.as_secs(),
545 pump_path.display()
546 );
547 pump_buf.fail();
548 return;
549 }
550 thread::sleep(Duration::from_millis(10));
551 continue;
552 }
553 let to_read = ((available - offset) as usize).min(buf.len());
554 match file.read(&mut buf[..to_read]) {
555 Ok(0) => {
556 if total > 0 && offset >= total {
559 break;
560 }
561 let latest = pump_written.load(Ordering::Acquire);
562 if total > 0 && latest >= total && offset >= latest {
563 break;
564 }
565 thread::sleep(Duration::from_millis(1));
567 continue;
568 }
569 Ok(n) => {
570 pump_buf.push(&buf[..n]);
571 offset += n as u64;
572 last_progress = Instant::now();
573 }
574 Err(e) => {
575 log::warn!("stream pump read error: {}", e);
576 pump_buf.fail();
577 return;
578 }
579 }
580 }
581 pump_buf.finish();
582 })
583 .map_err(|e| PlayerError::Decode(buffer::DecodeError::Io(e)))?;
584
585 let probe_reader = stream_buf.reader();
587 let probe_hint = {
588 let mut h = symphonia::core::formats::probe::Hint::new();
589 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
590 h.with_extension(ext);
591 }
592 h
593 };
594 let probe_mss =
595 symphonia::core::io::MediaSourceStream::new(Box::new(probe_reader), Default::default());
596 let info = buffer::probe_source(probe_mss, &probe_hint)?;
597
598 self.shared_state.set_track_info(Some(TrackInfo {
599 id,
600 path: path.to_path_buf(),
601 codec: info.codec.clone(),
602 sample_rate: info.sample_rate,
603 bit_depth: info.bit_depth,
604 bitrate_kbps: info.bitrate_kbps,
605 channels: info.channels,
606 duration_ms: info.duration_ms,
607 }));
608 self.shared_state.set_position_ms(0);
609 self.on_track_changed(id);
610 log::info!(
611 "streaming: {} ({:?}) — {} {}Hz/{}ch, {}ms",
612 path.display(),
613 id,
614 info.codec,
615 info.sample_rate,
616 info.channels,
617 info.duration_ms,
618 );
619
620 let (producer, consumer) = rtrb::RingBuffer::new(RING_BUFFER_SIZE);
621
622 self.timeline.reset();
623
624 let advance_state = self.shared_state.clone();
626 let decode_cursor = parking_lot::Mutex::new(Some(id));
627 let next_track = move || {
628 let current = decode_cursor.lock().take()?;
629 let next = advance_state.peek_next_ready_after(current);
630 if let Some((next_id, _)) = &next {
631 let mut guard = decode_cursor.lock();
632 *guard = Some(*next_id);
633 }
634 next
635 };
636
637 let decode_reader = stream_buf.reader();
642 let path_buf = path.to_path_buf();
643 let ext = path
644 .extension()
645 .and_then(|e| e.to_str())
646 .unwrap_or("")
647 .to_string();
648 let mut decode_hint = symphonia::core::formats::probe::Hint::new();
649 if !ext.is_empty() {
650 decode_hint.with_extension(&ext);
651 }
652 let first = buffer::SourceEntry {
653 id,
654 path: path_buf,
655 hint: decode_hint,
656 make_mss: Box::new(move || {
657 Ok(symphonia::core::io::MediaSourceStream::new(
658 Box::new(decode_reader),
659 Default::default(),
660 ))
661 }),
662 };
663
664 let cfg = crate::config::Config::load_or_default();
666 let rg_mode = cfg.playback.replaygain;
667 let pre_amp_db = cfg.playback.pre_amp_db;
668
669 let finish_tx = self.commands.tx.clone();
670 let (_stream_info, decode_handle) = buffer::start_decode(
671 first,
672 producer,
673 0,
674 move || {
675 let (next_id, next_path) = next_track()?;
676 Some(buffer::SourceEntry::from_file(next_id, next_path))
677 },
678 self.timeline.clone(),
679 Some(self.viz_buffer.clone()),
680 rg_mode,
681 pre_amp_db,
682 move || {
683 finish_tx.send(PlayerCommand::DecodeFinished).ok();
684 },
685 )?;
686
687 let (engine, rate_watch) = self.create_engine_for(&info, consumer)?;
688 engine.start()?;
689
690 self.shared_state.set_playback_state(PlaybackState::Playing);
691
692 self.active_playback = Some(ActivePlayback {
693 engine,
694 decode_handle,
695 _rate_watch: rate_watch,
696 });
697
698 Ok(())
699 }
700
701 pub fn seek(&mut self, position_ms: u64) {
704 let info = match self.shared_state.track_info() {
705 Some(info) => info,
706 None => return,
707 };
708 let id = info.id;
709 let path = info.path.clone();
710 let duration = info.duration_ms;
711
712 let mut clamped = if duration > 0 {
714 position_ms.min(duration.saturating_sub(500))
715 } else {
716 position_ms
717 };
718
719 if let Some(dl_frac) = self.shared_state.current_download_fraction() {
722 let max_ms = (dl_frac * duration as f64) as u64;
723 if max_ms > 5_000 {
724 clamped = clamped.min(max_ms - 5_000);
725 }
726 }
727
728 let was_paused = self.shared_state.playback_state() == PlaybackState::Paused;
729
730 if let Err(e) = self.start_playback(id, &path, clamped) {
731 log::error!("seek failed: {}", e);
732 return;
733 }
734
735 if was_paused {
736 self.pause();
737 }
738 }
739
740 pub fn next_track(&mut self) {
742 match self.shared_state.advance_cursor_loadable() {
743 Some(id) => self.play(id),
744 None => {
745 log::info!("no more tracks in playlist");
746 self.stop_playback_and_clear_state();
747 }
748 }
749 }
750
751 pub fn prev_track(&mut self) {
753 match self.shared_state.retreat_cursor() {
754 Some((id, path)) => {
755 if matches!(path.try_exists(), Ok(true)) {
756 if let Err(e) = self.start_playback(id, &path, 0) {
757 log::error!("prev track failed: {}", e);
758 }
759 } else {
760 log::warn!("prev track path doesn't exist: {}", path.display());
761 }
762 }
763 None => {
764 if let Some(info) = self.shared_state.track_info()
766 && let Err(e) = self.start_playback(info.id, &info.path, 0)
767 {
768 log::error!("restart failed: {}", e);
769 }
770 }
771 }
772 }
773
774 pub fn pause(&mut self) {
776 if let Some(ref playback) = self.active_playback {
777 if let Err(e) = playback.engine.stop() {
778 log::error!("pause failed: {}", e);
779 return;
780 }
781 self.shared_state.set_playback_state(PlaybackState::Paused);
782 }
783 }
784
785 pub fn resume(&mut self) {
787 if let Some(ref playback) = self.active_playback {
788 if let Err(e) = playback.engine.start() {
789 log::error!("resume failed: {}", e);
790 return;
791 }
792 self.shared_state.set_playback_state(PlaybackState::Playing);
793 }
794 }
795
796 pub fn stop(&mut self) {
798 self.shared_state.clear_playlist();
799 self.stop_playback_and_clear_state();
800 }
801
802 fn stop_engine(&mut self) {
809 let Some(playback) = self.active_playback.take() else {
810 return;
811 };
812 let ActivePlayback {
813 engine,
814 mut decode_handle,
815 _rate_watch,
816 } = playback;
817
818 let _ = engine.stop();
829 decode_handle.stop();
830 drop(engine);
831 }
832
833 fn stop_playback_and_clear_state(&mut self) {
835 self.finish_play();
836 self.stop_engine();
837 self.timeline.reset();
838 self.shared_state.set_playback_state(PlaybackState::Stopped);
839 self.shared_state.set_position_ms(0);
840 self.shared_state.set_track_info(None);
841 }
842
843 pub fn remove_from_playlist(&mut self, id: QueueItemId) {
850 let was_cursor = self.shared_state.is_cursor(id);
851 let resume_after = was_cursor
852 .then(|| self.shared_state.item_before(id))
853 .flatten();
854 self.shared_state.remove_item(id);
855 if was_cursor {
856 self.shared_state.set_cursor(resume_after);
857 self.next_track();
858 }
859 }
860
861 pub fn track_ready(&mut self, id: QueueItemId) {
864 self.shared_state.update_load_state(id, LoadState::Ready);
866
867 if !self.shared_state.is_cursor(id) {
868 return;
869 }
870
871 let is_playing = self.shared_state.playback_state() == PlaybackState::Playing;
872 let current_track_id = self.shared_state.track_info().map(|t| t.id);
873
874 if is_playing && current_track_id == Some(id) {
875 log::info!(
878 "track_ready: download complete while streaming {:?}, refreshing metadata",
879 id
880 );
881 self.refresh_track_metadata(id);
882 return;
883 }
884
885 if !is_playing && let Some(path) = self.shared_state.item_path_if_ready(id) {
887 log::info!("track_ready: starting playback for {:?}", id);
888 if let Err(e) = self.start_playback(id, &path, 0) {
889 log::error!("track_ready playback failed: {}", e);
890 }
891 }
892 }
893
894 pub fn track_stream_ready(&mut self, id: QueueItemId) {
897 if !self.shared_state.is_cursor(id) {
898 return;
899 }
900
901 let is_playing = self.shared_state.playback_state() == PlaybackState::Playing;
902 if is_playing {
903 return; }
905
906 match self.shared_state.item_playback_source(id) {
907 Some(PlaybackSource::Streaming {
908 path,
909 bytes_written,
910 total,
911 }) => {
912 log::info!(
913 "track_stream_ready: starting streaming playback for {:?}",
914 id
915 );
916 if let Err(e) = self.start_streaming_playback(id, &path, bytes_written, total) {
917 log::error!("track_stream_ready streaming failed: {}", e);
918 }
919 }
920 Some(PlaybackSource::Ready(path)) => {
921 log::info!(
923 "track_stream_ready: track already ready, starting normal playback for {:?}",
924 id
925 );
926 if let Err(e) = self.start_playback(id, &path, 0) {
927 log::error!("track_stream_ready playback failed: {}", e);
928 }
929 }
930 None => {} }
932 }
933
934 fn refresh_track_metadata(&mut self, id: QueueItemId) {
938 use crate::index::metadata;
939
940 let path = match self.shared_state.item_path_if_ready(id) {
941 Some(p) => p,
942 None => return,
943 };
944
945 match metadata::read_metadata(&path) {
946 Ok(meta) => {
947 self.shared_state.update_item_metadata(
949 id,
950 meta.title,
951 meta.artist,
952 meta.album_artist.unwrap_or_default(),
953 meta.album,
954 meta.duration_ms.map(|d| d as u64),
955 );
956
957 if let Ok(stream_info) = buffer::probe_file(&path)
962 && let Some(current) = self.shared_state.track_info()
963 && current.id == id
964 && stream_info.duration_ms > current.duration_ms
965 {
966 log::info!(
967 "track_ready: duration corrected {}ms → {}ms",
968 current.duration_ms,
969 stream_info.duration_ms
970 );
971 self.shared_state.set_track_info(Some(TrackInfo {
972 duration_ms: stream_info.duration_ms,
973 ..current
974 }));
975 }
976
977 self.shared_state.signal_metadata_refresh();
979 log::info!("track_ready: metadata refreshed for {:?}", id);
980 }
981 Err(e) => {
982 log::warn!("track_ready: metadata refresh failed for {:?}: {}", id, e);
983 }
984 }
985 }
986
987 fn on_track_changed(&mut self, id: QueueItemId) {
997 if self.in_flight.as_ref().is_some_and(|f| f.item == id) {
998 return;
999 }
1000 self.finish_play();
1001 let track_id = self.shared_state.item_db_id(id);
1002 self.in_flight = Some(InFlight::new(id, track_id));
1003 if let (Some(track_id), Some(recorder)) = (track_id, self.history.as_ref()) {
1004 recorder.record(PlayEvent::Started { track_id });
1005 }
1006 }
1007
1008 fn finish_play(&mut self) -> Option<PlayEvent> {
1011 let flight = self.in_flight.take()?;
1012 let event = PlayEvent::Finished {
1013 track_id: flight.track_id()?,
1014 listened_ms: flight.listened_ms(),
1015 };
1016 if let Some(recorder) = self.history.as_ref() {
1017 recorder.record(event);
1018 }
1019 Some(event)
1020 }
1021
1022 pub fn update_playback_state(&mut self) {
1023 if self.active_playback.is_none() {
1024 return;
1025 }
1026
1027 if let Some((id, path, info, position_ms)) = self.timeline.current_playback() {
1028 self.shared_state.set_position_ms(position_ms);
1029
1030 self.on_track_changed(id);
1033 if let Some(f) = self.in_flight.as_mut() {
1034 f.advance(position_ms);
1035 }
1036
1037 let current_id = self.shared_state.track_info().map(|t| t.id);
1040 if current_id != Some(id) {
1041 log::info!("timeline: now playing {:?}", id);
1042 self.shared_state.set_track_info(Some(TrackInfo {
1043 id,
1044 path,
1045 codec: info.codec,
1046 sample_rate: info.sample_rate,
1047 bit_depth: info.bit_depth,
1048 bitrate_kbps: info.bitrate_kbps,
1049 channels: info.channels,
1050 duration_ms: info.duration_ms,
1051 }));
1052 self.shared_state.set_cursor(Some(id));
1053 }
1054 }
1055 }
1056
1057 pub fn track_failed(&mut self, id: QueueItemId) {
1064 if !self.shared_state.is_cursor(id) {
1065 return;
1066 }
1067 if self.shared_state.playback_state() != PlaybackState::Stopped {
1071 return;
1072 }
1073 log::info!("track {:?} cannot load, moving on", id);
1074 self.next_track();
1075 }
1076
1077 fn on_decode_finished(&mut self) {
1084 log::info!("decode finished, checking for next track");
1085 match self.shared_state.advance_cursor_loadable() {
1086 Some(id) => self.play(id),
1087 None => {
1088 log::info!("no more tracks — stopping");
1089 self.stop_playback_and_clear_state();
1090 }
1091 }
1092 }
1093
1094 fn snapshot_for_undo(
1098 &self,
1099 ids: &[QueueItemId],
1100 ) -> Vec<(Box<state::PlaylistItem>, Option<QueueItemId>)> {
1101 self.shared_state
1102 .items_before(ids)
1103 .into_iter()
1104 .filter_map(|(id, after)| Some((Box::new(self.shared_state.get_item(id)?), after)))
1105 .collect()
1106 }
1107
1108 fn push_undo(&mut self, entry: UndoEntry) {
1110 if let Some(ref mut batch) = self.batch_buffer {
1111 batch.push(entry);
1112 } else {
1113 self.undo_stack.push(entry);
1114 }
1115 }
1116
1117 pub fn process_command(&mut self, cmd: PlayerCommand) {
1119 match cmd {
1120 PlayerCommand::Play(id) => self.play(id),
1121 PlayerCommand::Pause => self.pause(),
1122 PlayerCommand::Resume => self.resume(),
1123 PlayerCommand::Stop => self.stop(),
1124 PlayerCommand::Seek(pos) => self.seek(pos),
1125 PlayerCommand::NextTrack => {
1126 let now = std::time::Instant::now();
1128 if now.duration_since(self.last_skip).as_millis() >= 150 {
1129 self.last_skip = now;
1130 self.next_track();
1131 }
1132 }
1133 PlayerCommand::PrevTrack => {
1134 let now = std::time::Instant::now();
1135 if now.duration_since(self.last_skip).as_millis() >= 150 {
1136 self.last_skip = now;
1137 self.prev_track();
1138 }
1139 }
1140 PlayerCommand::AddToPlaylist(items) => {
1141 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
1142 self.shared_state.add_items(items);
1143 self.push_undo(UndoEntry::Added { ids });
1144 }
1145 PlayerCommand::UpdatePaths(updates) => {
1146 self.shared_state.update_paths(&updates);
1147 if let Some(info) = self.shared_state.track_info()
1148 && let Some((_, new_path)) = updates.iter().find(|(id, _)| *id == info.id)
1149 {
1150 self.shared_state.set_track_info(Some(TrackInfo {
1151 path: new_path.clone(),
1152 ..info
1153 }));
1154 }
1155 }
1156 PlayerCommand::InsertInPlaylist { items, after } => {
1157 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
1158 self.shared_state.insert_items_after(items, after);
1159 self.push_undo(UndoEntry::Inserted { ids });
1160 }
1161 PlayerCommand::ClearPlaylist => {
1162 self.stop_playback_and_clear_state();
1166 let (items, cursor) = self.shared_state.snapshot_playlist();
1167 self.shared_state.clear_playlist();
1168 self.push_undo(UndoEntry::Replaced { items, cursor });
1169 }
1170 PlayerCommand::ReplacePlaylist { items, start } => {
1171 self.stop_playback_and_clear_state();
1175 let (old_items, cursor) = self.shared_state.snapshot_playlist();
1176 self.shared_state.clear_playlist();
1177 self.push_undo(UndoEntry::Replaced {
1178 items: old_items,
1179 cursor,
1180 });
1181
1182 if items.is_empty() {
1183 return;
1184 }
1185 let start_id = items.get(start).unwrap_or(&items[0]).id;
1186 self.shared_state.add_items(items);
1187 self.play(start_id);
1188 }
1189 PlayerCommand::RemoveFromPlaylist(id) => {
1190 let item = self.shared_state.get_item(id);
1191 let after = self.shared_state.item_before(id);
1192 self.remove_from_playlist(id);
1193 if let Some(item) = item {
1194 self.push_undo(UndoEntry::Removed {
1195 items: vec![(Box::new(item), after)],
1196 });
1197 }
1198 }
1199 PlayerCommand::RemoveFromPlaylistBatch(ids) => {
1200 let items_with_pos = self.snapshot_for_undo(&ids);
1204 let resume_after = match self.shared_state.cursor() {
1205 Some(cursor) if ids.contains(&cursor) => {
1206 Some(self.shared_state.surviving_item_before(cursor, &ids))
1207 }
1208 _ => None,
1209 };
1210
1211 self.shared_state.remove_items(&ids);
1212
1213 if let Some(resume_after) = resume_after {
1214 self.shared_state.set_cursor(resume_after);
1215 self.next_track();
1216 }
1217
1218 if !items_with_pos.is_empty() {
1219 self.push_undo(UndoEntry::Removed {
1220 items: items_with_pos,
1221 });
1222 }
1223 }
1224 PlayerCommand::MoveInPlaylist { id, target, after } => {
1225 let was_after = self.shared_state.item_before(id);
1226 self.shared_state.move_item(id, target, after);
1227 self.push_undo(UndoEntry::Moved { id, was_after });
1228 }
1229 PlayerCommand::MoveItemsInPlaylist { ids, target, after } => {
1230 let entries = self.shared_state.items_before(&ids);
1231 self.shared_state.move_items(&ids, target, after);
1232 self.push_undo(UndoEntry::MovedBatch { entries });
1233 }
1234 PlayerCommand::ReorderPlaylist(order) => {
1235 let entries = self.shared_state.items_before(&order);
1239 self.shared_state.reorder_to(&order);
1240 self.push_undo(UndoEntry::MovedBatch { entries });
1241 }
1242 PlayerCommand::TrackReady(id) => self.track_ready(id),
1243 PlayerCommand::DecodeFinished => self.on_decode_finished(),
1244 PlayerCommand::TrackStreamReady(id) => self.track_stream_ready(id),
1245 PlayerCommand::TrackFailed(id) => self.track_failed(id),
1246 PlayerCommand::Undo => self.execute_undo(),
1247 PlayerCommand::Redo => self.execute_redo(),
1248 PlayerCommand::BeginUndoBatch => {
1249 self.batch_buffer = Some(Vec::new());
1250 }
1251 PlayerCommand::EndUndoBatch => {
1252 if let Some(entries) = self.batch_buffer.take() {
1253 if entries.len() == 1 {
1254 self.undo_stack.push(entries.into_iter().next().unwrap());
1256 } else if !entries.is_empty() {
1257 self.undo_stack.push(UndoEntry::Batch(entries));
1258 }
1259 }
1260 }
1261 PlayerCommand::SetOutputDevice(name) => self.set_output_device(name),
1262 PlayerCommand::ClearOutputDevice => self.clear_output_device(),
1263 }
1264 }
1265
1266 fn apply_entry(&mut self, entry: UndoEntry) -> Option<UndoEntry> {
1268 match entry {
1269 UndoEntry::Added { ids } => {
1270 let items_with_pos = self.snapshot_for_undo(&ids);
1272 self.shared_state.remove_items(&ids);
1273 Some(UndoEntry::Removed {
1274 items: items_with_pos,
1275 })
1276 }
1277 UndoEntry::Removed { items } => {
1278 let mut ids = Vec::with_capacity(items.len());
1280 for (item, after) in items {
1281 ids.push(item.id);
1282 self.shared_state.insert_item_at(*item, after);
1283 }
1284 Some(UndoEntry::Added { ids })
1285 }
1286 UndoEntry::Inserted { ids } => {
1287 let items_with_pos = self.snapshot_for_undo(&ids);
1289 self.shared_state.remove_items(&ids);
1290 Some(UndoEntry::Removed {
1291 items: items_with_pos,
1292 })
1293 }
1294 UndoEntry::Moved { id, was_after } => {
1295 let current_after = self.shared_state.item_before(id);
1296 self.shared_state.move_item_to(id, was_after);
1297 Some(UndoEntry::Moved {
1298 id,
1299 was_after: current_after,
1300 })
1301 }
1302 UndoEntry::MovedBatch { entries } => {
1303 let ids: Vec<QueueItemId> = entries.iter().map(|(id, _)| *id).collect();
1304 let current_positions = self.shared_state.items_before(&ids);
1305 self.shared_state.move_items_to(&entries);
1306 Some(UndoEntry::MovedBatch {
1307 entries: current_positions,
1308 })
1309 }
1310 UndoEntry::Replaced { items, cursor } => {
1311 let (current_items, current_cursor) = self.shared_state.snapshot_playlist();
1312 self.shared_state.restore_playlist(items, cursor);
1313 Some(UndoEntry::Replaced {
1314 items: current_items,
1315 cursor: current_cursor,
1316 })
1317 }
1318 UndoEntry::Batch(entries) => {
1319 let mut inverses = Vec::with_capacity(entries.len());
1321 for entry in entries.into_iter().rev() {
1322 if let Some(inverse) = self.apply_entry(entry) {
1323 inverses.push(inverse);
1324 }
1325 }
1326 inverses.reverse();
1327 Some(UndoEntry::Batch(inverses))
1328 }
1329 }
1330 }
1331
1332 fn reconcile_playback(&mut self) {
1346 let Some(playing) = self.shared_state.track_info().map(|t| t.id) else {
1347 return;
1348 };
1349 if self.shared_state.get_item(playing).is_some() {
1350 return;
1351 }
1352 let resume = (self.shared_state.playback_state() == PlaybackState::Playing)
1357 .then(|| self.shared_state.cursor())
1358 .flatten();
1359 self.stop_playback_and_clear_state();
1360 if let Some(id) = resume {
1361 self.play(id);
1362 }
1363 }
1364
1365 fn execute_undo(&mut self) {
1367 let Some(entry) = self.undo_stack.pop_undo() else {
1368 return;
1369 };
1370 if let Some(inverse) = self.apply_entry(entry) {
1371 self.undo_stack.push_redo(inverse);
1372 }
1373 self.reconcile_playback();
1374 }
1375
1376 fn execute_redo(&mut self) {
1378 let Some(entry) = self.undo_stack.pop_redo() else {
1379 return;
1380 };
1381 if let Some(inverse) = self.apply_entry(entry) {
1382 self.undo_stack.push_undo_keep_redo(inverse);
1383 }
1384 self.reconcile_playback();
1385 }
1386
1387 pub fn run(&mut self) {
1389 use std::time::Duration;
1390
1391 let rx = self.commands.rx.clone();
1392 loop {
1393 match rx.recv_timeout(Duration::from_millis(50)) {
1395 Ok(cmd) => self.process_command(cmd),
1396 Err(crossbeam_channel::RecvTimeoutError::Timeout) => {}
1397 Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
1398 }
1399 self.update_playback_state();
1400 }
1401 self.stop();
1402 }
1403
1404 pub fn spawn() -> (
1407 Arc<SharedPlayerState>,
1408 Arc<PlaybackTimeline>,
1409 Arc<VizSnapshot>,
1410 crossbeam_channel::Sender<PlayerCommand>,
1411 ) {
1412 let mut player = Self::new();
1413 player.history = PlayRecorder::spawn();
1414 let state = player.shared_state();
1415 let timeline = player.timeline();
1416 let viz_snapshot = player.viz_snapshot();
1417 let tx = player.command_sender();
1418
1419 thread::Builder::new()
1420 .name("koan-player".into())
1421 .spawn(move || player.run())
1422 .expect("failed to spawn player thread");
1423
1424 (state, timeline, viz_snapshot, tx)
1425 }
1426}
1427
1428#[cfg(test)]
1429mod tests {
1430 use super::*;
1431 use state::PlaylistItem;
1432 use std::path::PathBuf;
1433
1434 fn make_item(title: &str) -> PlaylistItem {
1435 PlaylistItem {
1436 playlist_entry_id: None,
1437 id: QueueItemId::new(),
1438 db_id: None,
1439 path: PathBuf::from(format!("/music/{title}.flac")),
1440 title: title.to_string(),
1441 artist: String::new(),
1442 album_artist: String::new(),
1443 album: String::new(),
1444 year: None,
1445 codec: None,
1446 track_number: None,
1447 disc: None,
1448 duration_ms: None,
1449 load_state: LoadState::Ready,
1450 }
1451 }
1452
1453 fn playlist_ids(player: &Player) -> Vec<QueueItemId> {
1454 let (items, _) = player.shared_state.snapshot_playlist();
1455 items.iter().map(|i| i.id).collect()
1456 }
1457
1458 fn playlist_titles(player: &Player) -> Vec<String> {
1459 let (items, _) = player.shared_state.snapshot_playlist();
1460 items.iter().map(|i| i.title.clone()).collect()
1461 }
1462
1463 fn pending_item(title: &str) -> PlaylistItem {
1464 PlaylistItem {
1465 playlist_entry_id: None,
1466 load_state: LoadState::Pending,
1467 ..make_item(title)
1468 }
1469 }
1470
1471 fn pretend_playing(player: &mut Player, id: QueueItemId) {
1475 let item = player
1476 .shared_state
1477 .get_item(id)
1478 .expect("item is in the queue");
1479 player.shared_state.set_track_info(Some(TrackInfo {
1480 id,
1481 path: item.path,
1482 codec: String::new(),
1483 sample_rate: 44_100,
1484 bit_depth: None,
1485 bitrate_kbps: None,
1486 channels: 2,
1487 duration_ms: 1_000,
1488 }));
1489 player
1490 .shared_state
1491 .set_playback_state(PlaybackState::Playing);
1492 }
1493
1494 fn playing_id(player: &Player) -> Option<QueueItemId> {
1495 player.shared_state.track_info().map(|t| t.id)
1496 }
1497
1498 fn seed(player: &mut Player, n: usize) -> Vec<QueueItemId> {
1500 let items: Vec<_> = (0..n).map(|i| make_item(&format!("t{i}"))).collect();
1501 let ids = items.iter().map(|i| i.id).collect();
1502 player.process_command(PlayerCommand::AddToPlaylist(items));
1503 ids
1504 }
1505
1506 fn listen(player: &mut Player, from_ms: u64, to_ms: u64) {
1510 let mut at = from_ms;
1511 if let Some(f) = player.in_flight.as_mut() {
1512 f.advance(at); }
1514 while at < to_ms {
1515 at = (at + 50).min(to_ms);
1516 if let Some(f) = player.in_flight.as_mut() {
1517 f.advance(at);
1518 }
1519 }
1520 }
1521
1522 fn start(player: &mut Player, track_id: i64) -> QueueItemId {
1523 let id = QueueItemId::new();
1524 player.on_track_changed(id);
1525 player
1527 .in_flight
1528 .as_mut()
1529 .unwrap()
1530 .track_id_for_test(track_id);
1531 id
1532 }
1533
1534 #[test]
1535 fn a_gapless_transition_closes_the_outgoing_track_and_opens_the_next() {
1536 let mut player = Player::new();
1537 start(&mut player, 11);
1538 listen(&mut player, 0, 200_000);
1539
1540 let b = QueueItemId::new();
1541 player.on_track_changed(b);
1542 let f = player
1543 .in_flight
1544 .as_ref()
1545 .expect("the next track is counting");
1546 assert_eq!(f.item, b);
1547 assert_eq!(f.listened_ms(), 0, "and starts from nothing");
1548 }
1549
1550 #[test]
1551 fn a_track_skipped_seconds_in_is_still_history() {
1552 let mut player = Player::new();
1553 start(&mut player, 7);
1554 listen(&mut player, 0, 2_000);
1555
1556 let event = player
1557 .finish_play()
1558 .expect("putting something on is a thing you did, however briefly");
1559 assert!(matches!(
1560 event,
1561 history::PlayEvent::Finished {
1562 track_id: 7,
1563 listened_ms: 2_000
1564 }
1565 ));
1566 }
1567
1568 #[test]
1569 fn a_track_is_closed_out_once() {
1570 let mut player = Player::new();
1571 start(&mut player, 7);
1572 listen(&mut player, 0, 200_000);
1573
1574 assert!(player.finish_play().is_some());
1575 assert!(player.finish_play().is_none());
1576 }
1577
1578 #[test]
1579 fn seeking_around_a_track_does_not_enter_it_twice() {
1580 let mut player = Player::new();
1581 let id = start(&mut player, 7);
1582 listen(&mut player, 0, 120_000);
1583
1584 player.on_track_changed(id);
1586 assert_eq!(
1587 player.in_flight.as_ref().unwrap().listened_ms(),
1588 120_000,
1589 "the seek kept the count rather than restarting it"
1590 );
1591 listen(&mut player, 30_000, 40_000);
1592
1593 let Some(history::PlayEvent::Finished { listened_ms, .. }) = player.finish_play() else {
1594 panic!("still one play");
1595 };
1596 assert_eq!(listened_ms, 130_000);
1597 assert!(player.finish_play().is_none());
1598 }
1599
1600 #[test]
1601 fn a_track_that_is_not_in_the_library_is_not_recorded() {
1602 let mut player = Player::new();
1603 let id = QueueItemId::new();
1604 player.on_track_changed(id);
1605 listen(&mut player, 0, 200_000);
1606 assert!(player.finish_play().is_none());
1607 }
1608
1609 #[test]
1610 fn stopping_closes_out_what_was_heard() {
1611 let mut player = Player::new();
1612 start(&mut player, 7);
1613 listen(&mut player, 0, 150_000);
1614
1615 player.stop_playback_and_clear_state();
1616 assert!(player.in_flight.is_none(), "the stop consumed it");
1617 }
1618
1619 #[test]
1620 fn removing_the_playing_track_resumes_at_its_successor() {
1621 let mut player = Player::new();
1622 let ids = seed(&mut player, 5);
1623 player.shared_state.set_cursor(Some(ids[2]));
1624
1625 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[2]));
1626
1627 assert_eq!(
1628 player.shared_state.cursor(),
1629 Some(ids[3]),
1630 "playback must continue at the next track, not restart the queue"
1631 );
1632 assert_eq!(player.playback_starts, 1);
1633 }
1634
1635 #[test]
1636 fn removing_the_first_playing_track_resumes_at_the_new_first() {
1637 let mut player = Player::new();
1638 let ids = seed(&mut player, 3);
1639 player.shared_state.set_cursor(Some(ids[0]));
1640
1641 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[0]));
1642
1643 assert_eq!(player.shared_state.cursor(), Some(ids[1]));
1644 }
1645
1646 #[test]
1647 fn next_track_parks_on_a_track_that_has_not_downloaded_yet() {
1648 let mut player = Player::new();
1649 let playing = make_item("playing");
1650 let waiting = pending_item("waiting");
1651 let later = make_item("later");
1652 let (playing_id, waiting_id) = (playing.id, waiting.id);
1653 player.process_command(PlayerCommand::AddToPlaylist(vec![playing, waiting, later]));
1654 player.shared_state.set_cursor(Some(playing_id));
1655
1656 player.process_command(PlayerCommand::DecodeFinished);
1657
1658 assert_eq!(
1659 player.shared_state.cursor(),
1660 Some(waiting_id),
1661 "the cursor parks on the track being fetched"
1662 );
1663 assert_eq!(
1664 player.playback_starts, 0,
1665 "nothing to play until its bytes land"
1666 );
1667
1668 player
1671 .shared_state
1672 .update_load_state(waiting_id, LoadState::Ready);
1673 player.process_command(PlayerCommand::TrackReady(waiting_id));
1674
1675 assert_eq!(player.playback_starts, 1);
1676 assert_eq!(player.shared_state.cursor(), Some(waiting_id));
1677 }
1678
1679 #[test]
1680 fn a_download_that_cannot_land_moves_the_cursor_on() {
1681 let mut player = Player::new();
1682 let waiting = pending_item("waiting");
1683 let later = make_item("later");
1684 let (waiting_id, later_id) = (waiting.id, later.id);
1685 player.process_command(PlayerCommand::AddToPlaylist(vec![waiting, later]));
1686
1687 player.process_command(PlayerCommand::Play(waiting_id));
1688 assert_eq!(player.playback_starts, 0, "nothing to play yet");
1689
1690 player
1692 .shared_state
1693 .update_load_state(waiting_id, LoadState::Failed("remote unavailable".into()));
1694 player.process_command(PlayerCommand::TrackFailed(waiting_id));
1695
1696 assert_eq!(
1697 player.shared_state.cursor(),
1698 Some(later_id),
1699 "the queue moves past a track that can never load"
1700 );
1701 assert_eq!(player.playback_starts, 1);
1702 }
1703
1704 #[test]
1705 fn a_queue_that_can_never_load_stops_rather_than_waiting() {
1706 let mut player = Player::new();
1707 let first = pending_item("first");
1708 let second = pending_item("second");
1709 let (first_id, second_id) = (first.id, second.id);
1710 player.process_command(PlayerCommand::AddToPlaylist(vec![first, second]));
1711
1712 player.process_command(PlayerCommand::Play(first_id));
1713 for id in [first_id, second_id] {
1714 player
1715 .shared_state
1716 .update_load_state(id, LoadState::Failed("remote unavailable".into()));
1717 player.process_command(PlayerCommand::TrackFailed(id));
1718 }
1719
1720 assert_eq!(player.playback_starts, 0);
1721 assert_eq!(
1722 player.shared_state.playback_state(),
1723 PlaybackState::Stopped,
1724 "a stop the UI can see, not an indefinite wait for TrackReady"
1725 );
1726 }
1727
1728 #[test]
1729 fn a_failure_elsewhere_in_the_queue_leaves_the_cursor_alone() {
1730 let mut player = Player::new();
1731 let waiting = pending_item("waiting");
1732 let other = pending_item("other");
1733 let (waiting_id, other_id) = (waiting.id, other.id);
1734 player.process_command(PlayerCommand::AddToPlaylist(vec![waiting, other]));
1735 player.process_command(PlayerCommand::Play(waiting_id));
1736
1737 player
1738 .shared_state
1739 .update_load_state(other_id, LoadState::Failed("remote unavailable".into()));
1740 player.process_command(PlayerCommand::TrackFailed(other_id));
1741
1742 assert_eq!(
1743 player.shared_state.cursor(),
1744 Some(waiting_id),
1745 "a track still downloading keeps the cursor"
1746 );
1747 }
1748
1749 #[test]
1750 fn batch_delete_containing_the_cursor_restarts_the_engine_once() {
1751 let mut player = Player::new();
1752 let ids = seed(&mut player, 5);
1753 player.shared_state.set_cursor(Some(ids[2]));
1754
1755 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![
1756 ids[1], ids[2], ids[3],
1757 ]));
1758
1759 assert_eq!(playlist_titles(&player), vec!["t0", "t4"]);
1760 assert_eq!(player.shared_state.cursor(), Some(ids[4]));
1761 assert_eq!(
1762 player.playback_starts, 1,
1763 "one resume for the whole selection, not one per deleted track"
1764 );
1765 }
1766
1767 #[test]
1768 fn batch_delete_below_the_cursor_leaves_playback_alone() {
1769 let mut player = Player::new();
1770 let ids = seed(&mut player, 4);
1771 player.shared_state.set_cursor(Some(ids[0]));
1772
1773 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![ids[2], ids[3]]));
1774
1775 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
1776 assert_eq!(player.playback_starts, 0);
1777 }
1778
1779 #[test]
1780 fn undo_of_a_batch_delete_restores_the_original_order() {
1781 let mut player = Player::new();
1785 let items = vec![
1786 make_item("A"),
1787 make_item("B"),
1788 make_item("C"),
1789 make_item("D"),
1790 ];
1791 let (b_id, c_id) = (items[1].id, items[2].id);
1792 player.process_command(PlayerCommand::AddToPlaylist(items));
1793
1794 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![c_id, b_id]));
1795 assert_eq!(playlist_titles(&player), vec!["A", "D"]);
1796
1797 player.process_command(PlayerCommand::Undo);
1798 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
1799 }
1800
1801 #[test]
1804 fn undo_add_removes_items() {
1805 let mut player = Player::new();
1806 let items = vec![make_item("A"), make_item("B")];
1807 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
1808
1809 player.process_command(PlayerCommand::AddToPlaylist(items));
1810 assert_eq!(playlist_ids(&player), ids);
1811 assert!(player.undo_stack().can_undo());
1812
1813 player.process_command(PlayerCommand::Undo);
1814 assert!(playlist_ids(&player).is_empty());
1815 assert!(player.undo_stack().can_redo());
1816 }
1817
1818 #[test]
1819 fn redo_add_restores_items() {
1820 let mut player = Player::new();
1821 let items = vec![make_item("A"), make_item("B")];
1822
1823 player.process_command(PlayerCommand::AddToPlaylist(items));
1824 player.process_command(PlayerCommand::Undo);
1825 assert!(playlist_ids(&player).is_empty());
1826
1827 player.process_command(PlayerCommand::Redo);
1828 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1829 }
1830
1831 #[test]
1834 fn undo_remove_restores_item_at_position() {
1835 let mut player = Player::new();
1836 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1837 let b_id = items[1].id;
1838
1839 player.process_command(PlayerCommand::AddToPlaylist(items));
1840 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1841 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1842
1843 player.process_command(PlayerCommand::Undo);
1844 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1845 }
1846
1847 #[test]
1848 fn undo_remove_first_item() {
1849 let mut player = Player::new();
1850 let items = vec![make_item("A"), make_item("B")];
1851 let a_id = items[0].id;
1852
1853 player.process_command(PlayerCommand::AddToPlaylist(items));
1854 player.process_command(PlayerCommand::RemoveFromPlaylist(a_id));
1855 assert_eq!(playlist_titles(&player), vec!["B"]);
1856
1857 player.process_command(PlayerCommand::Undo);
1858 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1859 }
1860
1861 #[test]
1862 fn undo_batch_remove_restores_all() {
1863 let mut player = Player::new();
1864 let items = vec![
1865 make_item("A"),
1866 make_item("B"),
1867 make_item("C"),
1868 make_item("D"),
1869 ];
1870 let b_id = items[1].id;
1871 let c_id = items[2].id;
1872
1873 player.process_command(PlayerCommand::AddToPlaylist(items));
1874 let version_before = player.shared_state.playlist_version();
1875 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![b_id, c_id]));
1876 assert_eq!(playlist_titles(&player), vec!["A", "D"]);
1877 assert_eq!(
1880 player.shared_state.playlist_version(),
1881 version_before + 1,
1882 "batch removal must bump the playlist version exactly once"
1883 );
1884
1885 player.process_command(PlayerCommand::Undo);
1887 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
1888 }
1889
1890 #[test]
1891 fn redo_batch_remove() {
1892 let mut player = Player::new();
1893 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1894 let a_id = items[0].id;
1895 let b_id = items[1].id;
1896
1897 player.process_command(PlayerCommand::AddToPlaylist(items));
1898 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![a_id, b_id]));
1899 player.process_command(PlayerCommand::Undo);
1900 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1901
1902 player.process_command(PlayerCommand::Redo);
1903 assert_eq!(playlist_titles(&player), vec!["C"]);
1904 }
1905
1906 #[test]
1907 fn redo_remove() {
1908 let mut player = Player::new();
1909 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1910 let b_id = items[1].id;
1911
1912 player.process_command(PlayerCommand::AddToPlaylist(items));
1913 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1914 player.process_command(PlayerCommand::Undo);
1915 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1916
1917 player.process_command(PlayerCommand::Redo);
1918 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1919 }
1920
1921 #[test]
1924 fn undo_insert_removes_inserted_items() {
1925 let mut player = Player::new();
1926 let items = vec![make_item("A"), make_item("C")];
1927 let a_id = items[0].id;
1928
1929 player.process_command(PlayerCommand::AddToPlaylist(items));
1930
1931 let inserted = vec![make_item("B")];
1932 player.process_command(PlayerCommand::InsertInPlaylist {
1933 items: inserted,
1934 after: a_id,
1935 });
1936 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1937
1938 player.process_command(PlayerCommand::Undo);
1939 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1940 }
1941
1942 #[test]
1945 fn undo_move_restores_position() {
1946 let mut player = Player::new();
1947 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1948 let a_id = items[0].id;
1949 let c_id = items[2].id;
1950
1951 player.process_command(PlayerCommand::AddToPlaylist(items));
1952
1953 player.process_command(PlayerCommand::MoveInPlaylist {
1955 id: a_id,
1956 target: c_id,
1957 after: true,
1958 });
1959 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1960
1961 player.process_command(PlayerCommand::Undo);
1962 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1963 }
1964
1965 #[test]
1966 fn redo_move() {
1967 let mut player = Player::new();
1968 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1969 let a_id = items[0].id;
1970 let c_id = items[2].id;
1971
1972 player.process_command(PlayerCommand::AddToPlaylist(items));
1973 player.process_command(PlayerCommand::MoveInPlaylist {
1974 id: a_id,
1975 target: c_id,
1976 after: true,
1977 });
1978 player.process_command(PlayerCommand::Undo);
1979 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1980
1981 player.process_command(PlayerCommand::Redo);
1982 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1983 }
1984
1985 #[test]
1988 fn undo_batch_move() {
1989 let mut player = Player::new();
1990 let items = vec![
1991 make_item("A"),
1992 make_item("B"),
1993 make_item("C"),
1994 make_item("D"),
1995 ];
1996 let a_id = items[0].id;
1997 let b_id = items[1].id;
1998 let d_id = items[3].id;
1999
2000 player.process_command(PlayerCommand::AddToPlaylist(items));
2001
2002 player.process_command(PlayerCommand::MoveItemsInPlaylist {
2004 ids: vec![a_id, b_id],
2005 target: d_id,
2006 after: true,
2007 });
2008 assert_eq!(playlist_titles(&player), vec!["C", "D", "A", "B"]);
2009
2010 player.process_command(PlayerCommand::Undo);
2011 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
2012 }
2013
2014 #[test]
2017 fn undo_clear_restores_playlist() {
2018 let mut player = Player::new();
2019 let items = vec![make_item("A"), make_item("B"), make_item("C")];
2020
2021 player.process_command(PlayerCommand::AddToPlaylist(items));
2022 player.process_command(PlayerCommand::ClearPlaylist);
2023 assert!(playlist_ids(&player).is_empty());
2024
2025 player.process_command(PlayerCommand::Undo);
2026 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
2027 }
2028
2029 #[test]
2034 fn undoing_a_replace_does_not_leave_the_engine_on_an_orphaned_track() {
2035 let mut player = Player::new();
2036 let original = seed(&mut player, 3);
2037 player.shared_state.set_cursor(Some(original[0]));
2038 pretend_playing(&mut player, original[0]);
2039
2040 let replacement = vec![make_item("something else")];
2041 let orphan = replacement[0].id;
2042 player.process_command(PlayerCommand::ReplacePlaylist {
2043 items: replacement,
2044 start: 0,
2045 });
2046 pretend_playing(&mut player, orphan);
2048
2049 player.process_command(PlayerCommand::Undo);
2050
2051 assert_eq!(playlist_ids(&player), original, "the queue comes back");
2052 assert!(
2053 player.shared_state.get_item(orphan).is_none(),
2054 "and the replacement is gone from it"
2055 );
2056 assert!(
2057 playing_id(&player).is_none_or(|id| player.shared_state.get_item(id).is_some()),
2058 "so nothing may still be playing out of it"
2059 );
2060 }
2061
2062 #[test]
2064 fn undoing_an_add_does_not_leave_the_engine_on_a_removed_track() {
2065 let mut player = Player::new();
2066 seed(&mut player, 2);
2067 let added = seed(&mut player, 1);
2068 pretend_playing(&mut player, added[0]);
2069
2070 player.process_command(PlayerCommand::Undo);
2071
2072 assert!(player.shared_state.get_item(added[0]).is_none());
2073 assert!(
2074 playing_id(&player).is_none_or(|id| player.shared_state.get_item(id).is_some()),
2075 "the engine cannot be left on the item the undo removed"
2076 );
2077 }
2078
2079 #[test]
2081 fn undoing_a_move_leaves_playback_alone() {
2082 let mut player = Player::new();
2083 let ids = seed(&mut player, 3);
2084 player.shared_state.set_cursor(Some(ids[0]));
2085 pretend_playing(&mut player, ids[0]);
2086 let starts = player.playback_starts;
2087
2088 player.process_command(PlayerCommand::MoveInPlaylist {
2089 id: ids[2],
2090 target: ids[0],
2091 after: false,
2092 });
2093 player.process_command(PlayerCommand::Undo);
2094
2095 assert_eq!(playlist_ids(&player), ids);
2096 assert_eq!(playing_id(&player), Some(ids[0]), "still on the same track");
2097 assert_eq!(player.playback_starts, starts, "and not restarted");
2098 }
2099
2100 #[test]
2101 fn redo_clear() {
2102 let mut player = Player::new();
2103 let items = vec![make_item("A"), make_item("B")];
2104
2105 player.process_command(PlayerCommand::AddToPlaylist(items));
2106 player.process_command(PlayerCommand::ClearPlaylist);
2107 player.process_command(PlayerCommand::Undo);
2108 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
2109
2110 player.process_command(PlayerCommand::Redo);
2111 assert!(playlist_ids(&player).is_empty());
2112 }
2113
2114 #[test]
2117 fn multiple_undos_in_sequence() {
2118 let mut player = Player::new();
2119
2120 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("A")]));
2121 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
2122 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("C")]));
2123 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
2124
2125 player.process_command(PlayerCommand::Undo);
2126 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
2127
2128 player.process_command(PlayerCommand::Undo);
2129 assert_eq!(playlist_titles(&player), vec!["A"]);
2130
2131 player.process_command(PlayerCommand::Undo);
2132 assert!(playlist_ids(&player).is_empty());
2133 }
2134
2135 #[test]
2136 fn undo_redo_undo_cycle() {
2137 let mut player = Player::new();
2138 let items = vec![make_item("A"), make_item("B")];
2139
2140 player.process_command(PlayerCommand::AddToPlaylist(items));
2141 player.process_command(PlayerCommand::Undo);
2142 assert!(playlist_ids(&player).is_empty());
2143
2144 player.process_command(PlayerCommand::Redo);
2145 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
2146
2147 player.process_command(PlayerCommand::Undo);
2148 assert!(playlist_ids(&player).is_empty());
2149 }
2150
2151 #[test]
2152 fn new_action_clears_redo_stack() {
2153 let mut player = Player::new();
2154 let items = vec![make_item("A")];
2155
2156 player.process_command(PlayerCommand::AddToPlaylist(items));
2157 player.process_command(PlayerCommand::Undo);
2158 assert!(player.undo_stack().can_redo());
2159
2160 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
2162 assert!(!player.undo_stack().can_redo());
2163 }
2164
2165 #[test]
2166 fn undo_on_empty_stack_is_noop() {
2167 let mut player = Player::new();
2168 player.process_command(PlayerCommand::Undo);
2169 assert!(playlist_ids(&player).is_empty());
2170 }
2171
2172 #[test]
2173 fn redo_on_empty_stack_is_noop() {
2174 let mut player = Player::new();
2175 player.process_command(PlayerCommand::Redo);
2176 assert!(playlist_ids(&player).is_empty());
2177 }
2178
2179 #[test]
2182 fn playback_commands_not_undoable() {
2183 let mut player = Player::new();
2184 player.process_command(PlayerCommand::Pause);
2185 player.process_command(PlayerCommand::Resume);
2186 player.process_command(PlayerCommand::NextTrack);
2187 player.process_command(PlayerCommand::PrevTrack);
2188 assert!(!player.undo_stack().can_undo());
2189 }
2190
2191 #[test]
2192 fn update_paths_not_undoable() {
2193 let mut player = Player::new();
2194 let items = vec![make_item("A")];
2195 let id = items[0].id;
2196 player.process_command(PlayerCommand::AddToPlaylist(items));
2197
2198 let undo_count = player.undo_stack().undo_len();
2199 player.process_command(PlayerCommand::UpdatePaths(vec![(
2200 id,
2201 PathBuf::from("/new/path.flac"),
2202 )]));
2203 assert_eq!(player.undo_stack().undo_len(), undo_count);
2204 }
2205
2206 #[test]
2209 fn add_remove_undo_undo_produces_original() {
2210 let mut player = Player::new();
2211 let items = vec![make_item("A"), make_item("B"), make_item("C")];
2212 let b_id = items[1].id;
2213 let original_titles = vec!["A", "B", "C"];
2214
2215 player.process_command(PlayerCommand::AddToPlaylist(items));
2216 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
2217 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
2218
2219 player.process_command(PlayerCommand::Undo);
2221 assert_eq!(playlist_titles(&player), original_titles);
2222
2223 player.process_command(PlayerCommand::Undo);
2225 assert!(playlist_ids(&player).is_empty());
2226 }
2227
2228 #[test]
2229 fn interleaved_adds_and_moves_undo() {
2230 let mut player = Player::new();
2231 let items = vec![make_item("A"), make_item("B"), make_item("C")];
2232 let a_id = items[0].id;
2233 let c_id = items[2].id;
2234
2235 player.process_command(PlayerCommand::AddToPlaylist(items));
2236
2237 player.process_command(PlayerCommand::MoveInPlaylist {
2239 id: a_id,
2240 target: c_id,
2241 after: true,
2242 });
2243 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
2244
2245 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("D")]));
2247 assert_eq!(playlist_titles(&player), vec!["B", "C", "A", "D"]);
2248
2249 player.process_command(PlayerCommand::Undo);
2251 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
2252
2253 player.process_command(PlayerCommand::Undo);
2255 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
2256 }
2257
2258 #[test]
2263 fn stop_engine_drops_engine_synchronously() {
2264 use std::sync::atomic::AtomicBool;
2265
2266 struct MockEngine {
2267 dropped: Arc<AtomicBool>,
2268 }
2269 impl AudioEngineHandle for MockEngine {
2270 fn start(&self) -> Result<(), BackendError> {
2271 Ok(())
2272 }
2273 fn stop(&self) -> Result<(), BackendError> {
2274 Ok(())
2275 }
2276 fn is_running(&self) -> bool {
2277 false
2278 }
2279 }
2280 impl Drop for MockEngine {
2281 fn drop(&mut self) {
2282 self.dropped.store(true, Ordering::SeqCst);
2283 }
2284 }
2285
2286 let dropped = Arc::new(AtomicBool::new(false));
2287
2288 let stop_flag = Arc::new(AtomicBool::new(false));
2290 let decode_handle = buffer::DecodeHandle::new_for_test(stop_flag);
2291
2292 let mut player = Player::new();
2293 player.active_playback = Some(ActivePlayback {
2294 engine: Box::new(MockEngine {
2295 dropped: dropped.clone(),
2296 }),
2297 decode_handle,
2298 _rate_watch: None,
2299 });
2300
2301 player.stop_engine();
2302
2303 assert!(
2307 dropped.load(Ordering::SeqCst),
2308 "AudioEngine must be dropped synchronously in stop_engine (GitHub #89)"
2309 );
2310 }
2311
2312 struct StuckBackend {
2317 rate: f64,
2318 asked: Arc<std::sync::Mutex<Option<(f64, u32)>>>,
2319 }
2320
2321 struct NullEngine;
2322 impl AudioEngineHandle for NullEngine {
2323 fn start(&self) -> Result<(), BackendError> {
2324 Ok(())
2325 }
2326 fn stop(&self) -> Result<(), BackendError> {
2327 Ok(())
2328 }
2329 fn is_running(&self) -> bool {
2330 false
2331 }
2332 }
2333
2334 impl AudioBackend for StuckBackend {
2335 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
2336 Ok(vec![self.default_device()?])
2337 }
2338 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
2339 Ok(backend::DeviceInfo {
2340 name: "Stuck DAC".into(),
2341 sample_rates: vec![self.rate],
2342 platform_id: 0,
2343 })
2344 }
2345 fn supported_sample_rates(
2346 &self,
2347 _device: &backend::DeviceInfo,
2348 ) -> Result<Vec<f64>, BackendError> {
2349 Ok(vec![self.rate])
2350 }
2351 fn get_device_sample_rate(
2352 &self,
2353 _device: &backend::DeviceInfo,
2354 ) -> Result<f64, BackendError> {
2355 Ok(self.rate)
2356 }
2357 fn set_device_sample_rate(
2358 &self,
2359 _device: &backend::DeviceInfo,
2360 rate: f64,
2361 ) -> Result<f64, BackendError> {
2362 Err(BackendError::UnsupportedSampleRate(rate))
2363 }
2364 fn create_engine(
2365 &self,
2366 _device: &backend::DeviceInfo,
2367 sample_rate: f64,
2368 channels: u32,
2369 _consumer: rtrb::Consumer<f32>,
2370 _samples_played: Arc<AtomicU64>,
2371 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
2372 *self.asked.lock().unwrap() = Some((sample_rate, channels));
2373 Ok(Box::new(NullEngine))
2374 }
2375 }
2376
2377 fn engine_format_for(source_rate: u32, channels: u16, device_rate: f64) -> (f64, u32) {
2378 let asked = Arc::new(std::sync::Mutex::new(None));
2379 let mut player = Player::new();
2380 player.backend = Box::new(StuckBackend {
2381 rate: device_rate,
2382 asked: asked.clone(),
2383 });
2384
2385 let info = buffer::StreamInfo {
2386 codec: "MP3".into(),
2387 sample_rate: source_rate,
2388 channels,
2389 bit_depth: Some(16),
2390 bitrate_kbps: None,
2391 duration_ms: 1000,
2392 };
2393 let (_producer, consumer) = rtrb::RingBuffer::new(16);
2394 player
2395 .create_engine_for(&info, consumer)
2396 .expect("engine creation should succeed");
2397 let asked = *asked.lock().unwrap();
2398 asked.expect("engine was never created")
2399 }
2400
2401 #[test]
2402 fn engine_uses_source_rate_when_device_refuses_switch() {
2403 assert_eq!(engine_format_for(22050, 2, 48000.0), (22050.0, 2));
2406 assert_eq!(engine_format_for(32000, 2, 44100.0), (32000.0, 2));
2407 }
2408
2409 #[test]
2410 fn engine_uses_source_channel_count() {
2411 assert_eq!(engine_format_for(44100, 1, 44100.0), (44100.0, 1));
2412 }
2413
2414 fn settled_rate_for(source_rate: u32, device_rate: f64) -> Option<u32> {
2416 let mut player = Player::new();
2417 player.backend = Box::new(StuckBackend {
2418 rate: device_rate,
2419 asked: Arc::new(std::sync::Mutex::new(None)),
2420 });
2421 let state = player.shared_state.clone();
2422
2423 let info = buffer::StreamInfo {
2424 codec: "MP3".into(),
2425 sample_rate: source_rate,
2426 channels: 2,
2427 bit_depth: Some(16),
2428 bitrate_kbps: None,
2429 duration_ms: 1000,
2430 };
2431 let (_producer, consumer) = rtrb::RingBuffer::new(16);
2432 player
2433 .create_engine_for(&info, consumer)
2434 .expect("engine creation should succeed");
2435 state.output_sample_rate()
2436 }
2437
2438 #[test]
2439 fn settled_device_rate_reaches_the_shared_state() {
2440 assert_eq!(settled_rate_for(22050, 48000.0), Some(48000));
2444 assert_eq!(settled_rate_for(44100, 44100.0), Some(44100));
2446 }
2447
2448 struct SlowBackend {
2450 observed: Arc<std::sync::Mutex<Vec<Option<u32>>>>,
2451 state: Arc<SharedPlayerState>,
2452 }
2453
2454 impl AudioBackend for SlowBackend {
2455 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
2456 Ok(vec![self.default_device()?])
2457 }
2458 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
2459 Ok(backend::DeviceInfo {
2460 name: "Slow DAC".into(),
2461 sample_rates: vec![44100.0, 48000.0],
2462 platform_id: 0,
2463 })
2464 }
2465 fn supported_sample_rates(
2466 &self,
2467 _device: &backend::DeviceInfo,
2468 ) -> Result<Vec<f64>, BackendError> {
2469 Ok(vec![44100.0, 48000.0])
2470 }
2471 fn get_device_sample_rate(
2472 &self,
2473 _device: &backend::DeviceInfo,
2474 ) -> Result<f64, BackendError> {
2475 Ok(48000.0)
2476 }
2477 fn set_device_sample_rate(
2478 &self,
2479 _device: &backend::DeviceInfo,
2480 rate: f64,
2481 ) -> Result<f64, BackendError> {
2482 self.observed
2484 .lock()
2485 .unwrap()
2486 .push(self.state.output_sample_rate());
2487 Ok(rate)
2488 }
2489 fn create_engine(
2490 &self,
2491 _device: &backend::DeviceInfo,
2492 _sample_rate: f64,
2493 _channels: u32,
2494 _consumer: rtrb::Consumer<f32>,
2495 _samples_played: Arc<AtomicU64>,
2496 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
2497 Ok(Box::new(NullEngine))
2498 }
2499 }
2500
2501 #[test]
2502 fn the_previous_rate_is_not_published_while_the_device_reclocks() {
2503 let mut player = Player::new();
2509 let state = player.shared_state.clone();
2510 state.set_output_sample_rate(48000);
2511
2512 let observed = Arc::new(std::sync::Mutex::new(Vec::new()));
2513 player.backend = Box::new(SlowBackend {
2514 observed: observed.clone(),
2515 state: state.clone(),
2516 });
2517
2518 let info = buffer::StreamInfo {
2519 codec: "FLAC".into(),
2520 sample_rate: 44100,
2521 channels: 2,
2522 bit_depth: Some(16),
2523 bitrate_kbps: None,
2524 duration_ms: 1000,
2525 };
2526 let (_producer, consumer) = rtrb::RingBuffer::new(16);
2527 player
2528 .create_engine_for(&info, consumer)
2529 .expect("engine creation should succeed");
2530
2531 assert_eq!(
2532 *observed.lock().unwrap(),
2533 vec![None],
2534 "mid-switch the output rate must read as unknown, not as the last track's"
2535 );
2536 assert_eq!(state.output_sample_rate(), Some(44100));
2537 }
2538
2539 struct WatchedBackend {
2541 inner: StuckBackend,
2542 #[allow(clippy::type_complexity)]
2543 captured: Arc<std::sync::Mutex<Option<Box<dyn Fn(f64) + Send + Sync>>>>,
2544 }
2545
2546 struct NullWatch;
2547 impl backend::SampleRateWatch for NullWatch {}
2548
2549 impl AudioBackend for WatchedBackend {
2550 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
2551 self.inner.list_devices()
2552 }
2553 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
2554 self.inner.default_device()
2555 }
2556 fn supported_sample_rates(
2557 &self,
2558 device: &backend::DeviceInfo,
2559 ) -> Result<Vec<f64>, BackendError> {
2560 self.inner.supported_sample_rates(device)
2561 }
2562 fn get_device_sample_rate(
2563 &self,
2564 device: &backend::DeviceInfo,
2565 ) -> Result<f64, BackendError> {
2566 self.inner.get_device_sample_rate(device)
2567 }
2568 fn set_device_sample_rate(
2569 &self,
2570 device: &backend::DeviceInfo,
2571 rate: f64,
2572 ) -> Result<f64, BackendError> {
2573 self.inner.set_device_sample_rate(device, rate)
2574 }
2575 fn watch_device_sample_rate(
2576 &self,
2577 _device: &backend::DeviceInfo,
2578 on_change: Box<dyn Fn(f64) + Send + Sync>,
2579 ) -> Option<Box<dyn backend::SampleRateWatch>> {
2580 *self.captured.lock().unwrap() = Some(on_change);
2581 Some(Box::new(NullWatch))
2582 }
2583 fn create_engine(
2584 &self,
2585 device: &backend::DeviceInfo,
2586 sample_rate: f64,
2587 channels: u32,
2588 consumer: rtrb::Consumer<f32>,
2589 samples_played: Arc<AtomicU64>,
2590 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
2591 self.inner
2592 .create_engine(device, sample_rate, channels, consumer, samples_played)
2593 }
2594 }
2595
2596 #[test]
2597 fn external_rate_change_reaches_the_shared_state() {
2598 let captured = Arc::new(std::sync::Mutex::new(None));
2602 let mut player = Player::new();
2603 player.backend = Box::new(WatchedBackend {
2604 inner: StuckBackend {
2605 rate: 44100.0,
2606 asked: Arc::new(std::sync::Mutex::new(None)),
2607 },
2608 captured: captured.clone(),
2609 });
2610 let state = player.shared_state.clone();
2611
2612 let info = buffer::StreamInfo {
2613 codec: "FLAC".into(),
2614 sample_rate: 44100,
2615 channels: 2,
2616 bit_depth: Some(16),
2617 bitrate_kbps: None,
2618 duration_ms: 1000,
2619 };
2620 let (_producer, consumer) = rtrb::RingBuffer::new(16);
2621 player
2622 .create_engine_for(&info, consumer)
2623 .expect("engine creation should succeed");
2624 assert_eq!(state.output_sample_rate(), Some(44100));
2625
2626 let on_change = captured.lock().unwrap().take().expect("watch registered");
2627 on_change(48000.0);
2628 assert_eq!(state.output_sample_rate(), Some(48000));
2629 }
2630}