1pub mod commands;
2pub mod history;
3pub mod state;
4pub mod undo;
5
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use std::sync::atomic::AtomicU64;
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::{
23 ItemState, LoadState, PlaybackSource, PlaybackState, QueueItemId, SharedPlayerState, TrackInfo,
24};
25use undo::{UndoEntry, UndoStack};
26
27pub(crate) const RING_BUFFER_SIZE: usize = 192_000 * 2;
29
30const SEEK_END_GUARD_MS: u64 = 500;
33
34#[derive(Debug, Error)]
35pub enum PlayerError {
36 #[error("backend error: {0}")]
37 Backend(#[from] BackendError),
38 #[error("decode error: {0}")]
39 Decode(#[from] buffer::DecodeError),
40}
41
42#[derive(Clone)]
45struct StreamSource {
46 path: PathBuf,
47 bytes_written: Arc<AtomicU64>,
48 total: u64,
49 mode: streaming::ProbeMode,
50}
51
52fn hint_for(path: &Path) -> symphonia::core::formats::probe::Hint {
54 let mut hint = symphonia::core::formats::probe::Hint::new();
55 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
56 hint.with_extension(ext);
57 }
58 hint
59}
60
61pub struct Player {
63 shared_state: Arc<SharedPlayerState>,
64 commands: CommandChannel,
65 active_playback: Option<ActivePlayback>,
66 timeline: Arc<PlaybackTimeline>,
67 viz_buffer: Arc<VizBuffer>,
68 viz_snapshot: Arc<VizSnapshot>,
69 _viz_analyzer: VizAnalyzer,
71 undo_stack: UndoStack,
72 batch_buffer: Option<Vec<UndoEntry>>,
75 output_device_name: Option<String>,
77 backend: Box<dyn AudioBackend>,
79 last_skip: std::time::Instant,
81 stream_mode: streaming::ProbeMode,
84 history: Option<PlayRecorder>,
87 in_flight: Option<InFlight>,
89 #[cfg(test)]
92 playback_starts: usize,
93}
94
95struct ActivePlayback {
97 engine: Box<dyn AudioEngineHandle>,
98 decode_handle: buffer::DecodeHandle,
99 _rate_watch: Option<Box<dyn SampleRateWatch>>,
102}
103
104impl Default for Player {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110impl Player {
111 pub fn new() -> Self {
112 let viz_buffer = VizBuffer::new();
113 let viz_snapshot = VizSnapshot::new();
114 let timeline = PlaybackTimeline::new();
115 let cfg = crate::config::Config::load_or_default();
116 let viz_analyzer = VizAnalyzer::spawn_with_snapshot(
117 Arc::clone(&viz_buffer),
118 &cfg.visualizer,
119 Arc::clone(&viz_snapshot),
120 timeline.samples_played_counter(),
121 );
122
123 Self {
124 shared_state: SharedPlayerState::new(),
125 commands: CommandChannel::new(),
126 active_playback: None,
127 timeline,
128 viz_buffer,
129 viz_snapshot,
130 _viz_analyzer: viz_analyzer,
131 undo_stack: UndoStack::new(),
132 batch_buffer: None,
133 output_device_name: cfg.playback.output_device.clone(),
134 backend: crate::audio::platform_backend(),
135 last_skip: std::time::Instant::now(),
136 stream_mode: streaming::ProbeMode::Full,
137 history: None,
138 in_flight: None,
139 #[cfg(test)]
140 playback_starts: 0,
141 }
142 }
143
144 pub fn shared_state(&self) -> Arc<SharedPlayerState> {
146 self.shared_state.clone()
147 }
148
149 pub fn timeline(&self) -> Arc<PlaybackTimeline> {
151 self.timeline.clone()
152 }
153
154 pub fn viz_buffer(&self) -> Arc<VizBuffer> {
156 self.viz_buffer.clone()
157 }
158
159 pub fn viz_snapshot(&self) -> Arc<VizSnapshot> {
162 self.viz_snapshot.clone()
163 }
164
165 pub fn undo_stack(&self) -> &UndoStack {
167 &self.undo_stack
168 }
169
170 #[allow(clippy::type_complexity)]
178 fn create_engine_for(
179 &self,
180 info: &buffer::StreamInfo,
181 consumer: rtrb::Consumer<f32>,
182 ) -> Result<(Box<dyn AudioEngineHandle>, Option<Box<dyn SampleRateWatch>>), PlayerError> {
183 let device = self.resolve_device()?;
184 let device_rate = self.backend.get_device_sample_rate(&device)?;
185 let source_rate = info.sample_rate as f64;
186
187 self.shared_state.clear_output_sample_rate();
191
192 let settled = if (device_rate - source_rate).abs() > 0.1 {
193 log::info!(
194 "switching device sample rate: {}Hz → {}Hz",
195 device_rate,
196 source_rate
197 );
198 match self.backend.set_device_sample_rate(&device, source_rate) {
199 Ok(rate) => rate,
200 Err(e) => {
201 log::warn!("failed to set device sample rate: {}", e);
202 device_rate
203 }
204 }
205 } else {
206 device_rate
207 };
208
209 if (settled - source_rate).abs() > 0.1 {
210 log::warn!(
211 "device stayed at {}Hz (wanted {}Hz) — output is resampled, not bit-perfect",
212 settled,
213 source_rate
214 );
215 }
216
217 self.shared_state
220 .set_output_sample_rate(settled.round() as u32);
221
222 let watch_state = self.shared_state.clone();
226 let watch_name = device.name.clone();
227 let rate_watch = self.backend.watch_device_sample_rate(
228 &device,
229 Box::new(move |rate| {
230 log::info!("device sample rate changed externally: {rate}Hz on '{watch_name}'");
231 watch_state.set_output_sample_rate(rate.round() as u32);
232 }),
233 );
234
235 let engine = self.backend.create_engine(
236 &device,
237 source_rate,
238 info.channels as u32,
239 consumer,
240 self.timeline.samples_played_counter(),
241 )?;
242
243 Ok((engine, rate_watch))
244 }
245
246 fn resolve_device(&self) -> Result<backend::DeviceInfo, PlayerError> {
249 if let Some(ref name) = self.output_device_name {
250 match self.backend.list_devices() {
251 Ok(devices) => {
252 if let Some(dev) = devices.into_iter().find(|d| d.name == *name) {
253 return Ok(dev);
254 }
255 log::warn!(
256 "configured output device '{}' not found, falling back to default",
257 name,
258 );
259 }
260 Err(e) => {
261 log::warn!("failed to list devices while resolving '{}': {}", name, e);
262 }
263 }
264 }
265 Ok(self.backend.default_device()?)
266 }
267
268 pub fn set_output_device(&mut self, name: String) {
271 log::info!("switching output device to: {}", name);
272 self.output_device_name = Some(name.clone());
273
274 if let Err(e) = crate::config::Config::persist(|cfg| {
275 cfg.playback.output_device = Some(name);
276 }) {
277 log::error!("failed to save output device config: {}", e);
278 }
279
280 self.restart_on_current_track();
281 }
282
283 pub fn clear_output_device(&mut self) {
285 log::info!("reverting to system default output device");
286 self.output_device_name = None;
287
288 if let Err(e) = crate::config::Config::persist(|cfg| {
289 cfg.playback.output_device = None;
290 }) {
291 log::error!("failed to save output device config: {}", e);
292 }
293
294 self.restart_on_current_track();
295 }
296
297 fn restart_on_current_track(&mut self) {
300 if let Some(info) = self.shared_state.track_info() {
301 let position_ms = self.shared_state.position_ms();
302 if let Err(e) = self.restart_current(&info, position_ms) {
303 log::error!("failed to restart playback on device switch: {}", e);
304 }
305 }
306 }
307
308 pub fn output_device_name(&self) -> Option<&str> {
310 self.output_device_name.as_deref()
311 }
312
313 pub fn command_sender(&self) -> crossbeam_channel::Sender<PlayerCommand> {
315 self.commands.tx.clone()
316 }
317
318 pub fn play(&mut self, id: QueueItemId) {
321 self.shared_state.set_cursor(Some(id));
322
323 match self.shared_state.item_playback_source(id) {
324 Some(PlaybackSource::Ready(path)) => {
325 if let Err(e) = self.start_playback(id, &path, 0) {
326 log::error!("play failed: {}", e);
327 }
328 }
329 Some(PlaybackSource::Streaming {
330 path,
331 bytes_written,
332 total,
333 }) => {
334 self.stop_engine();
338 self.shared_state.set_playback_state(PlaybackState::Stopped);
339 self.probe_stream_for_playback(id, &path, bytes_written, total);
340 }
341 None => {
342 self.stop_engine();
344 self.shared_state.set_playback_state(PlaybackState::Stopped);
345 log::info!("play: item {:?} not ready, waiting for TrackReady", id);
346 }
347 }
348 }
349
350 fn start_playback(
355 &mut self,
356 id: QueueItemId,
357 path: &Path,
358 seek_ms: u64,
359 ) -> Result<(), PlayerError> {
360 #[cfg(test)]
361 {
362 self.playback_starts += 1;
363 }
364 let result = self.open_playback(id, path, seek_ms);
365 if result.is_err() {
366 self.stop_playback_and_clear_state();
367 }
368 result
369 }
370
371 fn open_playback(
372 &mut self,
373 id: QueueItemId,
374 path: &Path,
375 seek_ms: u64,
376 ) -> Result<(), PlayerError> {
377 self.stop_engine();
378
379 let info = buffer::probe_file(path)?;
380
381 self.shared_state.set_track_info(Some(TrackInfo {
385 id,
386 path: path.to_path_buf(),
387 codec: info.codec.clone(),
388 sample_rate: info.sample_rate,
389 bit_depth: info.bit_depth,
390 bitrate_kbps: info.bitrate_kbps,
391 channels: info.channels,
392 duration_ms: info.duration_ms,
393 }));
394 self.shared_state.set_position_ms(seek_ms);
395 self.on_track_changed(id);
396 log::info!(
397 "playing: {} ({:?}) — {} {}Hz/{}ch, {}ms{}",
398 path.display(),
399 id,
400 info.codec,
401 info.sample_rate,
402 info.channels,
403 info.duration_ms,
404 if seek_ms > 0 {
405 format!(" @{}ms", seek_ms)
406 } else {
407 String::new()
408 }
409 );
410
411 let (producer, consumer) = rtrb::RingBuffer::new(RING_BUFFER_SIZE);
412
413 self.timeline.reset();
415
416 let advance_state = self.shared_state.clone();
420 let decode_cursor = parking_lot::Mutex::new(Some(id));
421 let next_track = move || {
422 let current = decode_cursor.lock().take()?;
423 let next = advance_state.peek_next_ready_after(current);
424 if let Some((next_id, _)) = &next {
425 let mut guard = decode_cursor.lock();
426 *guard = Some(*next_id);
427 }
428 next
429 };
430
431 let cfg = crate::config::Config::load_or_default();
433 let rg_mode = cfg.playback.replaygain;
434 let pre_amp_db = cfg.playback.pre_amp_db;
435
436 let finish_tx = self.commands.tx.clone();
437 let (_stream_info, decode_handle) = buffer::start_decode_file(
438 id,
439 path,
440 producer,
441 seek_ms,
442 next_track,
443 self.timeline.clone(),
444 Some(self.viz_buffer.clone()),
445 rg_mode,
446 pre_amp_db,
447 move || {
448 finish_tx.send(PlayerCommand::DecodeFinished).ok();
449 },
450 )?;
451
452 let (engine, rate_watch) = self.create_engine_for(&info, consumer)?;
453 engine.start()?;
454
455 self.shared_state.set_playback_state(PlaybackState::Playing);
456
457 self.active_playback = Some(ActivePlayback {
458 engine,
459 decode_handle,
460 _rate_watch: rate_watch,
461 });
462
463 Ok(())
464 }
465
466 fn probe_stream_for_playback(
481 &self,
482 id: QueueItemId,
483 path: &Path,
484 bytes_written: Arc<AtomicU64>,
485 total: u64,
486 ) {
487 let path = path.to_path_buf();
488 let tx = self.commands.tx.clone();
489 let hint = hint_for(&path);
490
491 let status = {
496 let downloading = self.stream_status_fn(id);
497 let state = self.shared_state.clone();
498 Arc::new(move || {
499 if state.is_cursor(id) {
500 downloading()
501 } else {
502 streaming::StreamStatus::Failed
503 }
504 }) as Arc<dyn Fn() -> streaming::StreamStatus + Send + Sync>
505 };
506
507 let spawned = thread::Builder::new()
508 .name("koan-stream-probe".into())
509 .spawn(move || {
510 let attempt = |mode, wait: bool| {
517 let open = if wait {
518 streaming::PartialFileSource::open(
519 &path,
520 bytes_written.clone(),
521 total,
522 status.clone(),
523 mode,
524 )
525 } else {
526 streaming::PartialFileSource::open_for_probe(
527 &path,
528 bytes_written.clone(),
529 total,
530 status.clone(),
531 mode,
532 )
533 };
534 open.map_err(buffer::DecodeError::Io).and_then(|source| {
535 let mss = symphonia::core::io::MediaSourceStream::new(
536 Box::new(source),
537 Default::default(),
538 );
539 buffer::probe_source(mss, &hint)
540 })
541 };
542
543 let info = match attempt(streaming::ProbeMode::Full, false) {
547 Ok(info) => Some((info, streaming::ProbeMode::Full)),
548 Err(e) => {
549 log::info!(
555 "stream probe: {} needs more than has arrived ({}), opening without a length",
556 path.display(),
557 e
558 );
559 attempt(streaming::ProbeMode::Lengthless, true)
560 .ok()
561 .map(|info| (info, streaming::ProbeMode::Lengthless))
562 }
563 };
564
565 match info {
566 Some((info, mode)) => {
567 tx.send(PlayerCommand::StreamProbed {
568 id,
569 info: Box::new(info),
570 mode,
571 })
572 .ok();
573 }
574 None => log::info!(
577 "stream probe: {} cannot start early, waiting for the download",
578 path.display()
579 ),
580 }
581 });
582
583 if let Err(e) = spawned {
584 log::warn!("stream probe: could not spawn for {:?}: {}", id, e);
585 }
586 }
587
588 fn stream_probed(
591 &mut self,
592 id: QueueItemId,
593 info: buffer::StreamInfo,
594 mode: streaming::ProbeMode,
595 ) {
596 if !self.shared_state.is_cursor(id) {
597 return; }
599 if self.shared_state.playback_state() != PlaybackState::Stopped {
600 return; }
602
603 match self.shared_state.item_playback_source(id) {
604 Some(PlaybackSource::Ready(path)) => {
606 if let Err(e) = self.start_playback(id, &path, 0) {
607 log::error!("stream probe: playback failed: {}", e);
608 }
609 }
610 Some(PlaybackSource::Streaming {
611 path,
612 bytes_written,
613 total,
614 }) => {
615 let source = StreamSource {
616 path,
617 bytes_written,
618 total,
619 mode,
620 };
621 if let Err(e) = self.start_streaming_playback(id, source, 0, info) {
622 log::error!("stream probe: streaming playback failed: {}", e);
623 }
624 }
625 None => {}
626 }
627 }
628
629 fn stream_status_fn(
633 &self,
634 id: QueueItemId,
635 ) -> Arc<dyn Fn() -> streaming::StreamStatus + Send + Sync> {
636 let state = self.shared_state.clone();
637 Arc::new(move || match state.item_load_state(id) {
638 Some(LoadState::Ready) => streaming::StreamStatus::Complete,
639 Some(LoadState::Failed(_)) => streaming::StreamStatus::Failed,
640 _ => streaming::StreamStatus::Downloading,
641 })
642 }
643
644 fn start_streaming_playback(
654 &mut self,
655 id: QueueItemId,
656 source: StreamSource,
657 seek_ms: u64,
658 info: buffer::StreamInfo,
659 ) -> Result<(), PlayerError> {
660 let result = self.open_streaming_playback(id, source, seek_ms, info);
661 if result.is_err() {
662 self.stop_playback_and_clear_state();
663 }
664 result
665 }
666
667 fn open_streaming_playback(
668 &mut self,
669 id: QueueItemId,
670 source: StreamSource,
671 seek_ms: u64,
672 info: buffer::StreamInfo,
673 ) -> Result<(), PlayerError> {
674 self.stop_engine();
675 self.stream_mode = source.mode;
677 let path = source.path.as_path();
678
679 let status = self.stream_status_fn(id);
680 let open_source = {
681 let StreamSource {
682 path,
683 bytes_written,
684 total,
685 mode,
686 } = source.clone();
687 let status = status.clone();
688 move || {
689 streaming::PartialFileSource::open(
690 &path,
691 bytes_written.clone(),
692 total,
693 status.clone(),
694 mode,
695 )
696 }
697 };
698
699 self.shared_state.set_track_info(Some(TrackInfo {
700 id,
701 path: path.to_path_buf(),
702 codec: info.codec.clone(),
703 sample_rate: info.sample_rate,
704 bit_depth: info.bit_depth,
705 bitrate_kbps: info.bitrate_kbps,
706 channels: info.channels,
707 duration_ms: info.duration_ms,
708 }));
709 self.shared_state.set_position_ms(seek_ms);
710 self.on_track_changed(id);
711 log::info!(
712 "streaming: {} ({:?}) — {} {}Hz/{}ch, {}ms{}",
713 path.display(),
714 id,
715 info.codec,
716 info.sample_rate,
717 info.channels,
718 info.duration_ms,
719 if seek_ms > 0 {
720 format!(" @{}ms", seek_ms)
721 } else {
722 String::new()
723 },
724 );
725
726 let (producer, consumer) = rtrb::RingBuffer::new(RING_BUFFER_SIZE);
727
728 self.timeline.reset();
729
730 let advance_state = self.shared_state.clone();
732 let decode_cursor = parking_lot::Mutex::new(Some(id));
733 let next_track = move || {
734 let current = decode_cursor.lock().take()?;
735 let next = advance_state.peek_next_ready_after(current);
736 if let Some((next_id, _)) = &next {
737 let mut guard = decode_cursor.lock();
738 *guard = Some(*next_id);
739 }
740 next
741 };
742
743 let first = buffer::SourceEntry {
744 id,
745 path: path.to_path_buf(),
746 hint: hint_for(path),
747 make_mss: Box::new(move || {
748 Ok(symphonia::core::io::MediaSourceStream::new(
749 Box::new(open_source()?),
750 Default::default(),
751 ))
752 }),
753 };
754
755 let cfg = crate::config::Config::load_or_default();
757 let rg_mode = cfg.playback.replaygain;
758 let pre_amp_db = cfg.playback.pre_amp_db;
759
760 let finish_tx = self.commands.tx.clone();
761 let (_stream_info, decode_handle) = buffer::start_decode(
762 first,
763 producer,
764 seek_ms,
765 move || {
766 let (next_id, next_path) = next_track()?;
767 Some(buffer::SourceEntry::from_file(next_id, next_path))
768 },
769 self.timeline.clone(),
770 Some(self.viz_buffer.clone()),
771 rg_mode,
772 pre_amp_db,
773 move || {
774 finish_tx.send(PlayerCommand::DecodeFinished).ok();
775 },
776 )?;
777
778 let (engine, rate_watch) = self.create_engine_for(&info, consumer)?;
779 engine.start()?;
780
781 self.shared_state.set_playback_state(PlaybackState::Playing);
782
783 self.active_playback = Some(ActivePlayback {
784 engine,
785 decode_handle,
786 _rate_watch: rate_watch,
787 });
788
789 Ok(())
790 }
791
792 pub fn seek(&mut self, position_ms: u64) {
799 let Some(info) = self.shared_state.track_info() else {
800 return;
801 };
802 let seekable = self.shared_state.seekable_ms();
804 if seekable == 0 {
805 log::debug!("seek declined: {:?} is not seekable yet", info.id);
809 return;
810 }
811 let ceiling = seekable.min(
812 self.shared_state
813 .duration_ms()
814 .saturating_sub(SEEK_END_GUARD_MS),
815 );
816 let clamped = position_ms.min(ceiling);
817
818 if let Err(e) = self.restart_current(&info, clamped) {
819 log::error!("seek failed: {}", e);
820 }
821 }
822
823 fn restart_current(&mut self, info: &TrackInfo, position_ms: u64) -> Result<(), PlayerError> {
831 let was_paused = self.shared_state.playback_state() == PlaybackState::Paused;
832
833 match self.shared_state.item_playback_source(info.id) {
834 Some(PlaybackSource::Streaming {
835 path,
836 bytes_written,
837 total,
838 }) => {
839 let known = buffer::StreamInfo {
843 codec: info.codec.clone(),
844 sample_rate: info.sample_rate,
845 channels: info.channels,
846 bit_depth: info.bit_depth,
847 bitrate_kbps: info.bitrate_kbps,
848 duration_ms: info.duration_ms,
849 };
850 let source = StreamSource {
851 path,
852 bytes_written,
853 total,
854 mode: self.stream_mode,
855 };
856 self.start_streaming_playback(info.id, source, position_ms, known)?;
857 }
858 Some(PlaybackSource::Ready(path)) => {
859 self.start_playback(info.id, &path, position_ms)?;
860 }
861 None => return Ok(()),
862 }
863
864 if was_paused {
865 self.pause();
866 }
867 Ok(())
868 }
869
870 pub fn next_track(&mut self) {
872 match self.shared_state.advance_cursor_loadable() {
873 Some(id) => self.play(id),
874 None => {
875 log::info!("no more tracks in playlist");
876 self.stop_playback_and_clear_state();
877 }
878 }
879 }
880
881 pub fn prev_track(&mut self) {
883 match self.shared_state.retreat_cursor() {
884 Some((id, path)) => {
885 if matches!(path.try_exists(), Ok(true)) {
886 if let Err(e) = self.start_playback(id, &path, 0) {
887 log::error!("prev track failed: {}", e);
888 }
889 } else {
890 log::warn!("prev track path doesn't exist: {}", path.display());
891 }
892 }
893 None => {
894 if let Some(info) = self.shared_state.track_info()
896 && let Err(e) = self.restart_current(&info, 0)
897 {
898 log::error!("restart failed: {}", e);
899 }
900 }
901 }
902 }
903
904 pub fn pause(&mut self) {
906 if let Some(ref playback) = self.active_playback {
907 if let Err(e) = playback.engine.stop() {
908 log::error!("pause failed: {}", e);
909 return;
910 }
911 self.shared_state.set_playback_state(PlaybackState::Paused);
912 }
913 }
914
915 pub fn resume(&mut self) {
917 if let Some(ref playback) = self.active_playback {
918 if let Err(e) = playback.engine.start() {
919 log::error!("resume failed: {}", e);
920 return;
921 }
922 self.shared_state.set_playback_state(PlaybackState::Playing);
923 }
924 }
925
926 pub fn stop(&mut self) {
928 self.shared_state.clear_playlist();
929 self.stop_playback_and_clear_state();
930 }
931
932 fn stop_engine(&mut self) {
939 let Some(playback) = self.active_playback.take() else {
940 return;
941 };
942 let ActivePlayback {
943 engine,
944 mut decode_handle,
945 _rate_watch,
946 } = playback;
947
948 let _ = engine.stop();
959 decode_handle.stop();
960 drop(engine);
961 }
962
963 fn stop_playback_and_clear_state(&mut self) {
965 self.finish_play();
966 self.stop_engine();
967 self.timeline.reset();
968 self.shared_state.set_playback_state(PlaybackState::Stopped);
969 self.shared_state.set_position_ms(0);
970 self.shared_state.set_track_info(None);
971 }
972
973 pub fn remove_from_playlist(&mut self, id: QueueItemId) {
980 let was_cursor = self.shared_state.is_cursor(id);
981 let resume_after = was_cursor
982 .then(|| self.shared_state.item_before(id))
983 .flatten();
984 self.shared_state.remove_item(id);
985 if was_cursor {
986 self.shared_state.set_cursor(resume_after);
987 self.next_track();
988 }
989 }
990
991 pub fn track_ready(&mut self, id: QueueItemId) {
994 self.shared_state.update_item_state(id, ItemState::Ready);
996
997 if !self.shared_state.is_cursor(id) {
998 return;
999 }
1000
1001 let is_playing = self.shared_state.playback_state() == PlaybackState::Playing;
1002 let current_track_id = self.shared_state.track_info().map(|t| t.id);
1003
1004 if is_playing && current_track_id == Some(id) {
1005 log::info!(
1008 "track_ready: download complete while streaming {:?}, refreshing metadata",
1009 id
1010 );
1011 self.refresh_track_metadata(id);
1012 return;
1013 }
1014
1015 if !is_playing && let Some(path) = self.shared_state.item_path_if_ready(id) {
1017 log::info!("track_ready: starting playback for {:?}", id);
1018 if let Err(e) = self.start_playback(id, &path, 0) {
1019 log::error!("track_ready playback failed: {}", e);
1020 }
1021 }
1022 }
1023
1024 pub fn track_stream_ready(&mut self, id: QueueItemId) {
1027 if !self.shared_state.is_cursor(id) {
1028 return;
1029 }
1030
1031 let is_playing = self.shared_state.playback_state() == PlaybackState::Playing;
1032 if is_playing {
1033 return; }
1035
1036 match self.shared_state.item_playback_source(id) {
1037 Some(PlaybackSource::Streaming {
1038 path,
1039 bytes_written,
1040 total,
1041 }) => {
1042 log::info!("track_stream_ready: probing partial file for {:?}", id);
1043 self.probe_stream_for_playback(id, &path, bytes_written, total);
1044 }
1045 Some(PlaybackSource::Ready(path)) => {
1046 log::info!(
1048 "track_stream_ready: track already ready, starting normal playback for {:?}",
1049 id
1050 );
1051 if let Err(e) = self.start_playback(id, &path, 0) {
1052 log::error!("track_stream_ready playback failed: {}", e);
1053 }
1054 }
1055 None => {} }
1057 }
1058
1059 fn refresh_track_metadata(&mut self, id: QueueItemId) {
1063 use crate::index::metadata;
1064
1065 let path = match self.shared_state.item_path_if_ready(id) {
1066 Some(p) => p,
1067 None => return,
1068 };
1069
1070 match metadata::read_metadata(&path) {
1071 Ok(meta) => {
1072 self.shared_state.update_item_metadata(
1074 id,
1075 meta.title,
1076 meta.artist,
1077 meta.album_artist.unwrap_or_default(),
1078 meta.album,
1079 meta.duration_ms.map(|d| d as u64),
1080 );
1081
1082 if let Some(current) = self.shared_state.track_info()
1091 && current.id == id
1092 {
1093 let probed = buffer::probe_file(&path).ok();
1094 let duration_ms = probed
1095 .as_ref()
1096 .map(|s| s.duration_ms)
1097 .filter(|d| *d > current.duration_ms)
1098 .unwrap_or(current.duration_ms);
1099 if duration_ms != current.duration_ms {
1100 log::info!(
1101 "track_ready: duration corrected {}ms → {}ms",
1102 current.duration_ms,
1103 duration_ms
1104 );
1105 }
1106 self.shared_state.set_track_info(Some(TrackInfo {
1107 duration_ms,
1108 path: path.clone(),
1109 ..current
1110 }));
1111 }
1112
1113 self.shared_state.signal_metadata_refresh();
1115 log::info!("track_ready: metadata refreshed for {:?}", id);
1116 }
1117 Err(e) => {
1118 log::warn!("track_ready: metadata refresh failed for {:?}: {}", id, e);
1119 }
1120 }
1121 }
1122
1123 fn on_track_changed(&mut self, id: QueueItemId) {
1133 if self.in_flight.as_ref().is_some_and(|f| f.item == id) {
1134 return;
1135 }
1136 self.finish_play();
1137 let track_id = self.shared_state.item_db_id(id);
1138 self.in_flight = Some(InFlight::new(id, track_id));
1139 if let (Some(track_id), Some(recorder)) = (track_id, self.history.as_ref()) {
1140 recorder.record(PlayEvent::Started { track_id });
1141 }
1142 }
1143
1144 fn finish_play(&mut self) -> Option<PlayEvent> {
1147 let flight = self.in_flight.take()?;
1148 let event = PlayEvent::Finished {
1149 track_id: flight.track_id()?,
1150 listened_ms: flight.listened_ms(),
1151 };
1152 if let Some(recorder) = self.history.as_ref() {
1153 recorder.record(event);
1154 }
1155 Some(event)
1156 }
1157
1158 pub fn update_playback_state(&mut self) {
1159 if self.active_playback.is_none() {
1160 return;
1161 }
1162
1163 if let Some((id, path, info, position_ms)) = self.timeline.current_playback() {
1164 self.shared_state.set_position_ms(position_ms);
1165
1166 self.on_track_changed(id);
1169 if let Some(f) = self.in_flight.as_mut() {
1170 f.advance(position_ms);
1171 }
1172
1173 let current_id = self.shared_state.track_info().map(|t| t.id);
1176 if current_id != Some(id) {
1177 log::info!("timeline: now playing {:?}", id);
1178 self.shared_state.set_track_info(Some(TrackInfo {
1179 id,
1180 path,
1181 codec: info.codec,
1182 sample_rate: info.sample_rate,
1183 bit_depth: info.bit_depth,
1184 bitrate_kbps: info.bitrate_kbps,
1185 channels: info.channels,
1186 duration_ms: info.duration_ms,
1187 }));
1188 self.shared_state.set_cursor(Some(id));
1189 }
1190 }
1191 }
1192
1193 pub fn track_failed(&mut self, id: QueueItemId) {
1200 if !self.shared_state.is_cursor(id) {
1201 return;
1202 }
1203 if self.shared_state.playback_state() != PlaybackState::Stopped {
1207 return;
1208 }
1209 log::info!("track {:?} cannot load, moving on", id);
1210 self.next_track();
1211 }
1212
1213 fn on_decode_finished(&mut self) {
1220 log::info!("decode finished, checking for next track");
1221 match self.shared_state.advance_cursor_loadable() {
1222 Some(id) => self.play(id),
1223 None => {
1224 log::info!("no more tracks — stopping");
1225 self.stop_playback_and_clear_state();
1226 }
1227 }
1228 }
1229
1230 fn snapshot_for_undo(
1234 &self,
1235 ids: &[QueueItemId],
1236 ) -> Vec<(Box<state::PlaylistItem>, Option<QueueItemId>)> {
1237 self.shared_state
1238 .items_before(ids)
1239 .into_iter()
1240 .filter_map(|(id, after)| Some((Box::new(self.shared_state.get_item(id)?), after)))
1241 .collect()
1242 }
1243
1244 fn push_undo(&mut self, entry: UndoEntry) {
1246 if let Some(ref mut batch) = self.batch_buffer {
1247 batch.push(entry);
1248 } else {
1249 self.undo_stack.push(entry);
1250 }
1251 }
1252
1253 pub fn process_command(&mut self, cmd: PlayerCommand) {
1255 match cmd {
1256 PlayerCommand::Play(id) => self.play(id),
1257 PlayerCommand::Pause => self.pause(),
1258 PlayerCommand::Resume => self.resume(),
1259 PlayerCommand::Stop => self.stop(),
1260 PlayerCommand::Seek(pos) => self.seek(pos),
1261 PlayerCommand::NextTrack => {
1262 let now = std::time::Instant::now();
1264 if now.duration_since(self.last_skip).as_millis() >= 150 {
1265 self.last_skip = now;
1266 self.next_track();
1267 }
1268 }
1269 PlayerCommand::PrevTrack => {
1270 let now = std::time::Instant::now();
1271 if now.duration_since(self.last_skip).as_millis() >= 150 {
1272 self.last_skip = now;
1273 self.prev_track();
1274 }
1275 }
1276 PlayerCommand::AddToPlaylist(items) => {
1277 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
1278 self.shared_state.add_items(items);
1279 self.push_undo(UndoEntry::Added { ids });
1280 }
1281 PlayerCommand::UpdatePaths(updates) => {
1282 self.shared_state.update_paths(&updates);
1283 if let Some(info) = self.shared_state.track_info()
1284 && let Some((_, new_path)) = updates.iter().find(|(id, _)| *id == info.id)
1285 {
1286 self.shared_state.set_track_info(Some(TrackInfo {
1287 path: new_path.clone(),
1288 ..info
1289 }));
1290 }
1291 }
1292 PlayerCommand::InsertInPlaylist { items, after } => {
1293 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
1294 self.shared_state.insert_items_after(items, after);
1295 self.push_undo(UndoEntry::Inserted { ids });
1296 }
1297 PlayerCommand::ClearPlaylist => {
1298 self.stop_playback_and_clear_state();
1302 let (items, cursor) = self.shared_state.snapshot_playlist();
1303 self.shared_state.clear_playlist();
1304 self.push_undo(UndoEntry::Replaced { items, cursor });
1305 }
1306 PlayerCommand::ReplacePlaylist { items, start } => {
1307 self.stop_playback_and_clear_state();
1311 let (old_items, cursor) = self.shared_state.snapshot_playlist();
1312 self.shared_state.clear_playlist();
1313 self.push_undo(UndoEntry::Replaced {
1314 items: old_items,
1315 cursor,
1316 });
1317
1318 if items.is_empty() {
1319 return;
1320 }
1321 let start_id = items.get(start).unwrap_or(&items[0]).id;
1322 self.shared_state.add_items(items);
1323 self.play(start_id);
1324 }
1325 PlayerCommand::RemoveFromPlaylist(id) => {
1326 let item = self.shared_state.get_item(id);
1327 let after = self.shared_state.item_before(id);
1328 self.remove_from_playlist(id);
1329 if let Some(item) = item {
1330 self.push_undo(UndoEntry::Removed {
1331 items: vec![(Box::new(item), after)],
1332 });
1333 }
1334 }
1335 PlayerCommand::RemoveFromPlaylistBatch(ids) => {
1336 let items_with_pos = self.snapshot_for_undo(&ids);
1340 let resume_after = match self.shared_state.cursor() {
1341 Some(cursor) if ids.contains(&cursor) => {
1342 Some(self.shared_state.surviving_item_before(cursor, &ids))
1343 }
1344 _ => None,
1345 };
1346
1347 self.shared_state.remove_items(&ids);
1348
1349 if let Some(resume_after) = resume_after {
1350 self.shared_state.set_cursor(resume_after);
1351 self.next_track();
1352 }
1353
1354 if !items_with_pos.is_empty() {
1355 self.push_undo(UndoEntry::Removed {
1356 items: items_with_pos,
1357 });
1358 }
1359 }
1360 PlayerCommand::MoveInPlaylist { id, target, after } => {
1361 let was_after = self.shared_state.item_before(id);
1362 self.shared_state.move_item(id, target, after);
1363 self.push_undo(UndoEntry::Moved { id, was_after });
1364 }
1365 PlayerCommand::MoveItemsInPlaylist { ids, target, after } => {
1366 let entries = self.shared_state.items_before(&ids);
1367 self.shared_state.move_items(&ids, target, after);
1368 self.push_undo(UndoEntry::MovedBatch { entries });
1369 }
1370 PlayerCommand::ReorderPlaylist(order) => {
1371 let entries = self.shared_state.items_before(&order);
1375 self.shared_state.reorder_to(&order);
1376 self.push_undo(UndoEntry::MovedBatch { entries });
1377 }
1378 PlayerCommand::TrackReady(id) => self.track_ready(id),
1379 PlayerCommand::DecodeFinished => self.on_decode_finished(),
1380 PlayerCommand::TrackStreamReady(id) => self.track_stream_ready(id),
1381 PlayerCommand::StreamProbed { id, info, mode } => self.stream_probed(id, *info, mode),
1382 PlayerCommand::TrackFailed(id) => self.track_failed(id),
1383 PlayerCommand::Undo => self.execute_undo(),
1384 PlayerCommand::Redo => self.execute_redo(),
1385 PlayerCommand::BeginUndoBatch => {
1386 self.batch_buffer = Some(Vec::new());
1387 }
1388 PlayerCommand::EndUndoBatch => {
1389 if let Some(entries) = self.batch_buffer.take() {
1390 if entries.len() == 1 {
1391 self.undo_stack.push(entries.into_iter().next().unwrap());
1393 } else if !entries.is_empty() {
1394 self.undo_stack.push(UndoEntry::Batch(entries));
1395 }
1396 }
1397 }
1398 PlayerCommand::SetOutputDevice(name) => self.set_output_device(name),
1399 PlayerCommand::ClearOutputDevice => self.clear_output_device(),
1400 }
1401 }
1402
1403 fn apply_entry(&mut self, entry: UndoEntry) -> Option<UndoEntry> {
1405 match entry {
1406 UndoEntry::Added { ids } => {
1407 let items_with_pos = self.snapshot_for_undo(&ids);
1409 self.shared_state.remove_items(&ids);
1410 Some(UndoEntry::Removed {
1411 items: items_with_pos,
1412 })
1413 }
1414 UndoEntry::Removed { items } => {
1415 let mut ids = Vec::with_capacity(items.len());
1417 for (item, after) in items {
1418 ids.push(item.id);
1419 self.shared_state.insert_item_at(*item, after);
1420 }
1421 Some(UndoEntry::Added { ids })
1422 }
1423 UndoEntry::Inserted { ids } => {
1424 let items_with_pos = self.snapshot_for_undo(&ids);
1426 self.shared_state.remove_items(&ids);
1427 Some(UndoEntry::Removed {
1428 items: items_with_pos,
1429 })
1430 }
1431 UndoEntry::Moved { id, was_after } => {
1432 let current_after = self.shared_state.item_before(id);
1433 self.shared_state.move_item_to(id, was_after);
1434 Some(UndoEntry::Moved {
1435 id,
1436 was_after: current_after,
1437 })
1438 }
1439 UndoEntry::MovedBatch { entries } => {
1440 let ids: Vec<QueueItemId> = entries.iter().map(|(id, _)| *id).collect();
1441 let current_positions = self.shared_state.items_before(&ids);
1442 self.shared_state.move_items_to(&entries);
1443 Some(UndoEntry::MovedBatch {
1444 entries: current_positions,
1445 })
1446 }
1447 UndoEntry::Replaced { items, cursor } => {
1448 let (current_items, current_cursor) = self.shared_state.snapshot_playlist();
1449 self.shared_state.restore_playlist(items, cursor);
1450 Some(UndoEntry::Replaced {
1451 items: current_items,
1452 cursor: current_cursor,
1453 })
1454 }
1455 UndoEntry::Batch(entries) => {
1456 let mut inverses = Vec::with_capacity(entries.len());
1458 for entry in entries.into_iter().rev() {
1459 if let Some(inverse) = self.apply_entry(entry) {
1460 inverses.push(inverse);
1461 }
1462 }
1463 inverses.reverse();
1464 Some(UndoEntry::Batch(inverses))
1465 }
1466 }
1467 }
1468
1469 fn reconcile_playback(&mut self) {
1483 let Some(playing) = self.shared_state.track_info().map(|t| t.id) else {
1484 return;
1485 };
1486 if self.shared_state.get_item(playing).is_some() {
1487 return;
1488 }
1489 let resume = (self.shared_state.playback_state() == PlaybackState::Playing)
1494 .then(|| self.shared_state.cursor())
1495 .flatten();
1496 self.stop_playback_and_clear_state();
1497 if let Some(id) = resume {
1498 self.play(id);
1499 }
1500 }
1501
1502 fn execute_undo(&mut self) {
1504 let Some(entry) = self.undo_stack.pop_undo() else {
1505 return;
1506 };
1507 if let Some(inverse) = self.apply_entry(entry) {
1508 self.undo_stack.push_redo(inverse);
1509 }
1510 self.reconcile_playback();
1511 }
1512
1513 fn execute_redo(&mut self) {
1515 let Some(entry) = self.undo_stack.pop_redo() else {
1516 return;
1517 };
1518 if let Some(inverse) = self.apply_entry(entry) {
1519 self.undo_stack.push_undo_keep_redo(inverse);
1520 }
1521 self.reconcile_playback();
1522 }
1523
1524 pub fn run(&mut self) {
1526 use std::time::Duration;
1527
1528 let rx = self.commands.rx.clone();
1529 loop {
1530 match rx.recv_timeout(Duration::from_millis(50)) {
1532 Ok(cmd) => self.process_command(cmd),
1533 Err(crossbeam_channel::RecvTimeoutError::Timeout) => {}
1534 Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
1535 }
1536 self.update_playback_state();
1537 }
1538 self.stop();
1539 }
1540
1541 pub fn spawn() -> (
1544 Arc<SharedPlayerState>,
1545 Arc<PlaybackTimeline>,
1546 Arc<VizSnapshot>,
1547 crossbeam_channel::Sender<PlayerCommand>,
1548 ) {
1549 let mut player = Self::new();
1550 player.history = PlayRecorder::spawn();
1551 let state = player.shared_state();
1552 let timeline = player.timeline();
1553 let viz_snapshot = player.viz_snapshot();
1554 let tx = player.command_sender();
1555
1556 thread::Builder::new()
1557 .name("koan-player".into())
1558 .spawn(move || player.run())
1559 .expect("failed to spawn player thread");
1560
1561 (state, timeline, viz_snapshot, tx)
1562 }
1563}
1564
1565#[cfg(test)]
1566mod tests {
1567 use super::*;
1568 use state::PlaylistItem;
1569 use std::path::PathBuf;
1570
1571 fn make_item(title: &str) -> PlaylistItem {
1572 PlaylistItem {
1573 playlist_entry_id: None,
1574 id: QueueItemId::new(),
1575 db_id: None,
1576 path: PathBuf::from(format!("/music/{title}.flac")),
1577 title: title.to_string(),
1578 artist: String::new(),
1579 album_artist: String::new(),
1580 album: String::new(),
1581 year: None,
1582 codec: None,
1583 track_number: None,
1584 disc: None,
1585 duration_ms: None,
1586 state: ItemState::Ready,
1587 }
1588 }
1589
1590 fn playlist_ids(player: &Player) -> Vec<QueueItemId> {
1591 let (items, _) = player.shared_state.snapshot_playlist();
1592 items.iter().map(|i| i.id).collect()
1593 }
1594
1595 fn playlist_titles(player: &Player) -> Vec<String> {
1596 let (items, _) = player.shared_state.snapshot_playlist();
1597 items.iter().map(|i| i.title.clone()).collect()
1598 }
1599
1600 fn pending_item(title: &str) -> PlaylistItem {
1601 PlaylistItem {
1602 playlist_entry_id: None,
1603 state: ItemState::Pending,
1604 ..make_item(title)
1605 }
1606 }
1607
1608 fn pretend_playing(player: &mut Player, id: QueueItemId) {
1612 let item = player
1613 .shared_state
1614 .get_item(id)
1615 .expect("item is in the queue");
1616 player.shared_state.set_track_info(Some(TrackInfo {
1617 id,
1618 path: item.path,
1619 codec: String::new(),
1620 sample_rate: 44_100,
1621 bit_depth: None,
1622 bitrate_kbps: None,
1623 channels: 2,
1624 duration_ms: 1_000,
1625 }));
1626 player
1627 .shared_state
1628 .set_playback_state(PlaybackState::Playing);
1629 }
1630
1631 fn playing_id(player: &Player) -> Option<QueueItemId> {
1632 player.shared_state.track_info().map(|t| t.id)
1633 }
1634
1635 fn seed(player: &mut Player, n: usize) -> Vec<QueueItemId> {
1637 let items: Vec<_> = (0..n).map(|i| make_item(&format!("t{i}"))).collect();
1638 let ids = items.iter().map(|i| i.id).collect();
1639 player.process_command(PlayerCommand::AddToPlaylist(items));
1640 ids
1641 }
1642
1643 fn listen(player: &mut Player, from_ms: u64, to_ms: u64) {
1647 let mut at = from_ms;
1648 if let Some(f) = player.in_flight.as_mut() {
1649 f.advance(at); }
1651 while at < to_ms {
1652 at = (at + 50).min(to_ms);
1653 if let Some(f) = player.in_flight.as_mut() {
1654 f.advance(at);
1655 }
1656 }
1657 }
1658
1659 fn start(player: &mut Player, track_id: i64) -> QueueItemId {
1660 let id = QueueItemId::new();
1661 player.on_track_changed(id);
1662 player
1664 .in_flight
1665 .as_mut()
1666 .unwrap()
1667 .track_id_for_test(track_id);
1668 id
1669 }
1670
1671 #[test]
1672 fn a_gapless_transition_closes_the_outgoing_track_and_opens_the_next() {
1673 let mut player = Player::new();
1674 start(&mut player, 11);
1675 listen(&mut player, 0, 200_000);
1676
1677 let b = QueueItemId::new();
1678 player.on_track_changed(b);
1679 let f = player
1680 .in_flight
1681 .as_ref()
1682 .expect("the next track is counting");
1683 assert_eq!(f.item, b);
1684 assert_eq!(f.listened_ms(), 0, "and starts from nothing");
1685 }
1686
1687 #[test]
1688 fn a_track_skipped_seconds_in_is_still_history() {
1689 let mut player = Player::new();
1690 start(&mut player, 7);
1691 listen(&mut player, 0, 2_000);
1692
1693 let event = player
1694 .finish_play()
1695 .expect("putting something on is a thing you did, however briefly");
1696 assert!(matches!(
1697 event,
1698 history::PlayEvent::Finished {
1699 track_id: 7,
1700 listened_ms: 2_000
1701 }
1702 ));
1703 }
1704
1705 #[test]
1706 fn a_track_is_closed_out_once() {
1707 let mut player = Player::new();
1708 start(&mut player, 7);
1709 listen(&mut player, 0, 200_000);
1710
1711 assert!(player.finish_play().is_some());
1712 assert!(player.finish_play().is_none());
1713 }
1714
1715 #[test]
1716 fn seeking_around_a_track_does_not_enter_it_twice() {
1717 let mut player = Player::new();
1718 let id = start(&mut player, 7);
1719 listen(&mut player, 0, 120_000);
1720
1721 player.on_track_changed(id);
1723 assert_eq!(
1724 player.in_flight.as_ref().unwrap().listened_ms(),
1725 120_000,
1726 "the seek kept the count rather than restarting it"
1727 );
1728 listen(&mut player, 30_000, 40_000);
1729
1730 let Some(history::PlayEvent::Finished { listened_ms, .. }) = player.finish_play() else {
1731 panic!("still one play");
1732 };
1733 assert_eq!(listened_ms, 130_000);
1734 assert!(player.finish_play().is_none());
1735 }
1736
1737 #[test]
1738 fn a_track_that_is_not_in_the_library_is_not_recorded() {
1739 let mut player = Player::new();
1740 let id = QueueItemId::new();
1741 player.on_track_changed(id);
1742 listen(&mut player, 0, 200_000);
1743 assert!(player.finish_play().is_none());
1744 }
1745
1746 #[test]
1747 fn stopping_closes_out_what_was_heard() {
1748 let mut player = Player::new();
1749 start(&mut player, 7);
1750 listen(&mut player, 0, 150_000);
1751
1752 player.stop_playback_and_clear_state();
1753 assert!(player.in_flight.is_none(), "the stop consumed it");
1754 }
1755
1756 #[test]
1757 fn removing_the_playing_track_resumes_at_its_successor() {
1758 let mut player = Player::new();
1759 let ids = seed(&mut player, 5);
1760 player.shared_state.set_cursor(Some(ids[2]));
1761
1762 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[2]));
1763
1764 assert_eq!(
1765 player.shared_state.cursor(),
1766 Some(ids[3]),
1767 "playback must continue at the next track, not restart the queue"
1768 );
1769 assert_eq!(player.playback_starts, 1);
1770 }
1771
1772 #[test]
1773 fn removing_the_first_playing_track_resumes_at_the_new_first() {
1774 let mut player = Player::new();
1775 let ids = seed(&mut player, 3);
1776 player.shared_state.set_cursor(Some(ids[0]));
1777
1778 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[0]));
1779
1780 assert_eq!(player.shared_state.cursor(), Some(ids[1]));
1781 }
1782
1783 #[test]
1784 fn next_track_parks_on_a_track_that_has_not_downloaded_yet() {
1785 let mut player = Player::new();
1786 let playing = make_item("playing");
1787 let waiting = pending_item("waiting");
1788 let later = make_item("later");
1789 let (playing_id, waiting_id) = (playing.id, waiting.id);
1790 player.process_command(PlayerCommand::AddToPlaylist(vec![playing, waiting, later]));
1791 player.shared_state.set_cursor(Some(playing_id));
1792
1793 player.process_command(PlayerCommand::DecodeFinished);
1794
1795 assert_eq!(
1796 player.shared_state.cursor(),
1797 Some(waiting_id),
1798 "the cursor parks on the track being fetched"
1799 );
1800 assert_eq!(
1801 player.playback_starts, 0,
1802 "nothing to play until its bytes land"
1803 );
1804
1805 player
1808 .shared_state
1809 .update_item_state(waiting_id, ItemState::Ready);
1810 player.process_command(PlayerCommand::TrackReady(waiting_id));
1811
1812 assert_eq!(player.playback_starts, 1);
1813 assert_eq!(player.shared_state.cursor(), Some(waiting_id));
1814 }
1815
1816 #[test]
1817 fn a_download_that_cannot_land_moves_the_cursor_on() {
1818 let mut player = Player::new();
1819 let waiting = pending_item("waiting");
1820 let later = make_item("later");
1821 let (waiting_id, later_id) = (waiting.id, later.id);
1822 player.process_command(PlayerCommand::AddToPlaylist(vec![waiting, later]));
1823
1824 player.process_command(PlayerCommand::Play(waiting_id));
1825 assert_eq!(player.playback_starts, 0, "nothing to play yet");
1826
1827 player
1829 .shared_state
1830 .update_item_state(waiting_id, ItemState::Failed("remote unavailable".into()));
1831 player.process_command(PlayerCommand::TrackFailed(waiting_id));
1832
1833 assert_eq!(
1834 player.shared_state.cursor(),
1835 Some(later_id),
1836 "the queue moves past a track that can never load"
1837 );
1838 assert_eq!(player.playback_starts, 1);
1839 }
1840
1841 #[test]
1842 fn a_queue_that_can_never_load_stops_rather_than_waiting() {
1843 let mut player = Player::new();
1844 let first = pending_item("first");
1845 let second = pending_item("second");
1846 let (first_id, second_id) = (first.id, second.id);
1847 player.process_command(PlayerCommand::AddToPlaylist(vec![first, second]));
1848
1849 player.process_command(PlayerCommand::Play(first_id));
1850 for id in [first_id, second_id] {
1851 player
1852 .shared_state
1853 .update_item_state(id, ItemState::Failed("remote unavailable".into()));
1854 player.process_command(PlayerCommand::TrackFailed(id));
1855 }
1856
1857 assert_eq!(player.playback_starts, 0);
1858 assert_eq!(
1859 player.shared_state.playback_state(),
1860 PlaybackState::Stopped,
1861 "a stop the UI can see, not an indefinite wait for TrackReady"
1862 );
1863 }
1864
1865 #[test]
1866 fn a_failure_elsewhere_in_the_queue_leaves_the_cursor_alone() {
1867 let mut player = Player::new();
1868 let waiting = pending_item("waiting");
1869 let other = pending_item("other");
1870 let (waiting_id, other_id) = (waiting.id, other.id);
1871 player.process_command(PlayerCommand::AddToPlaylist(vec![waiting, other]));
1872 player.process_command(PlayerCommand::Play(waiting_id));
1873
1874 player
1875 .shared_state
1876 .update_item_state(other_id, ItemState::Failed("remote unavailable".into()));
1877 player.process_command(PlayerCommand::TrackFailed(other_id));
1878
1879 assert_eq!(
1880 player.shared_state.cursor(),
1881 Some(waiting_id),
1882 "a track still downloading keeps the cursor"
1883 );
1884 }
1885
1886 #[test]
1887 fn batch_delete_containing_the_cursor_restarts_the_engine_once() {
1888 let mut player = Player::new();
1889 let ids = seed(&mut player, 5);
1890 player.shared_state.set_cursor(Some(ids[2]));
1891
1892 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![
1893 ids[1], ids[2], ids[3],
1894 ]));
1895
1896 assert_eq!(playlist_titles(&player), vec!["t0", "t4"]);
1897 assert_eq!(player.shared_state.cursor(), Some(ids[4]));
1898 assert_eq!(
1899 player.playback_starts, 1,
1900 "one resume for the whole selection, not one per deleted track"
1901 );
1902 }
1903
1904 #[test]
1905 fn batch_delete_below_the_cursor_leaves_playback_alone() {
1906 let mut player = Player::new();
1907 let ids = seed(&mut player, 4);
1908 player.shared_state.set_cursor(Some(ids[0]));
1909
1910 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![ids[2], ids[3]]));
1911
1912 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
1913 assert_eq!(player.playback_starts, 0);
1914 }
1915
1916 #[test]
1917 fn undo_of_a_batch_delete_restores_the_original_order() {
1918 let mut player = Player::new();
1922 let items = vec![
1923 make_item("A"),
1924 make_item("B"),
1925 make_item("C"),
1926 make_item("D"),
1927 ];
1928 let (b_id, c_id) = (items[1].id, items[2].id);
1929 player.process_command(PlayerCommand::AddToPlaylist(items));
1930
1931 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![c_id, b_id]));
1932 assert_eq!(playlist_titles(&player), vec!["A", "D"]);
1933
1934 player.process_command(PlayerCommand::Undo);
1935 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
1936 }
1937
1938 #[test]
1941 fn undo_add_removes_items() {
1942 let mut player = Player::new();
1943 let items = vec![make_item("A"), make_item("B")];
1944 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
1945
1946 player.process_command(PlayerCommand::AddToPlaylist(items));
1947 assert_eq!(playlist_ids(&player), ids);
1948 assert!(player.undo_stack().can_undo());
1949
1950 player.process_command(PlayerCommand::Undo);
1951 assert!(playlist_ids(&player).is_empty());
1952 assert!(player.undo_stack().can_redo());
1953 }
1954
1955 #[test]
1956 fn redo_add_restores_items() {
1957 let mut player = Player::new();
1958 let items = vec![make_item("A"), make_item("B")];
1959
1960 player.process_command(PlayerCommand::AddToPlaylist(items));
1961 player.process_command(PlayerCommand::Undo);
1962 assert!(playlist_ids(&player).is_empty());
1963
1964 player.process_command(PlayerCommand::Redo);
1965 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1966 }
1967
1968 #[test]
1971 fn undo_remove_restores_item_at_position() {
1972 let mut player = Player::new();
1973 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1974 let b_id = items[1].id;
1975
1976 player.process_command(PlayerCommand::AddToPlaylist(items));
1977 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1978 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1979
1980 player.process_command(PlayerCommand::Undo);
1981 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1982 }
1983
1984 #[test]
1985 fn undo_remove_first_item() {
1986 let mut player = Player::new();
1987 let items = vec![make_item("A"), make_item("B")];
1988 let a_id = items[0].id;
1989
1990 player.process_command(PlayerCommand::AddToPlaylist(items));
1991 player.process_command(PlayerCommand::RemoveFromPlaylist(a_id));
1992 assert_eq!(playlist_titles(&player), vec!["B"]);
1993
1994 player.process_command(PlayerCommand::Undo);
1995 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1996 }
1997
1998 #[test]
1999 fn undo_batch_remove_restores_all() {
2000 let mut player = Player::new();
2001 let items = vec![
2002 make_item("A"),
2003 make_item("B"),
2004 make_item("C"),
2005 make_item("D"),
2006 ];
2007 let b_id = items[1].id;
2008 let c_id = items[2].id;
2009
2010 player.process_command(PlayerCommand::AddToPlaylist(items));
2011 let version_before = player.shared_state.playlist_version();
2012 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![b_id, c_id]));
2013 assert_eq!(playlist_titles(&player), vec!["A", "D"]);
2014 assert_eq!(
2017 player.shared_state.playlist_version(),
2018 version_before + 1,
2019 "batch removal must bump the playlist version exactly once"
2020 );
2021
2022 player.process_command(PlayerCommand::Undo);
2024 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
2025 }
2026
2027 #[test]
2028 fn redo_batch_remove() {
2029 let mut player = Player::new();
2030 let items = vec![make_item("A"), make_item("B"), make_item("C")];
2031 let a_id = items[0].id;
2032 let b_id = items[1].id;
2033
2034 player.process_command(PlayerCommand::AddToPlaylist(items));
2035 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![a_id, b_id]));
2036 player.process_command(PlayerCommand::Undo);
2037 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
2038
2039 player.process_command(PlayerCommand::Redo);
2040 assert_eq!(playlist_titles(&player), vec!["C"]);
2041 }
2042
2043 #[test]
2044 fn redo_remove() {
2045 let mut player = Player::new();
2046 let items = vec![make_item("A"), make_item("B"), make_item("C")];
2047 let b_id = items[1].id;
2048
2049 player.process_command(PlayerCommand::AddToPlaylist(items));
2050 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
2051 player.process_command(PlayerCommand::Undo);
2052 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
2053
2054 player.process_command(PlayerCommand::Redo);
2055 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
2056 }
2057
2058 #[test]
2061 fn undo_insert_removes_inserted_items() {
2062 let mut player = Player::new();
2063 let items = vec![make_item("A"), make_item("C")];
2064 let a_id = items[0].id;
2065
2066 player.process_command(PlayerCommand::AddToPlaylist(items));
2067
2068 let inserted = vec![make_item("B")];
2069 player.process_command(PlayerCommand::InsertInPlaylist {
2070 items: inserted,
2071 after: a_id,
2072 });
2073 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
2074
2075 player.process_command(PlayerCommand::Undo);
2076 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
2077 }
2078
2079 #[test]
2082 fn undo_move_restores_position() {
2083 let mut player = Player::new();
2084 let items = vec![make_item("A"), make_item("B"), make_item("C")];
2085 let a_id = items[0].id;
2086 let c_id = items[2].id;
2087
2088 player.process_command(PlayerCommand::AddToPlaylist(items));
2089
2090 player.process_command(PlayerCommand::MoveInPlaylist {
2092 id: a_id,
2093 target: c_id,
2094 after: true,
2095 });
2096 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
2097
2098 player.process_command(PlayerCommand::Undo);
2099 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
2100 }
2101
2102 #[test]
2103 fn redo_move() {
2104 let mut player = Player::new();
2105 let items = vec![make_item("A"), make_item("B"), make_item("C")];
2106 let a_id = items[0].id;
2107 let c_id = items[2].id;
2108
2109 player.process_command(PlayerCommand::AddToPlaylist(items));
2110 player.process_command(PlayerCommand::MoveInPlaylist {
2111 id: a_id,
2112 target: c_id,
2113 after: true,
2114 });
2115 player.process_command(PlayerCommand::Undo);
2116 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
2117
2118 player.process_command(PlayerCommand::Redo);
2119 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
2120 }
2121
2122 #[test]
2125 fn undo_batch_move() {
2126 let mut player = Player::new();
2127 let items = vec![
2128 make_item("A"),
2129 make_item("B"),
2130 make_item("C"),
2131 make_item("D"),
2132 ];
2133 let a_id = items[0].id;
2134 let b_id = items[1].id;
2135 let d_id = items[3].id;
2136
2137 player.process_command(PlayerCommand::AddToPlaylist(items));
2138
2139 player.process_command(PlayerCommand::MoveItemsInPlaylist {
2141 ids: vec![a_id, b_id],
2142 target: d_id,
2143 after: true,
2144 });
2145 assert_eq!(playlist_titles(&player), vec!["C", "D", "A", "B"]);
2146
2147 player.process_command(PlayerCommand::Undo);
2148 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
2149 }
2150
2151 #[test]
2154 fn undo_clear_restores_playlist() {
2155 let mut player = Player::new();
2156 let items = vec![make_item("A"), make_item("B"), make_item("C")];
2157
2158 player.process_command(PlayerCommand::AddToPlaylist(items));
2159 player.process_command(PlayerCommand::ClearPlaylist);
2160 assert!(playlist_ids(&player).is_empty());
2161
2162 player.process_command(PlayerCommand::Undo);
2163 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
2164 }
2165
2166 #[test]
2171 fn undoing_a_replace_does_not_leave_the_engine_on_an_orphaned_track() {
2172 let mut player = Player::new();
2173 let original = seed(&mut player, 3);
2174 player.shared_state.set_cursor(Some(original[0]));
2175 pretend_playing(&mut player, original[0]);
2176
2177 let replacement = vec![make_item("something else")];
2178 let orphan = replacement[0].id;
2179 player.process_command(PlayerCommand::ReplacePlaylist {
2180 items: replacement,
2181 start: 0,
2182 });
2183 pretend_playing(&mut player, orphan);
2185
2186 player.process_command(PlayerCommand::Undo);
2187
2188 assert_eq!(playlist_ids(&player), original, "the queue comes back");
2189 assert!(
2190 player.shared_state.get_item(orphan).is_none(),
2191 "and the replacement is gone from it"
2192 );
2193 assert!(
2194 playing_id(&player).is_none_or(|id| player.shared_state.get_item(id).is_some()),
2195 "so nothing may still be playing out of it"
2196 );
2197 }
2198
2199 #[test]
2201 fn undoing_an_add_does_not_leave_the_engine_on_a_removed_track() {
2202 let mut player = Player::new();
2203 seed(&mut player, 2);
2204 let added = seed(&mut player, 1);
2205 pretend_playing(&mut player, added[0]);
2206
2207 player.process_command(PlayerCommand::Undo);
2208
2209 assert!(player.shared_state.get_item(added[0]).is_none());
2210 assert!(
2211 playing_id(&player).is_none_or(|id| player.shared_state.get_item(id).is_some()),
2212 "the engine cannot be left on the item the undo removed"
2213 );
2214 }
2215
2216 #[test]
2218 fn undoing_a_move_leaves_playback_alone() {
2219 let mut player = Player::new();
2220 let ids = seed(&mut player, 3);
2221 player.shared_state.set_cursor(Some(ids[0]));
2222 pretend_playing(&mut player, ids[0]);
2223 let starts = player.playback_starts;
2224
2225 player.process_command(PlayerCommand::MoveInPlaylist {
2226 id: ids[2],
2227 target: ids[0],
2228 after: false,
2229 });
2230 player.process_command(PlayerCommand::Undo);
2231
2232 assert_eq!(playlist_ids(&player), ids);
2233 assert_eq!(playing_id(&player), Some(ids[0]), "still on the same track");
2234 assert_eq!(player.playback_starts, starts, "and not restarted");
2235 }
2236
2237 #[test]
2238 fn redo_clear() {
2239 let mut player = Player::new();
2240 let items = vec![make_item("A"), make_item("B")];
2241
2242 player.process_command(PlayerCommand::AddToPlaylist(items));
2243 player.process_command(PlayerCommand::ClearPlaylist);
2244 player.process_command(PlayerCommand::Undo);
2245 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
2246
2247 player.process_command(PlayerCommand::Redo);
2248 assert!(playlist_ids(&player).is_empty());
2249 }
2250
2251 #[test]
2254 fn multiple_undos_in_sequence() {
2255 let mut player = Player::new();
2256
2257 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("A")]));
2258 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
2259 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("C")]));
2260 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
2261
2262 player.process_command(PlayerCommand::Undo);
2263 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
2264
2265 player.process_command(PlayerCommand::Undo);
2266 assert_eq!(playlist_titles(&player), vec!["A"]);
2267
2268 player.process_command(PlayerCommand::Undo);
2269 assert!(playlist_ids(&player).is_empty());
2270 }
2271
2272 #[test]
2273 fn undo_redo_undo_cycle() {
2274 let mut player = Player::new();
2275 let items = vec![make_item("A"), make_item("B")];
2276
2277 player.process_command(PlayerCommand::AddToPlaylist(items));
2278 player.process_command(PlayerCommand::Undo);
2279 assert!(playlist_ids(&player).is_empty());
2280
2281 player.process_command(PlayerCommand::Redo);
2282 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
2283
2284 player.process_command(PlayerCommand::Undo);
2285 assert!(playlist_ids(&player).is_empty());
2286 }
2287
2288 #[test]
2289 fn new_action_clears_redo_stack() {
2290 let mut player = Player::new();
2291 let items = vec![make_item("A")];
2292
2293 player.process_command(PlayerCommand::AddToPlaylist(items));
2294 player.process_command(PlayerCommand::Undo);
2295 assert!(player.undo_stack().can_redo());
2296
2297 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
2299 assert!(!player.undo_stack().can_redo());
2300 }
2301
2302 #[test]
2303 fn undo_on_empty_stack_is_noop() {
2304 let mut player = Player::new();
2305 player.process_command(PlayerCommand::Undo);
2306 assert!(playlist_ids(&player).is_empty());
2307 }
2308
2309 #[test]
2310 fn redo_on_empty_stack_is_noop() {
2311 let mut player = Player::new();
2312 player.process_command(PlayerCommand::Redo);
2313 assert!(playlist_ids(&player).is_empty());
2314 }
2315
2316 #[test]
2319 fn playback_commands_not_undoable() {
2320 let mut player = Player::new();
2321 player.process_command(PlayerCommand::Pause);
2322 player.process_command(PlayerCommand::Resume);
2323 player.process_command(PlayerCommand::NextTrack);
2324 player.process_command(PlayerCommand::PrevTrack);
2325 assert!(!player.undo_stack().can_undo());
2326 }
2327
2328 #[test]
2329 fn update_paths_not_undoable() {
2330 let mut player = Player::new();
2331 let items = vec![make_item("A")];
2332 let id = items[0].id;
2333 player.process_command(PlayerCommand::AddToPlaylist(items));
2334
2335 let undo_count = player.undo_stack().undo_len();
2336 player.process_command(PlayerCommand::UpdatePaths(vec![(
2337 id,
2338 PathBuf::from("/new/path.flac"),
2339 )]));
2340 assert_eq!(player.undo_stack().undo_len(), undo_count);
2341 }
2342
2343 #[test]
2346 fn add_remove_undo_undo_produces_original() {
2347 let mut player = Player::new();
2348 let items = vec![make_item("A"), make_item("B"), make_item("C")];
2349 let b_id = items[1].id;
2350 let original_titles = vec!["A", "B", "C"];
2351
2352 player.process_command(PlayerCommand::AddToPlaylist(items));
2353 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
2354 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
2355
2356 player.process_command(PlayerCommand::Undo);
2358 assert_eq!(playlist_titles(&player), original_titles);
2359
2360 player.process_command(PlayerCommand::Undo);
2362 assert!(playlist_ids(&player).is_empty());
2363 }
2364
2365 #[test]
2366 fn interleaved_adds_and_moves_undo() {
2367 let mut player = Player::new();
2368 let items = vec![make_item("A"), make_item("B"), make_item("C")];
2369 let a_id = items[0].id;
2370 let c_id = items[2].id;
2371
2372 player.process_command(PlayerCommand::AddToPlaylist(items));
2373
2374 player.process_command(PlayerCommand::MoveInPlaylist {
2376 id: a_id,
2377 target: c_id,
2378 after: true,
2379 });
2380 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
2381
2382 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("D")]));
2384 assert_eq!(playlist_titles(&player), vec!["B", "C", "A", "D"]);
2385
2386 player.process_command(PlayerCommand::Undo);
2388 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
2389
2390 player.process_command(PlayerCommand::Undo);
2392 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
2393 }
2394
2395 #[test]
2400 fn stop_engine_drops_engine_synchronously() {
2401 use std::sync::atomic::{AtomicBool, Ordering};
2402
2403 struct MockEngine {
2404 dropped: Arc<AtomicBool>,
2405 }
2406 impl AudioEngineHandle for MockEngine {
2407 fn start(&self) -> Result<(), BackendError> {
2408 Ok(())
2409 }
2410 fn stop(&self) -> Result<(), BackendError> {
2411 Ok(())
2412 }
2413 fn is_running(&self) -> bool {
2414 false
2415 }
2416 }
2417 impl Drop for MockEngine {
2418 fn drop(&mut self) {
2419 self.dropped.store(true, Ordering::SeqCst);
2420 }
2421 }
2422
2423 let dropped = Arc::new(AtomicBool::new(false));
2424
2425 let stop_flag = Arc::new(AtomicBool::new(false));
2427 let decode_handle = buffer::DecodeHandle::new_for_test(stop_flag);
2428
2429 let mut player = Player::new();
2430 player.active_playback = Some(ActivePlayback {
2431 engine: Box::new(MockEngine {
2432 dropped: dropped.clone(),
2433 }),
2434 decode_handle,
2435 _rate_watch: None,
2436 });
2437
2438 player.stop_engine();
2439
2440 assert!(
2444 dropped.load(Ordering::SeqCst),
2445 "AudioEngine must be dropped synchronously in stop_engine (GitHub #89)"
2446 );
2447 }
2448
2449 struct StuckBackend {
2454 rate: f64,
2455 asked: Arc<std::sync::Mutex<Option<(f64, u32)>>>,
2456 }
2457
2458 struct NullEngine;
2459 impl AudioEngineHandle for NullEngine {
2460 fn start(&self) -> Result<(), BackendError> {
2461 Ok(())
2462 }
2463 fn stop(&self) -> Result<(), BackendError> {
2464 Ok(())
2465 }
2466 fn is_running(&self) -> bool {
2467 false
2468 }
2469 }
2470
2471 impl AudioBackend for StuckBackend {
2472 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
2473 Ok(vec![self.default_device()?])
2474 }
2475 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
2476 Ok(backend::DeviceInfo {
2477 name: "Stuck DAC".into(),
2478 sample_rates: vec![self.rate],
2479 platform_id: 0,
2480 })
2481 }
2482 fn supported_sample_rates(
2483 &self,
2484 _device: &backend::DeviceInfo,
2485 ) -> Result<Vec<f64>, BackendError> {
2486 Ok(vec![self.rate])
2487 }
2488 fn get_device_sample_rate(
2489 &self,
2490 _device: &backend::DeviceInfo,
2491 ) -> Result<f64, BackendError> {
2492 Ok(self.rate)
2493 }
2494 fn set_device_sample_rate(
2495 &self,
2496 _device: &backend::DeviceInfo,
2497 rate: f64,
2498 ) -> Result<f64, BackendError> {
2499 Err(BackendError::UnsupportedSampleRate(rate))
2500 }
2501 fn create_engine(
2502 &self,
2503 _device: &backend::DeviceInfo,
2504 sample_rate: f64,
2505 channels: u32,
2506 _consumer: rtrb::Consumer<f32>,
2507 _samples_played: Arc<AtomicU64>,
2508 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
2509 *self.asked.lock().unwrap() = Some((sample_rate, channels));
2510 Ok(Box::new(NullEngine))
2511 }
2512 }
2513
2514 fn engine_format_for(source_rate: u32, channels: u16, device_rate: f64) -> (f64, u32) {
2515 let asked = Arc::new(std::sync::Mutex::new(None));
2516 let mut player = Player::new();
2517 player.backend = Box::new(StuckBackend {
2518 rate: device_rate,
2519 asked: asked.clone(),
2520 });
2521
2522 let info = buffer::StreamInfo {
2523 codec: "MP3".into(),
2524 sample_rate: source_rate,
2525 channels,
2526 bit_depth: Some(16),
2527 bitrate_kbps: None,
2528 duration_ms: 1000,
2529 };
2530 let (_producer, consumer) = rtrb::RingBuffer::new(16);
2531 player
2532 .create_engine_for(&info, consumer)
2533 .expect("engine creation should succeed");
2534 let asked = *asked.lock().unwrap();
2535 asked.expect("engine was never created")
2536 }
2537
2538 #[test]
2539 fn engine_uses_source_rate_when_device_refuses_switch() {
2540 assert_eq!(engine_format_for(22050, 2, 48000.0), (22050.0, 2));
2543 assert_eq!(engine_format_for(32000, 2, 44100.0), (32000.0, 2));
2544 }
2545
2546 #[test]
2547 fn engine_uses_source_channel_count() {
2548 assert_eq!(engine_format_for(44100, 1, 44100.0), (44100.0, 1));
2549 }
2550
2551 fn settled_rate_for(source_rate: u32, device_rate: f64) -> Option<u32> {
2553 let mut player = Player::new();
2554 player.backend = Box::new(StuckBackend {
2555 rate: device_rate,
2556 asked: Arc::new(std::sync::Mutex::new(None)),
2557 });
2558 let state = player.shared_state.clone();
2559
2560 let info = buffer::StreamInfo {
2561 codec: "MP3".into(),
2562 sample_rate: source_rate,
2563 channels: 2,
2564 bit_depth: Some(16),
2565 bitrate_kbps: None,
2566 duration_ms: 1000,
2567 };
2568 let (_producer, consumer) = rtrb::RingBuffer::new(16);
2569 player
2570 .create_engine_for(&info, consumer)
2571 .expect("engine creation should succeed");
2572 state.output_sample_rate()
2573 }
2574
2575 #[test]
2576 fn settled_device_rate_reaches_the_shared_state() {
2577 assert_eq!(settled_rate_for(22050, 48000.0), Some(48000));
2581 assert_eq!(settled_rate_for(44100, 44100.0), Some(44100));
2583 }
2584
2585 struct SlowBackend {
2587 observed: Arc<std::sync::Mutex<Vec<Option<u32>>>>,
2588 state: Arc<SharedPlayerState>,
2589 }
2590
2591 impl AudioBackend for SlowBackend {
2592 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
2593 Ok(vec![self.default_device()?])
2594 }
2595 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
2596 Ok(backend::DeviceInfo {
2597 name: "Slow DAC".into(),
2598 sample_rates: vec![44100.0, 48000.0],
2599 platform_id: 0,
2600 })
2601 }
2602 fn supported_sample_rates(
2603 &self,
2604 _device: &backend::DeviceInfo,
2605 ) -> Result<Vec<f64>, BackendError> {
2606 Ok(vec![44100.0, 48000.0])
2607 }
2608 fn get_device_sample_rate(
2609 &self,
2610 _device: &backend::DeviceInfo,
2611 ) -> Result<f64, BackendError> {
2612 Ok(48000.0)
2613 }
2614 fn set_device_sample_rate(
2615 &self,
2616 _device: &backend::DeviceInfo,
2617 rate: f64,
2618 ) -> Result<f64, BackendError> {
2619 self.observed
2621 .lock()
2622 .unwrap()
2623 .push(self.state.output_sample_rate());
2624 Ok(rate)
2625 }
2626 fn create_engine(
2627 &self,
2628 _device: &backend::DeviceInfo,
2629 _sample_rate: f64,
2630 _channels: u32,
2631 _consumer: rtrb::Consumer<f32>,
2632 _samples_played: Arc<AtomicU64>,
2633 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
2634 Ok(Box::new(NullEngine))
2635 }
2636 }
2637
2638 #[test]
2639 fn the_previous_rate_is_not_published_while_the_device_reclocks() {
2640 let mut player = Player::new();
2646 let state = player.shared_state.clone();
2647 state.set_output_sample_rate(48000);
2648
2649 let observed = Arc::new(std::sync::Mutex::new(Vec::new()));
2650 player.backend = Box::new(SlowBackend {
2651 observed: observed.clone(),
2652 state: state.clone(),
2653 });
2654
2655 let info = buffer::StreamInfo {
2656 codec: "FLAC".into(),
2657 sample_rate: 44100,
2658 channels: 2,
2659 bit_depth: Some(16),
2660 bitrate_kbps: None,
2661 duration_ms: 1000,
2662 };
2663 let (_producer, consumer) = rtrb::RingBuffer::new(16);
2664 player
2665 .create_engine_for(&info, consumer)
2666 .expect("engine creation should succeed");
2667
2668 assert_eq!(
2669 *observed.lock().unwrap(),
2670 vec![None],
2671 "mid-switch the output rate must read as unknown, not as the last track's"
2672 );
2673 assert_eq!(state.output_sample_rate(), Some(44100));
2674 }
2675
2676 struct WatchedBackend {
2678 inner: StuckBackend,
2679 #[allow(clippy::type_complexity)]
2680 captured: Arc<std::sync::Mutex<Option<Box<dyn Fn(f64) + Send + Sync>>>>,
2681 }
2682
2683 struct NullWatch;
2684 impl backend::SampleRateWatch for NullWatch {}
2685
2686 impl AudioBackend for WatchedBackend {
2687 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
2688 self.inner.list_devices()
2689 }
2690 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
2691 self.inner.default_device()
2692 }
2693 fn supported_sample_rates(
2694 &self,
2695 device: &backend::DeviceInfo,
2696 ) -> Result<Vec<f64>, BackendError> {
2697 self.inner.supported_sample_rates(device)
2698 }
2699 fn get_device_sample_rate(
2700 &self,
2701 device: &backend::DeviceInfo,
2702 ) -> Result<f64, BackendError> {
2703 self.inner.get_device_sample_rate(device)
2704 }
2705 fn set_device_sample_rate(
2706 &self,
2707 device: &backend::DeviceInfo,
2708 rate: f64,
2709 ) -> Result<f64, BackendError> {
2710 self.inner.set_device_sample_rate(device, rate)
2711 }
2712 fn watch_device_sample_rate(
2713 &self,
2714 _device: &backend::DeviceInfo,
2715 on_change: Box<dyn Fn(f64) + Send + Sync>,
2716 ) -> Option<Box<dyn backend::SampleRateWatch>> {
2717 *self.captured.lock().unwrap() = Some(on_change);
2718 Some(Box::new(NullWatch))
2719 }
2720 fn create_engine(
2721 &self,
2722 device: &backend::DeviceInfo,
2723 sample_rate: f64,
2724 channels: u32,
2725 consumer: rtrb::Consumer<f32>,
2726 samples_played: Arc<AtomicU64>,
2727 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
2728 self.inner
2729 .create_engine(device, sample_rate, channels, consumer, samples_played)
2730 }
2731 }
2732
2733 #[test]
2734 fn external_rate_change_reaches_the_shared_state() {
2735 let captured = Arc::new(std::sync::Mutex::new(None));
2739 let mut player = Player::new();
2740 player.backend = Box::new(WatchedBackend {
2741 inner: StuckBackend {
2742 rate: 44100.0,
2743 asked: Arc::new(std::sync::Mutex::new(None)),
2744 },
2745 captured: captured.clone(),
2746 });
2747 let state = player.shared_state.clone();
2748
2749 let info = buffer::StreamInfo {
2750 codec: "FLAC".into(),
2751 sample_rate: 44100,
2752 channels: 2,
2753 bit_depth: Some(16),
2754 bitrate_kbps: None,
2755 duration_ms: 1000,
2756 };
2757 let (_producer, consumer) = rtrb::RingBuffer::new(16);
2758 player
2759 .create_engine_for(&info, consumer)
2760 .expect("engine creation should succeed");
2761 assert_eq!(state.output_sample_rate(), Some(44100));
2762
2763 let on_change = captured.lock().unwrap().take().expect("watch registered");
2764 on_change(48000.0);
2765 assert_eq!(state.output_sample_rate(), Some(48000));
2766 }
2767}