1pub mod commands;
2pub mod history;
3mod renderer;
4mod sleep;
5pub mod state;
6pub mod undo;
7
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::thread;
11
12use thiserror::Error;
13
14use crate::audio::{
15 analyzer::VizAnalyzer,
16 backend::{self, AudioBackend, AudioEngineHandle, BackendError, SampleRateWatch},
17 buffer, streaming,
18 viz::{VizBuffer, VizSnapshot},
19};
20use crate::remote::client::PlaybackReportState;
21use buffer::PlaybackTimeline;
22use commands::{CommandChannel, PlayerCommand};
23use history::{InFlight, PlayEvent, PlayRecorder, PlaybackReport};
24use state::{
25 ItemState, PlayMode, PlaybackSource, PlaybackState, QueueItemId, QueueMode, Repeat,
26 SharedPlayerState, Sleep, SleepTimer, TrackInfo,
27};
28use undo::{UndoEntry, UndoStack};
29
30pub(crate) const RING_BUFFER_SIZE: usize = 192_000 * 2;
32
33const SEEK_END_GUARD_MS: u64 = 500;
36
37const BOUNDARY_SLACK: std::time::Duration = std::time::Duration::from_millis(5);
40
41const FADE_CHECK: std::time::Duration = std::time::Duration::from_millis(50);
44const QUICK_FADE_LIMIT: std::time::Duration = std::time::Duration::from_millis(100);
47
48const SESSION_GRACE: std::time::Duration = std::time::Duration::from_secs(5);
52
53fn dsp_wake(
59 since: std::time::Instant,
60 period: std::time::Duration,
61 now: std::time::Instant,
62) -> std::time::Instant {
63 let faded = since + crate::audio::fade::QUICK_FADE + period + BOUNDARY_SLACK;
64 [faded, faded + period, since + QUICK_FADE_LIMIT]
65 .into_iter()
66 .find(|&t| t > now)
67 .unwrap_or(now)
68}
69
70#[derive(Debug, Error)]
71pub enum PlayerError {
72 #[error("backend error: {0}")]
73 Backend(#[from] BackendError),
74 #[error("decode error: {0}")]
75 Decode(#[from] buffer::DecodeError),
76 #[error("renderer: {0}")]
77 Renderer(String),
78 #[error("{0}")]
81 Unplayable(String),
82}
83
84#[derive(Clone)]
87struct StreamSource {
88 path: PathBuf,
89 bytes_written: Arc<crate::remote::downloads::ByteFeed>,
90 total: u64,
91 mode: streaming::ProbeMode,
92}
93
94fn media_extension(path: &Path) -> Option<String> {
97 crate::remote::download::strip_part_suffix(path)
98 .extension()
99 .and_then(|e| e.to_str())
100 .map(str::to_ascii_lowercase)
101}
102
103fn hint_for(path: &Path) -> symphonia::core::formats::probe::Hint {
105 let mut hint = symphonia::core::formats::probe::Hint::new();
106 if let Some(ext) = media_extension(path) {
107 hint.with_extension(&ext);
108 }
109 hint
110}
111
112fn lengthless_mode_for(path: &Path) -> streaming::ProbeMode {
122 match media_extension(path).as_deref() {
123 Some("ogg" | "oga" | "opus" | "spx" | "m4a" | "m4b" | "mp4" | "mov") => {
124 streaming::ProbeMode::LengthlessWholeEnd
125 }
126 _ => streaming::ProbeMode::Lengthless,
127 }
128}
129
130pub struct Player {
132 shared_state: Arc<SharedPlayerState>,
133 commands: CommandChannel,
134 transport: Transport,
136 timeline: Arc<PlaybackTimeline>,
137 viz_buffer: Arc<VizBuffer>,
138 viz_snapshot: Arc<VizSnapshot>,
139 _viz_analyzer: VizAnalyzer,
141 undo_stack: UndoStack,
142 batch_buffer: Option<Vec<UndoEntry>>,
145 output_device_name: Option<String>,
147 backend: Box<dyn AudioBackend>,
149 stream_mode: streaming::ProbeMode,
152 history: Option<PlayRecorder>,
155 downloads: Option<crate::remote::queue::DownloadQueue>,
159 in_flight: Option<InFlight>,
161 lead_in_ends: Option<std::time::Instant>,
163 session_release_at: Option<std::time::Instant>,
166 session: u64,
169 silence_waiters: Vec<crossbeam_channel::Sender<u64>>,
171 dsp: Option<DspCache>,
174 dsp_restart: Option<std::time::Instant>,
178 quick_start: bool,
181 renderer: Option<renderer::RendererLink>,
184 mode: PlayMode,
187 sleep: Option<SleepSet>,
189 sleep_fade: Option<sleep::SleepFade>,
191 sleep_snap_on_resume: bool,
193 #[cfg(test)]
196 playback_starts: usize,
197 #[cfg(test)]
201 dsp_override: Option<Arc<crate::audio::dsp::Setup>>,
202 #[cfg(test)]
203 renderer_dsp_override: Option<Arc<crate::audio::dsp::Setup>>,
204 resume_renderer: bool,
207 held_run: Option<Run>,
211}
212
213#[derive(Clone, Copy)]
216struct SleepSet {
217 sleep: Sleep,
218 at: Option<std::time::Instant>,
219 fade: Option<std::time::Duration>,
221}
222
223struct DspCache {
224 config: Arc<crate::config::Config>,
225 device: String,
226 setup: Option<Arc<crate::audio::dsp::Setup>>,
227}
228
229#[derive(Clone, Copy)]
231enum Fade {
232 Cut,
233 Short,
234}
235
236#[derive(Clone, Copy, Debug, PartialEq, Eq)]
239enum Run {
240 Playing,
241 Paused,
242}
243
244enum Transport {
248 Idle,
250 Waiting(Waiting),
253 Loaded(Session),
255}
256
257#[derive(Clone, Copy)]
262struct Waiting {
263 id: QueueItemId,
264 position_ms: u64,
265 start: Run,
266}
267
268impl Waiting {
269 fn may_stream(&self, id: QueueItemId) -> bool {
270 self.id == id && self.position_ms == 0
271 }
272}
273
274struct Session {
277 track: TrackInfo,
280 run: Run,
281 lookahead: Arc<parking_lot::Mutex<Vec<state::Lookahead>>>,
284 output: Output,
285}
286
287enum Output {
289 Local(Local),
292 Renderer(Box<renderer::Play>),
296}
297
298struct Local {
299 engine: Box<dyn AudioEngineHandle>,
300 decode_handle: buffer::DecodeHandle,
301 stream: Option<LiveStream>,
303 _rate_watch: Option<Box<dyn SampleRateWatch>>,
306 dsp: Option<crate::audio::dsp::DspStatus>,
308}
309
310impl Session {
311 fn engine(&self) -> Option<&dyn AudioEngineHandle> {
313 match &self.output {
314 Output::Local(local) => Some(local.engine.as_ref()),
315 Output::Renderer(_) => None,
316 }
317 }
318}
319
320enum Source {
322 File(PathBuf),
323 Stream(StreamSource),
324}
325
326impl Source {
327 fn path(&self) -> &Path {
328 match self {
329 Source::File(path) => path,
330 Source::Stream(source) => &source.path,
331 }
332 }
333}
334
335struct LiveStream {
339 feed: Arc<crate::remote::downloads::ByteFeed>,
340 abandoned: Arc<std::sync::atomic::AtomicBool>,
341}
342
343impl LiveStream {
344 fn abandon(&self) {
345 self.abandoned
346 .store(true, std::sync::atomic::Ordering::Release);
347 self.feed.done();
348 }
349}
350
351impl Default for Player {
352 fn default() -> Self {
353 Self::new()
354 }
355}
356
357impl Player {
358 pub fn new() -> Self {
359 let viz_buffer = VizBuffer::new();
360 let viz_snapshot = VizSnapshot::new();
361 let timeline = PlaybackTimeline::new();
362 let cfg = crate::config::Config::cached();
363 let viz_analyzer = VizAnalyzer::spawn_with_snapshot(
364 Arc::clone(&viz_buffer),
365 &cfg.visualizer,
366 Arc::clone(&viz_snapshot),
367 timeline.samples_played_counter(),
368 );
369
370 let shared_state = SharedPlayerState::new();
371 shared_state.attach_timeline(timeline.clone());
372 let commands = CommandChannel::new();
373 let tx = commands.tx.clone();
374 timeline.on_queued(move || {
375 let _ = tx.try_send(PlayerCommand::TrackQueued);
376 });
377
378 Self {
379 shared_state,
380 commands,
381 transport: Transport::Idle,
382 lead_in_ends: None,
383 session_release_at: None,
384 dsp: None,
385 dsp_restart: None,
386 quick_start: false,
387 session: 0,
388 silence_waiters: Vec::new(),
389 renderer: None,
390 mode: PlayMode::default(),
391 sleep: None,
392 sleep_fade: None,
393 sleep_snap_on_resume: false,
394 timeline,
395 viz_buffer,
396 viz_snapshot,
397 _viz_analyzer: viz_analyzer,
398 undo_stack: UndoStack::new(),
399 batch_buffer: None,
400 output_device_name: cfg.playback.output_device.clone(),
401 backend: crate::audio::platform_backend(),
402 stream_mode: streaming::ProbeMode::Full,
403 history: None,
404 downloads: None,
405 in_flight: None,
406 #[cfg(test)]
407 playback_starts: 0,
408 #[cfg(test)]
409 dsp_override: None,
410 #[cfg(test)]
411 renderer_dsp_override: None,
412 resume_renderer: false,
413 held_run: None,
414 }
415 }
416
417 pub fn shared_state(&self) -> Arc<SharedPlayerState> {
419 self.shared_state.clone()
420 }
421
422 pub fn timeline(&self) -> Arc<PlaybackTimeline> {
424 self.timeline.clone()
425 }
426
427 pub fn viz_buffer(&self) -> Arc<VizBuffer> {
429 self.viz_buffer.clone()
430 }
431
432 pub fn viz_snapshot(&self) -> Arc<VizSnapshot> {
435 self.viz_snapshot.clone()
436 }
437
438 pub fn undo_stack(&self) -> &UndoStack {
440 &self.undo_stack
441 }
442
443 #[allow(clippy::type_complexity)]
451 fn create_engine_for(
452 &mut self,
453 info: &buffer::StreamInfo,
454 consumer: rtrb::Consumer<f32>,
455 ) -> Result<(Box<dyn AudioEngineHandle>, Option<Box<dyn SampleRateWatch>>), PlayerError> {
456 let device = self.resolve_device()?;
457 let device_rate = self.backend.get_device_sample_rate(&device)?;
458 let dsp = match &self.dsp {
461 Some(cache) if cache.device == device.name => cache.setup.clone(),
462 _ => self.dsp_for(&device.name),
463 };
464 let source_rate =
467 dsp.as_ref()
468 .map_or(info.sample_rate, |d| d.output_rate(info.sample_rate)) as f64;
469
470 self.shared_state.clear_output_sample_rate();
474
475 let settled = if (device_rate - source_rate).abs() > 0.1 {
476 log::info!(
477 "switching device sample rate: {}Hz → {}Hz",
478 device_rate,
479 source_rate
480 );
481 match self.backend.set_device_sample_rate(&device, source_rate) {
482 Ok(rate) => rate,
483 Err(e) => {
484 log::warn!("failed to set device sample rate: {}", e);
485 device_rate
486 }
487 }
488 } else {
489 device_rate
490 };
491
492 if (settled - source_rate).abs() > 0.1 {
493 log::warn!(
494 "device stayed at {}Hz (wanted {}Hz) — output is resampled, not bit-perfect",
495 settled,
496 source_rate
497 );
498 }
499
500 self.shared_state
503 .set_output_sample_rate(settled.round() as u32);
504
505 let watch_state = self.shared_state.clone();
509 let watch_name = device.name.clone();
510 let rate_watch = self.backend.watch_device_sample_rate(
511 &device,
512 Box::new(move |rate| {
513 log::info!("device sample rate changed externally: {rate}Hz on '{watch_name}'");
514 watch_state.set_output_sample_rate(rate.round() as u32);
515 }),
516 );
517
518 let engine = self.backend.create_engine(
519 &device,
520 source_rate,
521 info.channels as u32,
522 consumer,
523 self.timeline.samples_played_counter(),
524 )?;
525 let now = std::time::Instant::now();
529 let lead_in = if device_rate > 0.0 && (settled - device_rate).abs() > 0.1 {
530 std::time::Duration::from_millis(
531 crate::config::Config::cached()
532 .playback
533 .rate_switch_lead_in_ms as u64,
534 )
535 } else {
536 self.lead_in_ends
537 .map(|end| end.saturating_duration_since(now))
538 .unwrap_or_default()
539 };
540 self.lead_in_ends = None;
541 if !lead_in.is_zero() {
542 engine.lead_in((settled * lead_in.as_secs_f64()) as u64);
543 self.lead_in_ends = Some(now + lead_in);
544 }
545
546 Ok((engine, rate_watch))
547 }
548
549 fn dsp_for(&mut self, device: &str) -> Option<Arc<crate::audio::dsp::Setup>> {
551 let config = crate::config::Config::cached();
552 #[cfg(test)]
553 if let Some(setup) = if self
554 .renderer
555 .as_ref()
556 .is_some_and(|l| l.device_name() == device)
557 {
558 self.renderer_dsp_override.clone()
559 } else {
560 self.dsp_override.clone()
561 } {
562 self.dsp = Some(DspCache {
563 config,
564 device: device.to_string(),
565 setup: Some(setup.clone()),
566 });
567 return Some(setup);
568 }
569 if let Some(cache) = &self.dsp
570 && Arc::ptr_eq(&cache.config, &config)
571 && cache.device == device
572 {
573 return cache.setup.clone();
574 }
575 let setup = crate::audio::dsp::profiles::for_playing(|| {
576 let chain = crate::audio::dsp::output_chain(&config.dsp, device)?;
577 crate::audio::dsp::Setup::load(&chain.profile, &chain.all, &crate::config::config_dir())
578 .inspect_err(|e| {
579 log::error!(
580 "dsp: profile '{}' not loaded, playing without it: {e}",
581 chain.name
582 )
583 })
584 .ok()
585 .flatten()
586 .map(|mut setup| {
587 setup.name = chain.name;
588 Arc::new(setup)
589 })
590 });
591 self.dsp = Some(DspCache {
592 config,
593 device: device.to_string(),
594 setup: setup.clone(),
595 });
596 setup
597 }
598
599 fn reload_dsp(&mut self) {
605 let device = match &self.renderer {
606 Some(link) => Ok(link.device_name().to_string()),
607 None => self.resolve_device().map(|d| d.name),
608 };
609 let Ok(device) = device else {
610 self.dsp = None;
611 return;
612 };
613 let was = self
614 .dsp
615 .take()
616 .filter(|c| c.device == device)
617 .map(|c| c.setup);
618 let now = self.dsp_for(&device);
619 let changed = match was {
620 Some(was) => was != now,
621 None => now.is_some(),
622 };
623 if changed {
624 self.restart_for_dsp();
625 }
626 }
627
628 fn restart_for_dsp(&mut self) {
635 if self.dsp_restart.is_some() {
636 return;
637 }
638 if let Some(session) = self.session()
639 && session.run == Run::Playing
640 && let Output::Local(local) = &session.output
641 && local.engine.is_running()
642 {
643 local.engine.fade_out_quickly();
644 self.dsp_restart = Some(std::time::Instant::now());
645 return;
646 }
647 self.restart_on_current_track();
648 }
649
650 fn processing(&mut self) -> buffer::Processing {
652 let cfg = crate::config::Config::cached();
653 let dsp = match self.resolve_device() {
654 Ok(device) => self.dsp_for(&device.name),
655 Err(_) => None,
656 };
657 buffer::Processing {
658 rg_mode: cfg.playback.replaygain,
659 pre_amp_db: cfg.playback.pre_amp_db,
660 dsp,
661 }
662 }
663
664 fn resolve_device(&self) -> Result<backend::DeviceInfo, PlayerError> {
667 if let Some(ref name) = self.output_device_name {
668 match self.backend.list_devices() {
669 Ok(devices) => {
670 if let Some(dev) = devices.into_iter().find(|d| d.name == *name) {
671 return Ok(dev);
672 }
673 log::warn!(
674 "configured output device '{}' not found, falling back to default",
675 name,
676 );
677 }
678 Err(e) => {
679 log::warn!("failed to list devices while resolving '{}': {}", name, e);
680 }
681 }
682 }
683 Ok(self.backend.default_device()?)
684 }
685
686 pub fn set_output_device(&mut self, name: String) {
689 log::info!("switching output device to: {}", name);
690 self.output_device_name = Some(name.clone());
691
692 if let Err(e) = crate::config::Config::persist(|cfg| {
693 cfg.playback.output_device = Some(name);
694 cfg.playback.renderer = None;
695 cfg.playback.renderer_name = None;
696 }) {
697 log::error!("failed to save output device config: {}", e);
698 }
699
700 if self.renderer.is_some() {
701 self.use_renderer(None);
702 } else {
703 self.restart_on_current_track();
704 }
705 }
706
707 pub fn clear_output_device(&mut self) {
709 log::info!("reverting to system default output device");
710 self.output_device_name = None;
711
712 if let Err(e) = crate::config::Config::persist(|cfg| {
713 cfg.playback.output_device = None;
714 cfg.playback.renderer = None;
715 cfg.playback.renderer_name = None;
716 }) {
717 log::error!("failed to save output device config: {}", e);
718 }
719
720 if self.renderer.is_some() {
721 self.use_renderer(None);
722 } else {
723 self.restart_on_current_track();
724 }
725 }
726
727 fn restart_on_current_track(&mut self) {
730 let position_ms = self.shared_state.position_ms();
731 if let Err(e) = self.restart_current(position_ms) {
732 log::error!("failed to restart playback on device switch: {}", e);
733 }
734 }
735
736 pub fn output_device_name(&self) -> Option<&str> {
738 self.output_device_name.as_deref()
739 }
740
741 pub fn command_sender(&self) -> crossbeam_channel::Sender<PlayerCommand> {
743 self.commands.tx.clone()
744 }
745
746 fn session(&self) -> Option<&Session> {
747 match &self.transport {
748 Transport::Loaded(session) => Some(session),
749 _ => None,
750 }
751 }
752
753 fn waiting(&self) -> Option<Waiting> {
754 match self.transport {
755 Transport::Waiting(waiting) => Some(waiting),
756 _ => None,
757 }
758 }
759
760 fn may_stream(&self, waiting: &Waiting, id: QueueItemId) -> bool {
763 waiting.may_stream(id) && self.renderer.is_none()
764 }
765
766 fn forget_waiting(&mut self) {
767 if matches!(self.transport, Transport::Waiting(_)) {
768 self.transport = Transport::Idle;
769 }
770 }
771
772 fn publish(&self) {
776 let state = &self.shared_state;
777 let (playback, waiting, track) = match &self.transport {
778 Transport::Idle => (PlaybackState::Stopped, false, None),
779 Transport::Waiting(waiting) => (
780 match waiting.start {
781 Run::Playing => PlaybackState::Stopped,
782 Run::Paused => PlaybackState::Paused,
783 },
784 true,
785 None,
786 ),
787 Transport::Loaded(session) => (
788 match session.run {
789 Run::Playing => PlaybackState::Playing,
790 Run::Paused => PlaybackState::Paused,
791 },
792 false,
793 Some(&session.track),
794 ),
795 };
796 if state.track_info().as_ref() != track {
797 state.set_track_info(track.cloned());
798 }
799 state.set_transport(playback, waiting);
800 let dsp = match &self.transport {
803 Transport::Loaded(Session {
804 output: Output::Local(local),
805 ..
806 }) => local.dsp.clone(),
807 Transport::Loaded(Session {
808 output: Output::Renderer(play),
809 ..
810 }) => play.dsp(),
811 _ => None,
812 };
813 if state.dsp() != dsp {
814 state.set_dsp(dsp);
815 }
816 state.set_play_mode(self.mode);
817 state.set_sleep(self.sleep.map(|s| s.sleep));
818 state.set_sleep_fading(self.sleep_fading());
819 }
820
821 fn intent(&self) -> Option<Run> {
826 match &self.transport {
827 Transport::Idle => None,
828 Transport::Waiting(waiting) => Some(waiting.start),
829 Transport::Loaded(session) => Some(session.run),
830 }
831 }
832
833 fn carry_on(&mut self, to: Option<QueueItemId>, intent: Option<Run>) {
836 match (to, intent) {
837 (Some(id), Some(start)) => self.cue(id, 0, start),
838 (Some(id), None) => {
839 self.stop_playback_and_clear_state();
840 self.shared_state.set_cursor(Some(id));
841 }
842 (None, _) => self.stop_playback_and_clear_state(),
843 }
844 }
845
846 pub fn play(&mut self, id: QueueItemId) {
849 if self.in_flight.as_ref().is_some_and(|f| f.item == id) {
852 self.finish_play();
853 }
854 self.forget_waiting();
855 self.shared_state.set_cursor(Some(id));
856
857 match self.shared_state.item_playback_source(id) {
858 Some(PlaybackSource::Ready(path)) => {
859 if let Err(e) = self.start_playback(id, &path, 0, Run::Playing) {
860 log::error!("play failed: {}", e);
861 }
862 }
863 Some(PlaybackSource::Streaming {
864 path,
865 bytes_written,
866 total,
867 }) => {
868 self.park(id, 0, Run::Playing);
873 if self.renderer.is_none() {
874 self.probe_stream_for_playback(id, &path, bytes_written, total);
875 }
876 }
877 None => {
878 self.park(id, 0, Run::Playing);
879 log::info!("play: item {:?} not ready, waiting for TrackReady", id);
880 }
881 }
882 }
883
884 fn cue(&mut self, id: QueueItemId, position_ms: u64, start: Run) {
889 self.forget_waiting();
890 self.shared_state.set_cursor(Some(id));
891 let Some(PlaybackSource::Ready(path)) = self.shared_state.item_playback_source(id) else {
892 if position_ms == 0 && start == Run::Playing {
893 self.play(id);
894 return;
895 }
896 self.park(id, position_ms, start);
897 log::info!("cue: {id:?} not on disk yet, opening at {position_ms}ms once it is");
898 return;
899 };
900 if let Err(e) = self.start_playback(id, &path, position_ms, start) {
901 log::error!("cue failed: {}", e);
902 return;
903 }
904 self.report(match start {
905 Run::Playing => PlaybackReportState::Playing,
906 Run::Paused => PlaybackReportState::Paused,
907 });
908 }
909
910 fn park(&mut self, id: QueueItemId, position_ms: u64, start: Run) {
914 self.report(PlaybackReportState::Stopped);
915 self.finish_play();
916 self.stop_engine();
917 self.timeline.reset();
918 self.shared_state.set_position_ms(position_ms);
919 self.transport = Transport::Waiting(Waiting {
920 id,
921 position_ms,
922 start,
923 });
924 }
925
926 fn start_playback(
927 &mut self,
928 id: QueueItemId,
929 path: &Path,
930 seek_ms: u64,
931 start: Run,
932 ) -> Result<(), PlayerError> {
933 self.open_session(id, Source::File(path.to_path_buf()), None, seek_ms, start)
934 }
935
936 fn open_session(
944 &mut self,
945 id: QueueItemId,
946 source: Source,
947 info: Option<buffer::StreamInfo>,
948 seek_ms: u64,
949 start: Run,
950 ) -> Result<(), PlayerError> {
951 #[cfg(test)]
952 {
953 self.playback_starts += 1;
954 }
955 self.forget_waiting();
956 let mut result = self.try_open_session(id, source, info, seek_ms, start);
957 let mut skipped = id;
963 while matches!(result, Err(PlayerError::Unplayable(_)))
964 && self.shared_state.is_cursor(skipped)
965 {
966 let Some(next) = self.shared_state.advance_cursor_loadable() else {
967 log::info!("upnp: nothing further the renderer can play");
968 self.stop_playback_and_clear_state();
969 return Ok(());
970 };
971 match self.shared_state.item_playback_source(next) {
972 Some(PlaybackSource::Ready(path)) => {
973 skipped = next;
974 result = self.try_open_session(next, Source::File(path), None, 0, start);
975 }
976 _ => {
978 self.cue(next, 0, start);
979 return Ok(());
980 }
981 }
982 }
983 if result.is_err() {
984 self.stop_playback_and_clear_state();
985 }
986 self.wake_analyzer();
987 result
988 }
989
990 fn try_open_session(
991 &mut self,
992 id: QueueItemId,
993 source: Source,
994 info: Option<buffer::StreamInfo>,
995 seek_ms: u64,
996 start: Run,
997 ) -> Result<(), PlayerError> {
998 if self.renderer.is_some() {
999 return self.try_open_on_renderer(id, source, info, seek_ms, start);
1000 }
1001 self.stop_engine();
1002 let mut info = match info {
1003 Some(info) => info,
1004 None => buffer::probe_file(source.path())?,
1005 };
1006 if info.duration_ms == 0
1009 && let Some(known) = self.shared_state.get_item(id).and_then(|i| i.duration_ms)
1010 {
1011 info.duration_ms = known;
1012 }
1013 let path = source.path().to_path_buf();
1014 let streaming = matches!(source, Source::Stream(_));
1015
1016 let (first, stream) = match source {
1017 Source::File(path) => (buffer::SourceEntry::from_file(id, path), None),
1018 Source::Stream(source) => {
1019 self.stream_mode = source.mode;
1021 let live = LiveStream {
1022 feed: source.bytes_written.clone(),
1023 abandoned: Default::default(),
1024 };
1025 let status = {
1026 let downloading = self.stream_status_fn(id);
1027 let abandoned = live.abandoned.clone();
1028 Arc::new(move || {
1029 if abandoned.load(std::sync::atomic::Ordering::Acquire) {
1030 streaming::StreamStatus::Failed
1031 } else {
1032 downloading()
1033 }
1034 }) as Arc<dyn Fn() -> streaming::StreamStatus + Send + Sync>
1035 };
1036 let entry = buffer::SourceEntry {
1037 id,
1038 path: source.path.clone(),
1039 hint: hint_for(&source.path),
1040 make_mss: Box::new(move || {
1041 let partial = streaming::PartialFileSource::open(
1042 &source.path,
1043 source.bytes_written.clone(),
1044 source.total,
1045 status.clone(),
1046 source.mode,
1047 )?;
1048 Ok(symphonia::core::io::MediaSourceStream::new(
1049 Box::new(partial),
1050 Default::default(),
1051 ))
1052 }),
1053 };
1054 (entry, Some(live))
1055 }
1056 };
1057
1058 let track = TrackInfo {
1059 id,
1060 path: path.clone(),
1061 codec: info.codec.clone(),
1062 sample_rate: info.sample_rate,
1063 bit_depth: info.bit_depth,
1064 bitrate_kbps: info.bitrate_kbps,
1065 channels: info.channels,
1066 duration_ms: info.duration_ms,
1067 };
1068 self.shared_state.set_position_ms(seek_ms);
1071 self.on_track_changed(id, seek_ms);
1072 log::info!(
1073 "{}: {} ({:?}) — {} {}Hz/{}ch, {}ms{}",
1074 if streaming { "streaming" } else { "playing" },
1075 path.display(),
1076 id,
1077 info.codec,
1078 info.sample_rate,
1079 info.channels,
1080 info.duration_ms,
1081 if seek_ms > 0 {
1082 format!(" @{}ms", seek_ms)
1083 } else {
1084 String::new()
1085 }
1086 );
1087
1088 let (producer, consumer) = rtrb::RingBuffer::new(RING_BUFFER_SIZE);
1089 self.timeline.reset();
1090 let lookahead = Arc::new(parking_lot::Mutex::new(Vec::new()));
1091 let next_track = self.decode_cursor(id, lookahead.clone());
1092 let processing = self.processing();
1095 self.session += 1;
1096 let session = self.session;
1097 let finish_tx = self.commands.tx.clone();
1098 let decode_handle = buffer::start_decode(
1099 first,
1100 producer,
1101 seek_ms,
1102 move || {
1103 let (next_id, next_path) = next_track()?;
1104 Some(buffer::SourceEntry::from_file(next_id, next_path))
1105 },
1106 self.timeline.clone(),
1107 Some(self.viz_buffer.clone()),
1108 processing,
1109 move |stop| {
1110 commands::send_unless_stopped(
1111 &finish_tx,
1112 PlayerCommand::DecodeFinished(session),
1113 stop,
1114 );
1115 },
1116 )?;
1117
1118 let (engine, rate_watch) = self.create_engine_for(&info, consumer)?;
1119 let dsp = self
1121 .dsp
1122 .as_ref()
1123 .and_then(|c| c.setup.as_ref())
1124 .map(|s| s.status(info.sample_rate));
1125 if start == Run::Playing {
1128 if std::mem::take(&mut self.quick_start) {
1129 engine.fade_in_quickly()?;
1130 } else {
1131 engine.start()?;
1132 }
1133 }
1134 self.transport = Transport::Loaded(Session {
1135 track,
1136 run: start,
1137 lookahead,
1138 output: Output::Local(Local {
1139 engine,
1140 decode_handle,
1141 stream,
1142 _rate_watch: rate_watch,
1143 dsp,
1144 }),
1145 });
1146 Ok(())
1147 }
1148
1149 fn decode_cursor(
1159 &self,
1160 id: QueueItemId,
1161 steps: Arc<parking_lot::Mutex<Vec<state::Lookahead>>>,
1162 ) -> impl Fn() -> Option<(QueueItemId, PathBuf)> + Send + 'static {
1163 let state = self.shared_state.clone();
1164 let timeline = self.timeline.clone();
1165 move || {
1166 let mut steps = steps.lock();
1167 let mut step = match steps.last() {
1168 None => state.lookahead_after(id)?,
1169 Some(step) => state.lookahead_from(step)?,
1170 };
1171 step.boundary = timeline.boundary_count();
1172 let next = step.chosen.clone();
1173 steps.push(step);
1174 next
1175 }
1176 }
1177
1178 fn probe_stream_for_playback(
1193 &self,
1194 id: QueueItemId,
1195 path: &Path,
1196 bytes_written: Arc<crate::remote::downloads::ByteFeed>,
1197 total: u64,
1198 ) {
1199 let path = path.to_path_buf();
1200 let tx = self.commands.tx.clone();
1201 let hint = hint_for(&path);
1202
1203 let status = {
1208 let downloading = self.stream_status_fn(id);
1209 let state = self.shared_state.clone();
1210 Arc::new(move || {
1211 if state.is_cursor(id) {
1212 downloading()
1213 } else {
1214 streaming::StreamStatus::Failed
1215 }
1216 }) as Arc<dyn Fn() -> streaming::StreamStatus + Send + Sync>
1217 };
1218
1219 let spawned = thread::Builder::new()
1220 .name("koan-stream-probe".into())
1221 .spawn(move || {
1222 let attempt = |mode, wait: bool| {
1229 let open = if wait {
1230 streaming::PartialFileSource::open(
1231 &path,
1232 bytes_written.clone(),
1233 total,
1234 status.clone(),
1235 mode,
1236 )
1237 } else {
1238 streaming::PartialFileSource::open_for_probe(
1239 &path,
1240 bytes_written.clone(),
1241 total,
1242 status.clone(),
1243 mode,
1244 )
1245 };
1246 open.map_err(buffer::DecodeError::Io).and_then(|source| {
1247 let mss = symphonia::core::io::MediaSourceStream::new(
1248 Box::new(source),
1249 Default::default(),
1250 );
1251 buffer::probe_source(mss, &hint)
1252 })
1253 };
1254
1255 let info = match attempt(streaming::ProbeMode::Full, false) {
1259 Ok(info) => Some((info, streaming::ProbeMode::Full)),
1260 Err(e) => {
1261 log::info!(
1267 "stream probe: {} needs more than has arrived ({}), opening without a length",
1268 path.display(),
1269 e
1270 );
1271 let lengthless = lengthless_mode_for(&path);
1272 attempt(lengthless, true)
1273 .ok()
1274 .map(|info| (info, lengthless))
1275 }
1276 };
1277
1278 match info {
1279 Some((info, mode)) => {
1280 tx.send(PlayerCommand::StreamProbed {
1281 id,
1282 info: Box::new(info),
1283 mode,
1284 })
1285 .ok();
1286 }
1287 None => log::info!(
1290 "stream probe: {} cannot start early, waiting for the download",
1291 path.display()
1292 ),
1293 }
1294 });
1295
1296 if let Err(e) = spawned {
1297 log::warn!("stream probe: could not spawn for {:?}: {}", id, e);
1298 }
1299 }
1300
1301 fn stream_probed(
1304 &mut self,
1305 id: QueueItemId,
1306 info: buffer::StreamInfo,
1307 mode: streaming::ProbeMode,
1308 ) {
1309 let Some(waiting) = self.waiting().filter(|w| self.may_stream(w, id)) else {
1312 return;
1313 };
1314 if !self.shared_state.is_cursor(id) {
1315 return;
1316 }
1317
1318 match self.shared_state.item_playback_source(id) {
1319 Some(PlaybackSource::Ready(path)) => {
1321 if let Err(e) = self.start_playback(id, &path, 0, waiting.start) {
1322 log::error!("stream probe: playback failed: {}", e);
1323 }
1324 }
1325 Some(PlaybackSource::Streaming {
1326 path,
1327 bytes_written,
1328 total,
1329 }) => {
1330 let source = StreamSource {
1331 path,
1332 bytes_written,
1333 total,
1334 mode,
1335 };
1336 if let Err(e) =
1337 self.open_session(id, Source::Stream(source), Some(info), 0, waiting.start)
1338 {
1339 log::error!("stream probe: streaming playback failed: {}", e);
1340 }
1341 }
1342 None => {}
1343 }
1344 }
1345
1346 fn stream_status_fn(
1350 &self,
1351 id: QueueItemId,
1352 ) -> Arc<dyn Fn() -> streaming::StreamStatus + Send + Sync> {
1353 let state = self.shared_state.clone();
1357 Arc::new(move || match state.item_state(id) {
1358 Some(ItemState::Ready) => streaming::StreamStatus::Complete,
1359 Some(ItemState::Failed(_)) => streaming::StreamStatus::Failed,
1360 _ => streaming::StreamStatus::Downloading,
1361 })
1362 }
1363
1364 pub fn seek(&mut self, position_ms: u64) {
1371 let Some(id) = self.session().map(|s| s.track.id) else {
1372 return;
1373 };
1374 let seekable = self.shared_state.seekable_ms();
1376 if seekable == 0 {
1377 log::debug!("seek declined: {:?} is not seekable yet", id);
1381 return;
1382 }
1383 let ceiling = seekable.min(
1384 self.shared_state
1385 .duration_ms()
1386 .saturating_sub(SEEK_END_GUARD_MS),
1387 );
1388 let clamped = position_ms.min(ceiling);
1389
1390 if self.seek_on_renderer(clamped) {
1393 return;
1394 }
1395 if let Err(e) = self.restart_current(clamped) {
1396 log::error!("seek failed: {}", e);
1397 }
1398 }
1399
1400 fn restart_current(&mut self, position_ms: u64) -> Result<(), PlayerError> {
1408 let Some(session) = self.session() else {
1409 return Ok(());
1410 };
1411 let (info, start) = (session.track.clone(), session.run);
1412
1413 match self.shared_state.item_playback_source(info.id) {
1414 Some(PlaybackSource::Streaming { .. }) if self.renderer.is_some() => {
1417 self.park(info.id, position_ms, start);
1418 return Ok(());
1419 }
1420 Some(PlaybackSource::Streaming {
1421 path,
1422 bytes_written,
1423 total,
1424 }) => {
1425 let known = buffer::StreamInfo {
1429 codec: info.codec.clone(),
1430 sample_rate: info.sample_rate,
1431 channels: info.channels,
1432 bit_depth: info.bit_depth,
1433 bitrate_kbps: info.bitrate_kbps,
1434 duration_ms: info.duration_ms,
1435 };
1436 let source = StreamSource {
1437 path,
1438 bytes_written,
1439 total,
1440 mode: self.stream_mode,
1441 };
1442 self.open_session(
1443 info.id,
1444 Source::Stream(source),
1445 Some(known),
1446 position_ms,
1447 start,
1448 )?;
1449 }
1450 Some(PlaybackSource::Ready(path)) => {
1451 self.start_playback(info.id, &path, position_ms, start)?;
1452 }
1453 None => return Ok(()),
1454 }
1455
1456 self.report(match start {
1457 Run::Playing => PlaybackReportState::Playing,
1458 Run::Paused => PlaybackReportState::Paused,
1459 });
1460 Ok(())
1461 }
1462
1463 pub fn next_track(&mut self) {
1465 match self.shared_state.advance_cursor_loadable() {
1466 Some(id) => self.play(id),
1467 None => {
1468 log::info!("no more tracks in playlist");
1469 self.stop_playback_and_clear_state();
1470 }
1471 }
1472 }
1473
1474 pub fn prev_track(&mut self) {
1476 match self.shared_state.retreat_cursor() {
1477 Some((id, _)) => self.play(id),
1478 None => {
1479 if let Err(e) = self.restart_current(0) {
1481 log::error!("restart failed: {}", e);
1482 }
1483 }
1484 }
1485 }
1486
1487 pub fn pause(&mut self) {
1493 let fade = crate::config::Config::cached().playback.fade_on_pause;
1494 self.pause_with(if fade { Fade::Short } else { Fade::Cut });
1495 }
1496
1497 fn pause_with(&mut self, fade: Fade) {
1498 match &mut self.transport {
1499 Transport::Idle => return,
1500 Transport::Waiting(waiting) => {
1501 waiting.start = Run::Paused;
1502 return;
1503 }
1504 Transport::Loaded(session) => {
1505 if let Output::Local(local) = &session.output {
1506 match fade {
1507 Fade::Short => local.engine.fade_out(),
1508 Fade::Cut => {
1509 if let Err(e) = local.engine.stop() {
1510 log::error!("pause failed: {}", e);
1511 return;
1512 }
1513 }
1514 }
1515 }
1516 session.run = Run::Paused;
1517 }
1518 }
1519 self.pause_renderer();
1520 self.report(PlaybackReportState::Paused);
1521 }
1522
1523 pub fn resume(&mut self) {
1530 self.answer_silence();
1531 if std::mem::take(&mut self.sleep_snap_on_resume) {
1532 self.set_sleep_gain(1.0, true);
1533 }
1534 let session = match &mut self.transport {
1535 Transport::Idle => {
1536 if let Some(id) = self.shared_state.cursor() {
1537 self.play(id);
1538 }
1539 return;
1540 }
1541 Transport::Waiting(waiting) => {
1542 let waiting = *waiting;
1543 self.cue(waiting.id, waiting.position_ms, Run::Playing);
1544 return;
1545 }
1546 Transport::Loaded(session) => session,
1547 };
1548 let Output::Local(local) = &session.output else {
1549 self.resume_renderer();
1550 return;
1551 };
1552 let engine = &local.engine;
1553 let resumed = if engine.is_running() || engine.is_silent() {
1554 self.lead_in_ends = None;
1555 engine.fade_in()
1556 } else {
1557 engine.start()
1558 };
1559 if let Err(e) = resumed {
1560 log::error!("resume failed: {}", e);
1561 return;
1562 }
1563 session.run = Run::Playing;
1564 self.wake_analyzer();
1565 self.report(PlaybackReportState::Playing);
1566 }
1567
1568 fn wake_analyzer(&self) {
1575 self.viz_snapshot.wake();
1576 }
1577
1578 pub fn stop(&mut self) {
1580 self.shared_state.clear_playlist();
1581 self.stop_playback_and_clear_state();
1582 }
1583
1584 fn stop_engine(&mut self) {
1590 let playback = match std::mem::replace(&mut self.transport, Transport::Idle) {
1591 Transport::Loaded(playback) => playback,
1592 other => {
1593 self.transport = other;
1594 return;
1595 }
1596 };
1597 self.session += 1;
1598 self.dsp_restart = None;
1600 self.bank_listening();
1601 let local = match playback.output {
1602 Output::Local(local) => local,
1603 Output::Renderer(play) => {
1604 self.halt_renderer(*play);
1605 self.answer_silence();
1606 return;
1607 }
1608 };
1609 let Local {
1610 engine,
1611 mut decode_handle,
1612 stream,
1613 ..
1614 } = local;
1615
1616 let _ = engine.stop();
1617 decode_handle.signal_stop();
1620 if let Some(stream) = stream {
1621 stream.abandon();
1622 }
1623 decode_handle.stop();
1624 drop(engine);
1625 self.answer_silence();
1626 }
1627
1628 fn answer_silence(&mut self) {
1630 let position_ms = self.shared_state.position_ms();
1631 for reply in self.silence_waiters.drain(..) {
1632 let _ = reply.send(position_ms);
1633 }
1634 }
1635
1636 fn stop_playback_and_clear_state(&mut self) {
1638 self.forget_waiting();
1639 self.report(PlaybackReportState::Stopped);
1640 self.finish_play();
1641 self.stop_engine();
1642 self.timeline.reset();
1643 self.shared_state.set_position_ms(0);
1644 }
1645
1646 pub fn remove_from_playlist(&mut self, id: QueueItemId) {
1653 let was_cursor = self.shared_state.is_cursor(id);
1654 let resume_after = was_cursor
1655 .then(|| self.shared_state.item_before(id))
1656 .flatten();
1657 self.shared_state.remove_item(id);
1658 if was_cursor {
1659 self.resume_from(resume_after);
1660 let next = self.shared_state.advance_cursor_loadable();
1661 self.carry_on(next, self.intent());
1662 }
1663 }
1664
1665 fn resume_from(&self, before: Option<QueueItemId>) {
1670 if !self.shared_state.is_shuffled() {
1671 self.shared_state.set_cursor(before);
1672 }
1673 }
1674
1675 pub fn track_ready(&mut self, id: QueueItemId) {
1683 if !self.shared_state.is_cursor(id) {
1684 return;
1685 }
1686
1687 if let Some(waiting) = self.waiting().filter(|w| w.id == id) {
1688 log::info!("track_ready: opening {:?}", id);
1689 self.cue(id, waiting.position_ms, waiting.start);
1690 return;
1691 }
1692
1693 if self.session().is_some_and(|s| s.track.id == id) {
1694 log::info!(
1695 "track_ready: download complete while streaming {:?}, refreshing metadata",
1696 id
1697 );
1698 self.refresh_track_metadata(id);
1699 }
1700 }
1701
1702 pub fn track_stream_ready(&mut self, id: QueueItemId) {
1705 let Some(waiting) = self.waiting().filter(|w| self.may_stream(w, id)) else {
1706 return;
1707 };
1708 if !self.shared_state.is_cursor(id) {
1709 return;
1710 }
1711
1712 match self.shared_state.item_playback_source(id) {
1713 Some(PlaybackSource::Streaming {
1714 path,
1715 bytes_written,
1716 total,
1717 }) => {
1718 log::info!("track_stream_ready: probing partial file for {:?}", id);
1719 self.probe_stream_for_playback(id, &path, bytes_written, total);
1720 }
1721 Some(PlaybackSource::Ready(path)) => {
1722 log::info!(
1724 "track_stream_ready: track already ready, starting normal playback for {:?}",
1725 id
1726 );
1727 if let Err(e) = self.start_playback(id, &path, 0, waiting.start) {
1728 log::error!("track_stream_ready playback failed: {}", e);
1729 }
1730 }
1731 None => {} }
1733 }
1734
1735 fn refresh_track_metadata(&mut self, id: QueueItemId) {
1739 use crate::index::metadata;
1740
1741 let path = match self.shared_state.item_path_if_ready(id) {
1742 Some(p) => p,
1743 None => return,
1744 };
1745
1746 match metadata::read_metadata(&path) {
1747 Ok(meta) => {
1748 self.shared_state.update_item_metadata(
1749 id,
1750 meta.title,
1751 meta.artist,
1752 meta.album_artist.unwrap_or_default(),
1753 meta.album,
1754 meta.duration_ms.map(|d| d as u64),
1755 );
1756
1757 if let Transport::Loaded(session) = &mut self.transport
1766 && session.track.id == id
1767 {
1768 let current = &mut session.track;
1769 let probed = buffer::probe_file(&path).ok();
1770 let duration_ms = probed
1771 .as_ref()
1772 .map(|s| s.duration_ms)
1773 .filter(|d| *d > current.duration_ms)
1774 .unwrap_or(current.duration_ms);
1775 if duration_ms != current.duration_ms {
1776 log::info!(
1777 "track_ready: duration corrected {}ms → {}ms",
1778 current.duration_ms,
1779 duration_ms
1780 );
1781 }
1782 current.duration_ms = duration_ms;
1783 current.path = path.clone();
1784 }
1785
1786 self.shared_state.signal_metadata_refresh();
1788 log::info!("track_ready: metadata refreshed for {:?}", id);
1789 }
1790 Err(e) => {
1791 log::warn!("track_ready: metadata refresh failed for {:?}: {}", id, e);
1792 }
1793 }
1794 }
1795
1796 fn on_track_changed(&mut self, id: QueueItemId, position_ms: u64) {
1803 if let Some(f) = self.in_flight.as_mut().filter(|f| f.item == id) {
1804 f.jump(position_ms);
1805 f.boundary = 0;
1807 return;
1808 }
1809 self.begin_play(id, position_ms, 0);
1810 }
1811
1812 fn begin_play(&mut self, id: QueueItemId, position_ms: u64, boundary: usize) {
1817 self.finish_play();
1818 self.shared_state.mark_played(id);
1819 let track_id = self.shared_state.item_db_id(id);
1820 let mut flight = InFlight::new(id, track_id, position_ms);
1821 flight.boundary = boundary;
1822 self.in_flight = Some(flight);
1823 if let (Some(track_id), Some(recorder)) = (track_id, self.history.as_ref()) {
1824 recorder.record(PlayEvent::Started {
1825 track_id,
1826 position_ms,
1827 });
1828 }
1829 }
1830
1831 fn bank_listening(&mut self) {
1835 let renderer_at = self
1837 .shared_state
1838 .renderer_clock()
1839 .map(|_| self.shared_state.position_ms());
1840 if let Some(f) = self.in_flight.as_mut()
1841 && let Some(at) = self.timeline.position_in(f.boundary).or(renderer_at)
1842 {
1843 f.advance(at);
1844 }
1845 }
1846
1847 fn report(&self, state: PlaybackReportState) {
1851 let Some(track_id) = self.in_flight.as_ref().and_then(InFlight::track_id) else {
1852 return;
1853 };
1854 if let Some(recorder) = self.history.as_ref() {
1855 recorder.record(PlayEvent::Playback(PlaybackReport {
1856 track_id,
1857 state,
1858 position_ms: self.shared_state.position_ms(),
1859 }));
1860 }
1861 }
1862
1863 fn finish_play(&mut self) -> Option<PlayEvent> {
1866 self.bank_listening();
1867 let flight = self.in_flight.take()?;
1868 let event = PlayEvent::Finished {
1869 track_id: flight.track_id()?,
1870 listened_ms: flight.listened_ms(),
1871 };
1872 if let Some(recorder) = self.history.as_ref() {
1873 recorder.record(event);
1874 }
1875 Some(event)
1876 }
1877
1878 pub fn update_playback_state(&mut self) {
1882 self.sleep_tick();
1883 if let Some(SleepSet { at: Some(at), .. }) = self.sleep
1884 && std::time::Instant::now() >= at
1885 {
1886 self.sleep = None;
1887 log::info!("sleep timer: time's up");
1888 if self.intent() == Some(Run::Playing) {
1890 self.pause_with(Fade::Cut);
1891 }
1892 self.end_sleep_fade();
1893 }
1894
1895 if self.session().is_some()
1896 && self
1897 .lead_in_ends
1898 .is_some_and(|end| std::time::Instant::now() >= end)
1899 {
1900 self.lead_in_ends = None;
1904 self.shared_state.changed();
1905 }
1906
1907 if let Some(session) = self.session()
1908 && session.run == Run::Paused
1909 && let Some(engine) = session.engine()
1910 && engine.is_running()
1911 && engine.is_silent()
1912 {
1913 if let Err(e) = engine.stop() {
1914 log::error!("stopping after fade failed: {}", e);
1915 }
1916 self.answer_silence();
1917 }
1918
1919 if let Some(since) = self.dsp_restart {
1920 let faded = self
1921 .session()
1922 .and_then(Session::engine)
1923 .is_none_or(|e| e.is_silent());
1924 if faded || since.elapsed() >= QUICK_FADE_LIMIT {
1925 self.dsp_restart = None;
1926 self.quick_start = true;
1927 self.restart_on_current_track();
1928 self.quick_start = false;
1929 }
1930 }
1931
1932 self.release_idle_session();
1933 self.follow_playhead();
1934 self.renderer_tick();
1935 self.publish();
1936 }
1937
1938 fn follow_playhead(&mut self) {
1947 let before = self.in_flight.as_ref().map(|f| (f.item, f.boundary));
1948 self.move_with_playhead();
1949 let after = self.in_flight.as_ref().map(|f| (f.item, f.boundary));
1950 if let (Some((ended, _)), Some((next, _))) = (before, after)
1951 && before != after
1952 && self.sleeps_between(Some(ended), Some(next))
1953 {
1954 self.pause_with(Fade::Cut);
1955 self.end_sleep_fade();
1956 }
1957 }
1958
1959 fn move_with_playhead(&mut self) {
1960 if self.session().is_none() {
1961 return;
1962 }
1963 let Some(playhead) = self.timeline.playhead() else {
1964 return;
1965 };
1966 let id = playhead.id;
1967 if self
1968 .in_flight
1969 .as_ref()
1970 .is_none_or(|f| f.item != id || f.boundary != playhead.boundary)
1971 {
1972 self.begin_play(id, playhead.position_ms, playhead.boundary);
1973 }
1974 let Transport::Loaded(session) = &mut self.transport else {
1975 return;
1976 };
1977 let pass = step_pass(&session.lookahead.lock(), playhead.boundary);
1978 if session.track.id == id {
1979 return;
1980 }
1981 let Some((id, path, info, _)) = self.timeline.current_playback() else {
1982 return;
1983 };
1984 log::info!("timeline: now playing {:?}", id);
1985 session.track = TrackInfo {
1986 id,
1987 path,
1988 codec: info.codec,
1989 sample_rate: info.sample_rate,
1990 bit_depth: info.bit_depth,
1991 bitrate_kbps: info.bitrate_kbps,
1992 channels: info.channels,
1993 duration_ms: info.duration_ms,
1994 };
1995 self.shared_state.move_on_to(id, pass);
1996 }
1997
1998 fn sleeps_between(&mut self, ended: Option<QueueItemId>, next: Option<QueueItemId>) -> bool {
2002 let record = |id: Option<QueueItemId>| {
2003 id.and_then(|id| self.shared_state.get_item(id))
2004 .map(|i| (i.album, i.album_artist))
2005 };
2006 let due = match self.sleep.map(|s| s.sleep) {
2007 Some(Sleep::EndOfTrack) => true,
2008 Some(Sleep::EndOfRecord) => next.is_none() || record(ended) != record(next),
2009 _ => false,
2010 };
2011 if due {
2012 log::info!(
2013 "sleep timer: stopping at the end of the {}",
2014 match self.sleep {
2015 Some(SleepSet {
2016 sleep: Sleep::EndOfTrack,
2017 ..
2018 }) => "track",
2019 _ => "record",
2020 }
2021 );
2022 self.sleep = None;
2023 }
2024 due
2025 }
2026
2027 fn set_sleep_timer(&mut self, timer: Option<SleepTimer>) {
2029 self.restore_sleep_fade();
2030 self.sleep = timer.map(|timer| match timer {
2031 SleepTimer::After { minutes } => {
2032 let after = std::time::Duration::from_secs(u64::from(minutes) * 60);
2033 let unix_ms = std::time::SystemTime::now()
2034 .duration_since(std::time::UNIX_EPOCH)
2035 .unwrap_or_default()
2036 + after;
2037 SleepSet {
2038 sleep: Sleep::At {
2039 unix_ms: unix_ms.as_millis() as u64,
2040 },
2041 at: Some(std::time::Instant::now() + after),
2042 fade: Some(sleep::fade_length(minutes)),
2043 }
2044 }
2045 SleepTimer::EndOfTrack => SleepSet {
2046 sleep: Sleep::EndOfTrack,
2047 at: None,
2048 fade: None,
2049 },
2050 SleepTimer::EndOfRecord => SleepSet {
2051 sleep: Sleep::EndOfRecord,
2052 at: None,
2053 fade: None,
2054 },
2055 });
2056 log::info!("sleep timer: {:?}", self.sleep.map(|s| s.sleep));
2057 }
2058
2059 pub fn track_failed(&mut self, id: QueueItemId) {
2067 let Some(waiting) = self.waiting().filter(|w| w.id == id) else {
2068 return;
2069 };
2070 if !self.shared_state.is_cursor(id) {
2071 return;
2072 }
2073 log::info!("track {:?} cannot load, moving on", id);
2074 let next = self.shared_state.advance_cursor_loadable();
2075 self.carry_on(next, Some(waiting.start));
2076 }
2077
2078 fn on_decode_finished(&mut self, session: u64) {
2084 if session != self.session || self.renderer_stream_finished() {
2085 return;
2086 }
2087 log::info!("decode finished, checking for next track");
2088 self.track_ended();
2089 }
2090
2091 fn track_ended(&mut self) {
2105 self.bank_listening();
2106 let ended = self.in_flight.as_ref().map(|f| f.item);
2107 let heard = self.in_flight.as_ref().is_some_and(|f| f.listened_ms() > 0);
2108 if !heard
2114 && let Some(id) = self.session().map(|s| s.track.id)
2115 && self.shared_state.is_cursor(id)
2116 && matches!(self.shared_state.item_state(id), Some(ItemState::Pending))
2117 {
2118 log::info!("track {id:?} ended unheard before its download did; waiting for it");
2119 let start = self.intent().unwrap_or(Run::Playing);
2120 self.park(id, 0, start);
2121 return;
2122 }
2123 if self.mode.repeat != Repeat::Off && !heard {
2124 log::info!("track ended with nothing heard; not repeating it");
2125 self.stop_playback_and_clear_state();
2126 return;
2127 }
2128 let again = (self.mode.repeat == Repeat::One)
2129 .then(|| self.shared_state.cursor())
2130 .flatten()
2131 .filter(|id| {
2132 self.shared_state
2133 .item_state(*id)
2134 .is_some_and(|s| !matches!(s, ItemState::Failed(_)))
2135 });
2136 let next = again.or_else(|| self.shared_state.advance_cursor_loadable());
2137 if next.is_some() && next == ended {
2138 self.finish_play();
2139 }
2140 let sleeps = self.intent().is_some() && self.sleeps_between(ended, next);
2141 let intent = if sleeps {
2142 Some(Run::Paused)
2143 } else {
2144 self.intent()
2145 };
2146 self.carry_on(next, intent);
2147 if sleeps {
2148 self.end_sleep_fade();
2149 }
2150 }
2151
2152 fn revoke_stale_lookahead(&mut self) {
2162 if self.session().is_none() {
2163 return;
2164 }
2165 self.update_playback_state();
2166 let Some(playhead) = self.timeline.playhead() else {
2167 return;
2168 };
2169 let position_ms = playhead.position_ms;
2170 let Some(session) = self.session().filter(|s| s.track.id == playhead.id) else {
2171 return;
2172 };
2173 let stale = {
2174 let steps = session.lookahead.lock();
2175 steps
2179 .iter()
2180 .filter(|step| step.boundary > playhead.boundary)
2181 .any(|step| !self.shared_state.still_follows(step))
2182 };
2183 if !stale {
2184 return;
2185 }
2186 log::info!("queue changed under the lookahead, restarting at {position_ms}ms");
2187 if let Err(e) = self.restart_current(position_ms) {
2188 log::error!("restart after a queue edit failed: {}", e);
2189 }
2190 }
2191
2192 fn snapshot_for_undo(
2196 &self,
2197 ids: &[QueueItemId],
2198 ) -> Vec<(Box<state::PlaylistItem>, Option<QueueItemId>)> {
2199 self.shared_state
2200 .items_before(ids)
2201 .into_iter()
2202 .filter_map(|(id, after)| Some((Box::new(self.shared_state.get_item(id)?), after)))
2203 .collect()
2204 }
2205
2206 fn push_undo(&mut self, entry: UndoEntry) {
2208 if let Some(ref mut batch) = self.batch_buffer {
2209 batch.push(entry);
2210 } else {
2211 self.undo_stack.push(entry);
2212 }
2213 }
2214
2215 pub fn process_command(&mut self, cmd: PlayerCommand) {
2217 if cmd.asks_to_play() && !matches!(cmd, PlayerCommand::Cue { .. })
2224 || matches!(
2225 cmd,
2226 PlayerCommand::UseRenderer(_)
2227 | PlayerCommand::SetOutputDevice(_)
2228 | PlayerCommand::ClearOutputDevice
2229 | PlayerCommand::Pause
2230 | PlayerCommand::PauseAndReport(_)
2231 | PlayerCommand::Stop
2232 | PlayerCommand::ClearPlaylist
2233 | PlayerCommand::ReplacePlaylist { .. }
2234 )
2235 {
2236 self.resume_renderer = false;
2237 self.held_run = None;
2238 }
2239 let cmd = match cmd {
2242 PlayerCommand::Cue {
2243 id,
2244 position_ms,
2245 play: true,
2246 } if self.resume_renderer => {
2247 self.held_run = Some(Run::Playing);
2248 PlayerCommand::Cue {
2249 id,
2250 position_ms,
2251 play: false,
2252 }
2253 }
2254 cmd => cmd,
2255 };
2256 let edits_queue = matches!(
2257 cmd,
2258 PlayerCommand::AddToPlaylist(_)
2259 | PlayerCommand::InsertInPlaylist { .. }
2260 | PlayerCommand::RemoveFromPlaylist(_)
2261 | PlayerCommand::RemoveFromPlaylistBatch(_)
2262 | PlayerCommand::MoveInPlaylist { .. }
2263 | PlayerCommand::MoveItemsInPlaylist { .. }
2264 | PlayerCommand::ReorderPlaylist(_)
2265 | PlayerCommand::Undo
2266 | PlayerCommand::Redo
2267 | PlayerCommand::SetShuffle(_)
2268 | PlayerCommand::SetRepeat(_)
2269 | PlayerCommand::RestorePlayMode(_)
2270 );
2271 self.apply_command(cmd);
2272 if edits_queue {
2273 self.revoke_stale_lookahead();
2274 }
2275 self.publish();
2276 }
2277
2278 fn apply_command(&mut self, cmd: PlayerCommand) {
2279 match cmd {
2280 PlayerCommand::Play(id) => {
2281 self.shared_state.pick(id);
2282 self.play(id);
2283 }
2284 PlayerCommand::Cue {
2285 id,
2286 position_ms,
2287 play,
2288 } => self.cue(
2289 id,
2290 position_ms,
2291 if play { Run::Playing } else { Run::Paused },
2292 ),
2293 PlayerCommand::Pause => {
2294 self.paused_in_sleep_fade();
2295 self.pause();
2296 }
2297 PlayerCommand::Barrier(reply) => {
2300 let _ = reply.send(());
2301 }
2302 PlayerCommand::PauseAndReport(reply) => {
2303 self.pause();
2304 self.silence_waiters.push(reply);
2305 if self
2306 .session()
2307 .is_none_or(|p| p.engine().is_none_or(|e| !e.is_running()))
2308 {
2309 self.answer_silence();
2310 }
2311 }
2312 PlayerCommand::Resume => self.resume(),
2313 PlayerCommand::Stop => self.stop(),
2314 PlayerCommand::Seek(pos) => self.seek(pos),
2315 PlayerCommand::NextTrack => self.next_track(),
2316 PlayerCommand::PrevTrack => self.prev_track(),
2317 PlayerCommand::AddToPlaylist(items) => {
2318 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
2319 self.shared_state.add_items(items);
2320 self.push_undo(UndoEntry::Added { ids });
2321 }
2322 PlayerCommand::UpdatePaths(updates) => {
2323 self.shared_state.update_paths(&updates);
2324 if let Transport::Loaded(session) = &mut self.transport
2325 && let Some((_, new_path)) =
2326 updates.iter().find(|(id, _)| *id == session.track.id)
2327 {
2328 session.track.path = new_path.clone();
2329 }
2330 }
2331 PlayerCommand::InsertInPlaylist { items, after } => {
2332 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
2333 self.shared_state.insert_items_after(items, after);
2334 self.push_undo(UndoEntry::Inserted { ids });
2335 }
2336 PlayerCommand::ClearPlaylist => self.clear_playlist(),
2337 PlayerCommand::ReplacePlaylist {
2338 items,
2339 start,
2340 position_ms,
2341 play,
2342 mode,
2343 } => {
2344 if items.is_empty() {
2345 self.clear_playlist();
2346 return;
2347 }
2348 let start_id = items.get(start).unwrap_or(&items[0]).id;
2349 self.stop_playback_and_clear_state();
2352 if let Some(mode) = mode.play_mode() {
2353 self.mode = mode;
2354 self.shared_state.set_shuffled(mode.shuffle);
2355 }
2356 let (old_items, cursor) = self.shared_state.replace_playlist(items);
2357 let start_id = match mode {
2358 QueueMode::Shuffled => self
2359 .shared_state
2360 .advance_cursor_loadable()
2361 .unwrap_or(start_id),
2362 _ => start_id,
2363 };
2364 self.push_undo(UndoEntry::Replaced {
2365 items: old_items,
2366 cursor,
2367 });
2368 self.cue(
2369 start_id,
2370 position_ms,
2371 if play { Run::Playing } else { Run::Paused },
2372 );
2373 }
2374 PlayerCommand::RemoveFromPlaylist(id) => {
2375 let item = self.shared_state.get_item(id);
2376 let after = self.shared_state.item_before(id);
2377 self.remove_from_playlist(id);
2378 if let Some(item) = item {
2379 self.push_undo(UndoEntry::Removed {
2380 items: vec![(Box::new(item), after)],
2381 });
2382 }
2383 }
2384 PlayerCommand::RemoveFromPlaylistBatch(ids) => {
2385 let items_with_pos = self.snapshot_for_undo(&ids);
2389 let intent = self.intent();
2390 let resume_after = match self.shared_state.cursor() {
2391 Some(cursor) if ids.contains(&cursor) => {
2392 Some(self.shared_state.surviving_item_before(cursor, &ids))
2393 }
2394 _ => None,
2395 };
2396
2397 self.shared_state.remove_items(&ids);
2398
2399 if let Some(resume_after) = resume_after {
2400 self.resume_from(resume_after);
2401 let next = self.shared_state.advance_cursor_loadable();
2402 self.carry_on(next, intent);
2403 }
2404
2405 if !items_with_pos.is_empty() {
2406 self.push_undo(UndoEntry::Removed {
2407 items: items_with_pos,
2408 });
2409 }
2410 }
2411 PlayerCommand::MoveInPlaylist { id, target, after } => {
2412 let was_after = self.shared_state.item_before(id);
2413 self.shared_state.move_item(id, target, after);
2414 self.push_undo(UndoEntry::Moved { id, was_after });
2415 }
2416 PlayerCommand::MoveItemsInPlaylist { ids, target, after } => {
2417 let entries = self.shared_state.items_before(&ids);
2418 self.shared_state.move_items(&ids, target, after);
2419 self.push_undo(UndoEntry::MovedBatch { entries });
2420 }
2421 PlayerCommand::ReorderPlaylist(order) => {
2422 let entries = self.shared_state.items_before(&order);
2426 self.shared_state.reorder_to(&order);
2427 self.push_undo(UndoEntry::MovedBatch { entries });
2428 }
2429 PlayerCommand::TrackReady(id) => self.track_ready(id),
2430 PlayerCommand::DecodeFinished(session) => self.on_decode_finished(session),
2431 PlayerCommand::TrackQueued => {}
2434 PlayerCommand::TrackStreamReady(id) => self.track_stream_ready(id),
2435 PlayerCommand::StreamProbed { id, info, mode } => self.stream_probed(id, *info, mode),
2436 PlayerCommand::TrackFailed(id) => self.track_failed(id),
2437 PlayerCommand::CacheTracks(ids) => {
2438 if let Some(downloads) = &self.downloads {
2439 downloads.cache(ids);
2440 }
2441 }
2442 PlayerCommand::Undo => self.execute_undo(),
2443 PlayerCommand::Redo => self.execute_redo(),
2444 PlayerCommand::BeginUndoBatch => {
2445 self.batch_buffer = Some(Vec::new());
2446 }
2447 PlayerCommand::EndUndoBatch => {
2448 if let Some(entries) = self.batch_buffer.take() {
2449 if entries.len() == 1 {
2450 self.undo_stack.push(entries.into_iter().next().unwrap());
2452 } else if !entries.is_empty() {
2453 self.undo_stack.push(UndoEntry::Batch(entries));
2454 }
2455 }
2456 }
2457 PlayerCommand::SetOutputDevice(name) => self.set_output_device(name),
2458 PlayerCommand::RestartOutput => {
2459 log::info!("restarting audio output");
2460 self.restart_on_current_track();
2461 }
2462 PlayerCommand::ClearOutputDevice => self.clear_output_device(),
2463 PlayerCommand::ReloadDsp => self.reload_dsp(),
2464 PlayerCommand::RouteChanged => {
2465 crate::audio::follow_route();
2466 self.reload_dsp();
2467 }
2468 PlayerCommand::UseRenderer(connection) => {
2469 self.remember_renderer(connection.as_deref());
2470 self.use_renderer(connection);
2471 }
2472 PlayerCommand::ResumeRenderer(connection) => {
2473 if std::mem::take(&mut self.resume_renderer) && self.renderer.is_none() {
2474 log::info!(
2475 "upnp: back to {}, as last time",
2476 connection.session.renderer().name
2477 );
2478 self.use_renderer(Some(connection));
2479 if self.held_run.take() == Some(Run::Playing) {
2480 self.resume();
2481 }
2482 } else {
2483 log::info!(
2484 "upnp: not going back to {}: playback or the output moved first",
2485 connection.session.renderer().name
2486 );
2487 }
2488 }
2489 PlayerCommand::ResumeRendererMissed => {
2490 if std::mem::take(&mut self.resume_renderer)
2491 && self.held_run.take() == Some(Run::Playing)
2492 {
2493 log::info!("upnp: playing here what was held for the renderer");
2494 self.resume();
2495 }
2496 }
2497 PlayerCommand::ReleaseRenderer(reply) => {
2498 self.release_renderer();
2499 let _ = reply.send(());
2500 }
2501 PlayerCommand::SetRendererVolume(volume) => self.set_renderer_volume(volume),
2502 PlayerCommand::Renderer { session, event } => self.on_renderer_event(session, event),
2503 PlayerCommand::SetShuffle(on) => self.set_shuffle(on),
2504 PlayerCommand::SetRepeat(repeat) => self.mode.repeat = repeat,
2505 PlayerCommand::RestorePlayMode(mode) => {
2506 self.mode = mode;
2507 self.shared_state.set_shuffled(mode.shuffle);
2508 }
2509 PlayerCommand::SetSleepTimer(timer) => self.set_sleep_timer(timer),
2510 }
2511 }
2512
2513 fn set_shuffle(&mut self, on: bool) {
2518 self.mode.shuffle = on;
2519 self.shared_state.set_shuffled(on);
2520 }
2521
2522 fn clear_playlist(&mut self) {
2526 self.stop_playback_and_clear_state();
2527 let (items, cursor) = self.shared_state.snapshot_playlist();
2528 self.shared_state.clear_playlist();
2529 self.push_undo(UndoEntry::Replaced { items, cursor });
2530 }
2531
2532 fn apply_entry(&mut self, entry: UndoEntry) -> UndoEntry {
2534 match entry {
2535 UndoEntry::Added { ids } | UndoEntry::Inserted { ids } => {
2536 let items_with_pos = self.snapshot_for_undo(&ids);
2538 self.shared_state.remove_items(&ids);
2539 UndoEntry::Removed {
2540 items: items_with_pos,
2541 }
2542 }
2543 UndoEntry::Removed { items } => {
2544 let mut ids = Vec::with_capacity(items.len());
2546 for (item, after) in items {
2547 ids.push(item.id);
2548 self.shared_state.insert_item_at(*item, after);
2549 }
2550 UndoEntry::Added { ids }
2551 }
2552 UndoEntry::Moved { id, was_after } => {
2553 let current_after = self.shared_state.item_before(id);
2554 self.shared_state.move_item_to(id, was_after);
2555 UndoEntry::Moved {
2556 id,
2557 was_after: current_after,
2558 }
2559 }
2560 UndoEntry::MovedBatch { entries } => {
2561 let ids: Vec<QueueItemId> = entries.iter().map(|(id, _)| *id).collect();
2562 let current_positions = self.shared_state.items_before(&ids);
2563 self.shared_state.move_items_to(&entries);
2564 UndoEntry::MovedBatch {
2565 entries: current_positions,
2566 }
2567 }
2568 UndoEntry::Replaced { items, cursor } => {
2569 let (current_items, current_cursor) = self.shared_state.snapshot_playlist();
2570 self.shared_state.restore_playlist(items, cursor);
2571 UndoEntry::Replaced {
2572 items: current_items,
2573 cursor: current_cursor,
2574 }
2575 }
2576 UndoEntry::Batch(entries) => {
2577 let mut inverses: Vec<_> = entries
2579 .into_iter()
2580 .rev()
2581 .map(|e| self.apply_entry(e))
2582 .collect();
2583 inverses.reverse();
2584 UndoEntry::Batch(inverses)
2585 }
2586 }
2587 }
2588
2589 fn reconcile_playback(&mut self) {
2608 let orphaned = match &self.transport {
2609 Transport::Idle => false,
2610 Transport::Waiting(waiting) => self.shared_state.get_item(waiting.id).is_none(),
2611 Transport::Loaded(session) => self.shared_state.get_item(session.track.id).is_none(),
2612 };
2613 if !orphaned {
2614 return;
2615 }
2616 let cursor = self.shared_state.cursor();
2620 self.carry_on(cursor, self.intent());
2621 }
2622
2623 fn execute_undo(&mut self) {
2625 let Some(entry) = self.undo_stack.pop_undo() else {
2626 return;
2627 };
2628 let inverse = self.apply_entry(entry);
2629 self.undo_stack.push_redo(inverse);
2630 self.reconcile_playback();
2631 }
2632
2633 fn execute_redo(&mut self) {
2635 let Some(entry) = self.undo_stack.pop_redo() else {
2636 return;
2637 };
2638 let inverse = self.apply_entry(entry);
2639 self.undo_stack.push_undo_keep_redo(inverse);
2640 self.reconcile_playback();
2641 }
2642
2643 pub fn run(&mut self) {
2649 use crossbeam_channel::RecvTimeoutError;
2650
2651 let rx = self.commands.rx.clone();
2652 loop {
2653 let received = match self.next_wake() {
2654 Some(at) => rx.recv_deadline(at),
2655 None => rx.recv().map_err(|_| RecvTimeoutError::Disconnected),
2656 };
2657 match received {
2658 Ok(cmd) => self.process_command(cmd),
2659 Err(RecvTimeoutError::Timeout) => {}
2660 Err(RecvTimeoutError::Disconnected) => break,
2661 }
2662 self.update_playback_state();
2663 }
2664 self.stop();
2665 }
2666
2667 fn release_idle_session(&mut self) {
2672 if self.session_release_due(
2673 self.wants_output(),
2674 crate::audio::session_held(),
2675 std::time::Instant::now(),
2676 ) {
2677 log::info!("output idle: releasing the audio session");
2678 crate::audio::release_session();
2679 }
2680 }
2681
2682 fn wants_output(&self) -> bool {
2684 self.intent() == Some(Run::Playing)
2685 || self
2686 .session()
2687 .and_then(Session::engine)
2688 .is_some_and(|e| e.is_running())
2689 }
2690
2691 fn session_release_due(&mut self, playing: bool, held: bool, now: std::time::Instant) -> bool {
2694 if playing || !held {
2695 self.session_release_at = None;
2696 return false;
2697 }
2698 match self.session_release_at {
2699 None => {
2700 self.session_release_at = Some(now + SESSION_GRACE);
2701 false
2702 }
2703 Some(at) if now >= at => {
2704 self.session_release_at = None;
2705 true
2706 }
2707 Some(_) => false,
2708 }
2709 }
2710
2711 fn next_wake(&self) -> Option<std::time::Instant> {
2715 [
2716 self.next_event(),
2717 self.sleep.and_then(|s| s.at),
2718 self.sleep_wake(),
2719 self.session_release_at,
2720 ]
2721 .into_iter()
2722 .flatten()
2723 .min()
2724 }
2725
2726 fn next_event(&self) -> Option<std::time::Instant> {
2729 let session = self.session()?;
2730 let now = std::time::Instant::now();
2731 let Output::Local(local) = &session.output else {
2732 let next_track = (session.run == Run::Playing && self.streaming_to_renderer())
2734 .then(|| self.timeline.until_next_track())
2735 .flatten()
2736 .map(|left| now + left + BOUNDARY_SLACK);
2737 return match (next_track, self.renderer_deadline()) {
2738 (Some(a), Some(b)) => Some(a.min(b)),
2739 (a, b) => a.or(b),
2740 };
2741 };
2742 if let Some(since) = self.dsp_restart {
2743 return Some(dsp_wake(since, local.engine.period(), now));
2744 }
2745 match session.run {
2746 Run::Playing => {
2747 let next_track = self
2748 .timeline
2749 .until_next_track()
2750 .map(|left| now + left + BOUNDARY_SLACK);
2751 match (next_track, self.lead_in_ends) {
2752 (Some(a), Some(b)) => Some(a.min(b)),
2753 (a, b) => a.or(b),
2754 }
2755 }
2756 Run::Paused if local.engine.is_running() => Some(now + FADE_CHECK),
2757 Run::Paused => None,
2758 }
2759 }
2760
2761 fn remember_renderer(&self, connection: Option<&crate::upnp::Connection>) {
2767 let renderer = connection.map(|c| c.session.renderer());
2768 if let Err(e) = crate::config::Config::persist(|cfg| {
2769 cfg.playback.renderer = renderer.map(|r| r.udn.clone());
2770 cfg.playback.renderer_name = renderer.map(|r| r.name.clone());
2771 }) {
2772 log::error!("failed to save the output: {e}");
2773 }
2774 }
2775
2776 pub fn spawn() -> (
2779 Arc<SharedPlayerState>,
2780 Arc<PlaybackTimeline>,
2781 Arc<VizSnapshot>,
2782 crossbeam_channel::Sender<PlayerCommand>,
2783 ) {
2784 Self::spawn_with(false)
2785 }
2786
2787 pub fn spawn_for_listening() -> (
2793 Arc<SharedPlayerState>,
2794 Arc<PlaybackTimeline>,
2795 Arc<VizSnapshot>,
2796 crossbeam_channel::Sender<PlayerCommand>,
2797 ) {
2798 Self::spawn_with(true)
2799 }
2800
2801 fn renderer_to_resume(listening: bool) -> Option<String> {
2804 listening
2805 .then(|| crate::config::Config::cached().playback.renderer.clone())
2806 .flatten()
2807 }
2808
2809 fn spawn_with(
2810 listening: bool,
2811 ) -> (
2812 Arc<SharedPlayerState>,
2813 Arc<PlaybackTimeline>,
2814 Arc<VizSnapshot>,
2815 crossbeam_channel::Sender<PlayerCommand>,
2816 ) {
2817 if let Err(e) = crate::audio::dsp::profiles::migrate() {
2820 log::warn!("dsp: not brought up to date: {e}");
2821 }
2822 let mut player = Self::new();
2823 player.history = PlayRecorder::spawn();
2824 let state = player.shared_state();
2825 let timeline = player.timeline();
2826 let viz_snapshot = player.viz_snapshot();
2827 let tx = player.command_sender();
2828 player.downloads = Some(crate::remote::queue::DownloadQueue::spawn(
2831 tx.clone(),
2832 state.clone(),
2833 ));
2834
2835 if let Some(udn) = Self::renderer_to_resume(listening) {
2836 player.resume_renderer = true;
2837 crate::upnp::resume(udn, &tx);
2838 }
2839
2840 thread::Builder::new()
2841 .name("koan-player".into())
2842 .spawn(move || player.run())
2843 .expect("failed to spawn player thread");
2844
2845 (state, timeline, viz_snapshot, tx)
2846 }
2847}
2848
2849fn step_pass(steps: &[state::Lookahead], boundary: usize) -> Option<u64> {
2854 steps
2855 .iter()
2856 .rfind(|step| step.boundary == boundary)
2857 .map(|step| step.pass)
2858}
2859
2860#[cfg(test)]
2861mod tests {
2862 #[test]
2865 fn a_headless_player_does_not_go_back_to_a_renderer() {
2866 let _guard = crate::config::tests::PERSIST_LOCK
2867 .lock()
2868 .unwrap_or_else(|e| e.into_inner());
2869 let dir = tempfile::tempdir().unwrap();
2870 crate::config::set_config_dir(dir.path());
2871 crate::config::Config::persist(|cfg| {
2872 cfg.playback.renderer = Some("uuid:headless-test".into());
2873 })
2874 .unwrap();
2875 assert_eq!(Player::renderer_to_resume(false), None);
2876 }
2877
2878 #[test]
2879 fn a_download_in_progress_is_known_by_its_own_extension() {
2880 use std::path::Path;
2881 assert_eq!(
2883 lengthless_mode_for(Path::new("/c/t.m4a.part")),
2884 streaming::ProbeMode::LengthlessWholeEnd
2885 );
2886 assert_eq!(
2887 lengthless_mode_for(Path::new("/c/t.OPUS.part")),
2888 streaming::ProbeMode::LengthlessWholeEnd
2889 );
2890 assert_eq!(
2891 lengthless_mode_for(Path::new("/c/t.flac.part")),
2892 streaming::ProbeMode::Lengthless
2893 );
2894 assert_eq!(
2895 media_extension(Path::new("/c/t.m4a.part")).as_deref(),
2896 Some("m4a")
2897 );
2898 assert_eq!(
2899 media_extension(Path::new("/c/t.mp3")).as_deref(),
2900 Some("mp3")
2901 );
2902 }
2903
2904 use super::*;
2905 use state::PlaylistItem;
2906 use std::path::PathBuf;
2907 use std::sync::atomic::{AtomicU64, Ordering};
2908
2909 fn make_item(title: &str) -> PlaylistItem {
2910 PlaylistItem {
2911 playlist_entry_id: None,
2912 id: QueueItemId::new(),
2913 db_id: None,
2914 path: PathBuf::from(format!("/music/{title}.flac")),
2915 title: title.to_string(),
2916 artist: String::new(),
2917 album_artist: String::new(),
2918 album: String::new(),
2919 year: None,
2920 codec: None,
2921 track_number: None,
2922 disc: None,
2923 duration_ms: None,
2924 state: ItemState::Ready,
2925 played: false,
2926 }
2927 }
2928
2929 pub(super) fn playlist_ids(player: &Player) -> Vec<QueueItemId> {
2930 let (items, _) = player.shared_state.snapshot_playlist();
2931 items.iter().map(|i| i.id).collect()
2932 }
2933
2934 fn playlist_titles(player: &Player) -> Vec<String> {
2935 let (items, _) = player.shared_state.snapshot_playlist();
2936 items.iter().map(|i| i.title.clone()).collect()
2937 }
2938
2939 fn pending_item(title: &str) -> PlaylistItem {
2940 PlaylistItem {
2941 playlist_entry_id: None,
2942 state: ItemState::Pending,
2943 ..make_item(title)
2944 }
2945 }
2946
2947 fn test_session(id: QueueItemId, engine: Box<dyn AudioEngineHandle>) -> Session {
2949 Session {
2950 track: TrackInfo {
2951 id,
2952 path: PathBuf::from("/music/t.flac"),
2953 codec: String::new(),
2954 sample_rate: 44_100,
2955 bit_depth: None,
2956 bitrate_kbps: None,
2957 channels: 2,
2958 duration_ms: 1_000,
2959 },
2960 run: Run::Playing,
2961 lookahead: Default::default(),
2962 output: Output::Local(Local {
2963 engine,
2964 decode_handle: buffer::DecodeHandle::new_for_test(Default::default()),
2965 stream: None,
2966 _rate_watch: None,
2967 dsp: None,
2968 }),
2969 }
2970 }
2971
2972 fn pretend_playing(player: &mut Player, id: QueueItemId) {
2976 assert!(
2977 player.shared_state.get_item(id).is_some(),
2978 "item is in the queue"
2979 );
2980 player.stop_engine();
2981 player.transport = Transport::Loaded(test_session(
2982 id,
2983 Box::new(NullEngine {
2984 starts: Default::default(),
2985 running: Default::default(),
2986 lead_in: Default::default(),
2987 }),
2988 ));
2989 player.shared_state.set_cursor(Some(id));
2990 player.publish();
2991 }
2992
2993 fn playing_id(player: &Player) -> Option<QueueItemId> {
2994 player.shared_state.track_info().map(|t| t.id)
2995 }
2996
2997 fn seed(player: &mut Player, n: usize) -> Vec<QueueItemId> {
2999 let items: Vec<_> = (0..n).map(|i| make_item(&format!("t{i}"))).collect();
3000 let ids = items.iter().map(|i| i.id).collect();
3001 player.process_command(PlayerCommand::AddToPlaylist(items));
3002 ids
3003 }
3004
3005 fn listen(player: &mut Player, from_ms: u64, to_ms: u64) {
3009 let mut at = from_ms;
3010 if let Some(f) = player.in_flight.as_mut() {
3011 f.advance(at); }
3013 while at < to_ms {
3014 at = (at + 50).min(to_ms);
3015 if let Some(f) = player.in_flight.as_mut() {
3016 f.advance(at);
3017 }
3018 }
3019 }
3020
3021 fn start(player: &mut Player, track_id: i64) -> QueueItemId {
3022 let id = QueueItemId::new();
3023 player.on_track_changed(id, 0);
3024 player
3026 .in_flight
3027 .as_mut()
3028 .unwrap()
3029 .track_id_for_test(track_id);
3030 id
3031 }
3032
3033 #[test]
3034 fn a_gapless_transition_closes_the_outgoing_track_and_opens_the_next() {
3035 let mut player = Player::new();
3036 start(&mut player, 11);
3037 listen(&mut player, 0, 200_000);
3038
3039 let b = QueueItemId::new();
3040 player.on_track_changed(b, 0);
3041 let f = player
3042 .in_flight
3043 .as_ref()
3044 .expect("the next track is counting");
3045 assert_eq!(f.item, b);
3046 assert_eq!(f.listened_ms(), 0, "and starts from nothing");
3047 }
3048
3049 #[test]
3050 fn a_track_skipped_seconds_in_is_still_history() {
3051 let mut player = Player::new();
3052 start(&mut player, 7);
3053 listen(&mut player, 0, 2_000);
3054
3055 let event = player
3056 .finish_play()
3057 .expect("putting something on is a thing you did, however briefly");
3058 assert!(matches!(
3059 event,
3060 history::PlayEvent::Finished {
3061 track_id: 7,
3062 listened_ms: 2_000
3063 }
3064 ));
3065 }
3066
3067 #[test]
3068 fn a_track_is_closed_out_once() {
3069 let mut player = Player::new();
3070 start(&mut player, 7);
3071 listen(&mut player, 0, 200_000);
3072
3073 assert!(player.finish_play().is_some());
3074 assert!(player.finish_play().is_none());
3075 }
3076
3077 #[test]
3078 fn seeking_around_a_track_does_not_enter_it_twice() {
3079 let mut player = Player::new();
3080 let id = start(&mut player, 7);
3081 listen(&mut player, 0, 120_000);
3082
3083 player.on_track_changed(id, 30_000);
3085 assert_eq!(
3086 player.in_flight.as_ref().unwrap().listened_ms(),
3087 120_000,
3088 "the seek kept the count rather than restarting it"
3089 );
3090 listen(&mut player, 30_000, 40_000);
3091
3092 let Some(history::PlayEvent::Finished { listened_ms, .. }) = player.finish_play() else {
3093 panic!("still one play");
3094 };
3095 assert_eq!(listened_ms, 130_000);
3096 assert!(player.finish_play().is_none());
3097 }
3098
3099 #[test]
3100 fn a_track_that_is_not_in_the_library_is_not_recorded() {
3101 let mut player = Player::new();
3102 let id = QueueItemId::new();
3103 player.on_track_changed(id, 0);
3104 listen(&mut player, 0, 200_000);
3105 assert!(player.finish_play().is_none());
3106 }
3107
3108 #[test]
3109 fn stopping_closes_out_what_was_heard() {
3110 let mut player = Player::new();
3111 start(&mut player, 7);
3112 listen(&mut player, 0, 150_000);
3113
3114 player.stop_playback_and_clear_state();
3115 assert!(player.in_flight.is_none(), "the stop consumed it");
3116 }
3117
3118 #[test]
3119 fn resume_with_nothing_loaded_plays_the_cursor() {
3120 let dir = tempfile::tempdir().unwrap();
3121 let path = dir.path().join("t.wav");
3122 crate::test_utils::generate_wav(&path, 8_000, 1, 10.0, 16);
3123
3124 let mut player = Player::new();
3125 player.backend = Box::new(StuckBackend {
3126 rate: 8_000.0,
3127 asked: Default::default(),
3128 starts: Default::default(),
3129 });
3130 let item = PlaylistItem {
3131 db_id: Some(5),
3132 path,
3133 ..make_item("t")
3134 };
3135 let id = item.id;
3136 player.process_command(PlayerCommand::AddToPlaylist(vec![item]));
3137 player.shared_state.set_cursor(Some(id));
3138 assert!(player.session().is_none());
3139
3140 player.process_command(PlayerCommand::Resume);
3141 assert!(player.session().is_some(), "the cursor's track started");
3142 assert_eq!(player.shared_state.playback_state(), PlaybackState::Playing);
3143 player.process_command(PlayerCommand::Stop);
3144 }
3145
3146 #[test]
3147 fn the_audio_session_is_released_only_after_output_has_been_idle_a_while() {
3148 let mut player = Player::new();
3149 let now = std::time::Instant::now();
3150 assert!(
3151 !player.session_release_due(false, true, now),
3152 "the grace starts"
3153 );
3154 assert!(!player.session_release_due(false, true, now + SESSION_GRACE / 2));
3155 assert!(
3156 player.session_release_due(false, true, now + SESSION_GRACE),
3157 "released once it runs out"
3158 );
3159 assert_eq!(player.session_release_at, None);
3160
3161 assert!(!player.session_release_due(false, true, now));
3162 assert!(
3163 !player.session_release_due(true, true, now + SESSION_GRACE / 2),
3164 "playing again inside the grace keeps the session"
3165 );
3166 assert_eq!(player.session_release_at, None);
3167 assert!(
3168 !player.session_release_due(false, false, now),
3169 "nothing held"
3170 );
3171 assert_eq!(player.session_release_at, None);
3172 }
3173
3174 #[test]
3175 fn waiting_to_play_a_track_keeps_the_audio_session() {
3176 let mut player = Player::new();
3177 let item = make_item("t");
3178 player.transport = Transport::Waiting(Waiting {
3179 id: item.id,
3180 position_ms: 0,
3181 start: Run::Playing,
3182 });
3183 assert!(player.wants_output(), "play was asked for");
3184 let playing = player.wants_output();
3185 let now = std::time::Instant::now();
3186 for later in [now, now + SESSION_GRACE * 2] {
3187 assert!(!player.session_release_due(playing, true, later));
3188 }
3189 assert_eq!(player.session_release_at, None);
3190 }
3191
3192 #[test]
3193 fn a_player_with_nothing_coming_has_nothing_to_wake_for() {
3194 let dir = tempfile::tempdir().unwrap();
3195 let path = dir.path().join("t.wav");
3196 crate::test_utils::generate_wav(&path, 8_000, 1, 10.0, 16);
3197
3198 let mut player = Player::new();
3199 player.backend = Box::new(StuckBackend {
3200 rate: 8_000.0,
3201 asked: Default::default(),
3202 starts: Default::default(),
3203 });
3204 let item = PlaylistItem {
3205 path,
3206 ..make_item("t")
3207 };
3208 let id = item.id;
3209 player.process_command(PlayerCommand::AddToPlaylist(vec![item]));
3210 assert_eq!(player.next_wake(), None, "stopped");
3211
3212 player.process_command(PlayerCommand::Play(id));
3213 assert_eq!(player.shared_state.playback_state(), PlaybackState::Playing);
3214 assert_eq!(
3215 player.next_wake(),
3216 None,
3217 "playing, with no track queued after it"
3218 );
3219
3220 if let Transport::Loaded(session) = &mut player.transport {
3221 session.engine().unwrap().stop().unwrap();
3222 session.run = Run::Paused;
3223 }
3224 assert_eq!(player.next_wake(), None, "paused");
3225 player.process_command(PlayerCommand::Stop);
3226 }
3227
3228 #[test]
3229 fn a_track_cued_paused_never_starts_the_output() {
3230 let dir = tempfile::tempdir().unwrap();
3231 let path = dir.path().join("t.wav");
3232 crate::test_utils::generate_wav(&path, 8_000, 1, 10.0, 16);
3233
3234 let starts = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3235 let mut player = Player::new();
3236 player.backend = Box::new(StuckBackend {
3237 rate: 8_000.0,
3238 asked: Default::default(),
3239 starts: starts.clone(),
3240 });
3241 let item = PlaylistItem {
3242 path,
3243 ..make_item("t")
3244 };
3245 let id = item.id;
3246 player.process_command(PlayerCommand::AddToPlaylist(vec![item]));
3247
3248 player.process_command(PlayerCommand::Cue {
3249 id,
3250 position_ms: 2_000,
3251 play: false,
3252 });
3253 assert!(player.session().is_some(), "loaded");
3254 assert_eq!(player.shared_state.playback_state(), PlaybackState::Paused);
3255 loop {
3259 match player
3260 .commands
3261 .rx
3262 .recv_timeout(std::time::Duration::from_secs(5))
3263 {
3264 Ok(PlayerCommand::TrackQueued) => break,
3265 Ok(_) => {}
3266 Err(e) => panic!("the decoder never queued the track: {e}"),
3267 }
3268 }
3269 let at = player.shared_state.position_ms();
3270 assert!((1_750..=2_000).contains(&at), "cued at {at}ms");
3271 assert_eq!(starts.load(Ordering::Relaxed), 0, "not a sample let out");
3272
3273 player.process_command(PlayerCommand::Seek(4_000));
3275 assert_eq!(player.shared_state.playback_state(), PlaybackState::Paused);
3276 assert_eq!(starts.load(Ordering::Relaxed), 0);
3277
3278 player.process_command(PlayerCommand::Resume);
3279 assert_eq!(player.shared_state.playback_state(), PlaybackState::Playing);
3280 assert_eq!(starts.load(Ordering::Relaxed), 1);
3281 player.process_command(PlayerCommand::Stop);
3282 }
3283
3284 fn downloading_wav(dir: &Path) -> (Player, QueueItemId, Arc<std::sync::atomic::AtomicUsize>) {
3287 let path = dir.join("t.wav");
3288 crate::test_utils::generate_wav(&path, 8_000, 1, 10.0, 16);
3289 let starts = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3290 let mut player = Player::new();
3291 player.backend = Box::new(StuckBackend {
3292 rate: 8_000.0,
3293 asked: Default::default(),
3294 starts: starts.clone(),
3295 });
3296 let item = PlaylistItem {
3297 path,
3298 state: ItemState::Pending,
3299 ..make_item("t")
3300 };
3301 let id = item.id;
3302 player.process_command(PlayerCommand::AddToPlaylist(vec![item]));
3303 (player, id, starts)
3304 }
3305
3306 fn land(player: &mut Player, id: QueueItemId) {
3309 player.shared_state.update_item_state(id, ItemState::Ready);
3310 player.process_command(PlayerCommand::TrackReady(id));
3311 }
3312
3313 #[test]
3314 fn track_ready_never_resurrects_an_item_that_failed() {
3315 let mut player = Player::new();
3316 let item = PlaylistItem {
3317 state: ItemState::Failed("gone".into()),
3318 ..make_item("t")
3319 };
3320 let id = item.id;
3321 player.process_command(PlayerCommand::AddToPlaylist(vec![item]));
3322
3323 player.process_command(PlayerCommand::TrackReady(id));
3324
3325 assert!(matches!(
3326 player.shared_state.get_item(id).map(|i| i.state),
3327 Some(ItemState::Failed(_))
3328 ));
3329 }
3330
3331 fn await_queued(player: &Player) {
3332 loop {
3333 match player
3334 .commands
3335 .rx
3336 .recv_timeout(std::time::Duration::from_secs(5))
3337 {
3338 Ok(PlayerCommand::TrackQueued) => return,
3339 Ok(_) => {}
3340 Err(e) => panic!("the decoder never queued the track: {e}"),
3341 }
3342 }
3343 }
3344
3345 #[test]
3346 fn a_cue_on_a_downloading_track_opens_at_its_position_once_it_lands() {
3347 let dir = tempfile::tempdir().unwrap();
3348 let (mut player, id, starts) = downloading_wav(dir.path());
3349
3350 player.process_command(PlayerCommand::Cue {
3351 id,
3352 position_ms: 6_000,
3353 play: true,
3354 });
3355 assert!(player.session().is_none(), "nothing opened early");
3356 assert_eq!(player.shared_state.playback_state(), PlaybackState::Stopped);
3357 assert_eq!(starts.load(Ordering::Relaxed), 0);
3358
3359 land(&mut player, id);
3360 assert_eq!(player.shared_state.playback_state(), PlaybackState::Playing);
3361 assert_eq!(player.playback_starts, 1, "opened once, at the position");
3362 assert_eq!(starts.load(Ordering::Relaxed), 1);
3363 await_queued(&player);
3364 let at = player.shared_state.position_ms();
3365 assert!((5_750..=6_000).contains(&at), "opened at {at}ms");
3366 player.process_command(PlayerCommand::Stop);
3367 }
3368
3369 #[test]
3370 fn a_paused_cue_on_a_downloading_track_lands_paused() {
3371 let dir = tempfile::tempdir().unwrap();
3372 let (mut player, id, starts) = downloading_wav(dir.path());
3373
3374 player.process_command(PlayerCommand::Cue {
3375 id,
3376 position_ms: 3_000,
3377 play: false,
3378 });
3379 land(&mut player, id);
3380 assert_eq!(player.shared_state.playback_state(), PlaybackState::Paused);
3381 assert_eq!(starts.load(Ordering::Relaxed), 0, "not a sample let out");
3382 await_queued(&player);
3383 let at = player.shared_state.position_ms();
3384 assert!((2_750..=3_000).contains(&at), "cued at {at}ms");
3385 player.process_command(PlayerCommand::Stop);
3386 }
3387
3388 #[test]
3389 fn playing_something_else_forgets_a_waiting_cue() {
3390 let dir = tempfile::tempdir().unwrap();
3391 let (mut player, id, _) = downloading_wav(dir.path());
3392 let other = seed(&mut player, 1)[0];
3393
3394 player.process_command(PlayerCommand::Cue {
3395 id,
3396 position_ms: 6_000,
3397 play: true,
3398 });
3399 player.process_command(PlayerCommand::Play(other));
3400 player.process_command(PlayerCommand::Play(id));
3401 assert!(player.waiting().is_some_and(|w| w.position_ms == 0));
3402 player.process_command(PlayerCommand::Stop);
3403 }
3404
3405 #[test]
3406 fn a_paused_track_whose_download_lands_stays_paused_where_it_was() {
3407 let dir = tempfile::tempdir().unwrap();
3408 let (mut player, id, starts) = downloading_wav(dir.path());
3409 player.shared_state.update_item_state(id, ItemState::Ready);
3411 player.process_command(PlayerCommand::Cue {
3412 id,
3413 position_ms: 3_000,
3414 play: false,
3415 });
3416 await_queued(&player);
3417
3418 player.process_command(PlayerCommand::TrackReady(id));
3419 assert_eq!(player.shared_state.playback_state(), PlaybackState::Paused);
3420 assert_eq!(player.playback_starts, 1, "not reopened");
3421 assert_eq!(starts.load(Ordering::Relaxed), 0, "not a sample let out");
3422 player.process_command(PlayerCommand::Stop);
3423 }
3424
3425 #[test]
3426 fn pausing_a_track_on_its_way_opens_it_paused() {
3427 let dir = tempfile::tempdir().unwrap();
3428 let (mut player, id, starts) = downloading_wav(dir.path());
3429
3430 player.process_command(PlayerCommand::Play(id));
3431 assert!(player.shared_state.is_waiting());
3432 assert!(player.shared_state.wants_to_play(), "a toggle pauses it");
3433 assert!(!player.shared_state.is_idle(), "adding tracks leaves it be");
3434 player.process_command(PlayerCommand::Pause);
3435 assert_eq!(player.shared_state.playback_state(), PlaybackState::Paused);
3436 assert!(!player.shared_state.wants_to_play());
3437
3438 player.shared_state.update_item_state(id, ItemState::Ready);
3439 player.process_command(PlayerCommand::TrackReady(id));
3440 assert!(player.session().is_some(), "loaded");
3441 assert_eq!(player.shared_state.playback_state(), PlaybackState::Paused);
3442 assert_eq!(starts.load(Ordering::Relaxed), 0, "not a sample let out");
3443 player.process_command(PlayerCommand::Stop);
3444 }
3445
3446 #[test]
3447 fn a_cue_paused_and_resumed_on_its_way_keeps_its_position() {
3448 let dir = tempfile::tempdir().unwrap();
3449 let (mut player, id, starts) = downloading_wav(dir.path());
3450
3451 player.process_command(PlayerCommand::Cue {
3452 id,
3453 position_ms: 6_000,
3454 play: true,
3455 });
3456 player.process_command(PlayerCommand::Pause);
3457 player.process_command(PlayerCommand::Resume);
3458 assert!(player.session().is_none(), "still on its way");
3459 assert_eq!(player.shared_state.playback_state(), PlaybackState::Stopped);
3460
3461 player.shared_state.update_item_state(id, ItemState::Ready);
3462 player.process_command(PlayerCommand::TrackReady(id));
3463 assert_eq!(player.shared_state.playback_state(), PlaybackState::Playing);
3464 assert_eq!(starts.load(Ordering::Relaxed), 1);
3465 await_queued(&player);
3466 let at = player.shared_state.position_ms();
3467 assert!((5_750..=6_000).contains(&at), "opened at {at}ms");
3468 player.process_command(PlayerCommand::Stop);
3469 }
3470
3471 #[test]
3472 fn a_restored_cursor_does_not_start_when_its_download_lands() {
3473 let dir = tempfile::tempdir().unwrap();
3474 let (mut player, id, starts) = downloading_wav(dir.path());
3475 player.shared_state.set_cursor(Some(id));
3476
3477 player.shared_state.update_item_state(id, ItemState::Ready);
3478 player.process_command(PlayerCommand::TrackReady(id));
3479 assert!(player.session().is_none());
3480 assert_eq!(starts.load(Ordering::Relaxed), 0);
3481 }
3482
3483 #[test]
3484 fn replacing_the_queue_keeps_a_transfer_both_queues_want() {
3485 let pending = |title: &str| PlaylistItem {
3491 db_id: Some(7),
3492 state: ItemState::Pending,
3493 ..make_item(title)
3494 };
3495 let mut player = Player::new();
3496 let old = pending("old");
3497 let old_id = old.id;
3498 player.process_command(PlayerCommand::AddToPlaylist(vec![old]));
3499 let store = player.shared_state.downloads().clone();
3500 store.claim(7, Some(old_id));
3501
3502 let before = player.shared_state.pending_version();
3503 player.process_command(PlayerCommand::ReplacePlaylist {
3504 items: vec![pending("again")],
3505 start: 0,
3506 position_ms: 0,
3507 play: false,
3508 mode: QueueMode::Keep,
3509 });
3510 assert_eq!(
3511 player.shared_state.pending_version(),
3512 before + 1,
3513 "one change, so no reader can see the playlist between two"
3514 );
3515
3516 store.resync(&player.shared_state.pending_downloads());
3518 assert!(!store.abandoned(7));
3519 }
3520
3521 #[test]
3522 fn a_hand_off_opens_paused_at_its_position_in_one_command() {
3523 let dir = tempfile::tempdir().unwrap();
3524 let path = dir.path().join("t.wav");
3525 crate::test_utils::generate_wav(&path, 8_000, 1, 10.0, 16);
3526 let starts = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3527 let mut player = Player::new();
3528 player.backend = Box::new(StuckBackend {
3529 rate: 8_000.0,
3530 asked: Default::default(),
3531 starts: starts.clone(),
3532 });
3533 seed(&mut player, 2);
3534 let item = PlaylistItem {
3535 path,
3536 ..make_item("t")
3537 };
3538 let id = item.id;
3539
3540 player.process_command(PlayerCommand::ReplacePlaylist {
3541 items: vec![make_item("before"), item],
3542 start: 1,
3543 position_ms: 4_000,
3544 play: false,
3545 mode: QueueMode::Keep,
3546 });
3547 assert_eq!(
3548 playlist_titles(&player),
3549 vec!["before", "t"],
3550 "replaced, not added to"
3551 );
3552 assert_eq!(player.shared_state.cursor(), Some(id));
3553 assert_eq!(player.shared_state.playback_state(), PlaybackState::Paused);
3554 assert_eq!(player.playback_starts, 1, "opened once, at the position");
3555 assert_eq!(starts.load(Ordering::Relaxed), 0, "not a sample let out");
3556 await_queued(&player);
3557 let at = player.shared_state.position_ms();
3558 assert!((3_750..=4_000).contains(&at), "opened at {at}ms");
3559
3560 player.process_command(PlayerCommand::Undo);
3561 assert_eq!(playlist_titles(&player), vec!["t0", "t1"], "one undo step");
3562 player.process_command(PlayerCommand::Stop);
3563 }
3564
3565 #[test]
3566 fn a_decode_end_from_a_replaced_session_is_ignored() {
3567 let dir = tempfile::tempdir().unwrap();
3568 let mut player = Player::new();
3569 player.backend = Box::new(StuckBackend {
3570 rate: 8_000.0,
3571 asked: Default::default(),
3572 starts: Default::default(),
3573 });
3574 let items: Vec<_> = ["a", "b", "c"]
3575 .iter()
3576 .map(|name| {
3577 let path = dir.path().join(format!("{name}.wav"));
3578 crate::test_utils::generate_wav(&path, 8_000, 1, 10.0, 16);
3579 PlaylistItem {
3580 path,
3581 ..make_item(name)
3582 }
3583 })
3584 .collect();
3585 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
3586 player.process_command(PlayerCommand::AddToPlaylist(items));
3587
3588 player.process_command(PlayerCommand::Play(ids[0]));
3589 let stale = player.session;
3590 player.process_command(PlayerCommand::Play(ids[1]));
3591 player.process_command(PlayerCommand::DecodeFinished(stale));
3592
3593 assert_eq!(player.shared_state.cursor(), Some(ids[1]));
3594 assert_eq!(player.playback_starts, 2);
3595 player.process_command(PlayerCommand::Stop);
3596 }
3597
3598 fn queued_wavs(dir: &Path, names: &[&str]) -> (Player, Vec<QueueItemId>) {
3601 let (player, ids) = wavs_playing(dir, names, 1.0);
3602 for _ in &ids {
3603 await_queued(&player);
3604 }
3605 (player, ids)
3606 }
3607
3608 fn wavs_playing(dir: &Path, names: &[&str], seconds: f32) -> (Player, Vec<QueueItemId>) {
3611 wavs_in(dir, names, seconds, Repeat::Off)
3612 }
3613
3614 fn wavs_in(
3617 dir: &Path,
3618 names: &[&str],
3619 seconds: f32,
3620 repeat: Repeat,
3621 ) -> (Player, Vec<QueueItemId>) {
3622 let mut player = Player::new();
3623 player.backend = Box::new(StuckBackend {
3624 rate: 8_000.0,
3625 asked: Default::default(),
3626 starts: Default::default(),
3627 });
3628 let items: Vec<_> = names
3629 .iter()
3630 .zip(1..)
3631 .map(|(name, track)| {
3632 let path = dir.join(format!("{name}.wav"));
3633 crate::test_utils::generate_wav(&path, 8_000, 1, seconds, 16);
3634 PlaylistItem {
3635 path,
3636 db_id: Some(track),
3637 ..make_item(name)
3638 }
3639 })
3640 .collect();
3641 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
3642 player.process_command(PlayerCommand::AddToPlaylist(items));
3643 player.process_command(PlayerCommand::SetRepeat(repeat));
3644 player.process_command(PlayerCommand::Play(ids[0]));
3645 (player, ids)
3646 }
3647
3648 fn queued_at_least(player: &Player, n: usize) -> Vec<QueueItemId> {
3650 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
3651 loop {
3652 let queued = player.timeline.queued_after_playhead();
3653 if queued.len() >= n {
3654 return queued;
3655 }
3656 assert!(
3657 std::time::Instant::now() < deadline,
3658 "the decoder queued {queued:?}, never {n}"
3659 );
3660 std::thread::yield_now();
3661 }
3662 }
3663
3664 #[test]
3667 fn repeating_the_queue_runs_the_last_track_into_the_first() {
3668 let dir = tempfile::tempdir().unwrap();
3669 let (mut player, ids) = wavs_in(dir.path(), &["a", "b"], 1.0, Repeat::Queue);
3670
3671 let queued = queued_at_least(&player, 3);
3672 assert_eq!(
3673 queued[..3],
3674 [ids[1], ids[0], ids[1]],
3675 "gapless, round again"
3676 );
3677 let steps = player.session().unwrap().lookahead.lock().clone();
3678 assert!(!steps[0].wrapped);
3679 let wrap = steps.iter().find(|s| s.after == ids[1]).unwrap();
3680 assert_eq!(wrap.next, Some(ids[0]));
3681 assert!(wrap.wrapped);
3682 assert_eq!(player.playback_starts, 1);
3683 player.process_command(PlayerCommand::Stop);
3684 }
3685
3686 #[test]
3687 fn turning_repeat_off_takes_back_a_wrap_already_queued() {
3688 let dir = tempfile::tempdir().unwrap();
3689 let (mut player, ids) = wavs_in(dir.path(), &["a"], 1.0, Repeat::Queue);
3690 assert_eq!(queued_at_least(&player, 1)[0], ids[0]);
3691
3692 player.process_command(PlayerCommand::SetRepeat(Repeat::Off));
3693 assert_eq!(player.playback_starts, 2, "restarted at the playhead");
3694 wait_for_lookahead(&player);
3695 assert!(player.timeline.queued_after_playhead().is_empty());
3696 player.process_command(PlayerCommand::Stop);
3697 }
3698
3699 #[test]
3700 fn repeating_one_track_queues_it_again_and_each_pass_is_a_play() {
3701 let dir = tempfile::tempdir().unwrap();
3702 let (mut player, ids) = wavs_in(dir.path(), &["a", "b"], 1.0, Repeat::One);
3703 let (recorder, events) = history::PlayRecorder::capture();
3704 player.history = Some(recorder);
3705 player.in_flight = None;
3708 player.on_track_changed(ids[0], 0);
3709
3710 assert_eq!(queued_at_least(&player, 2)[..2], [ids[0], ids[0]]);
3711 player
3713 .timeline
3714 .samples_played
3715 .store(8_400, Ordering::Relaxed);
3716 player.update_playback_state();
3717
3718 let flight = player.in_flight.as_ref().unwrap();
3719 assert_eq!((flight.item, flight.boundary), (ids[0], 1));
3720 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
3721 let events: Vec<_> = events.try_iter().collect();
3722 let started = events
3723 .iter()
3724 .filter(|e| matches!(e, PlayEvent::Started { track_id: 1, .. }))
3725 .count();
3726 assert_eq!(started, 2, "two plays: {events:?}");
3727 assert!(
3728 events.contains(&PlayEvent::Finished {
3729 track_id: 1,
3730 listened_ms: 1_000
3731 }),
3732 "the first pass banked whole: {events:?}"
3733 );
3734 player.process_command(PlayerCommand::Stop);
3735 }
3736
3737 #[test]
3738 fn next_moves_on_from_a_track_repeating() {
3739 let dir = tempfile::tempdir().unwrap();
3740 let (mut player, ids) = wavs_in(dir.path(), &["a", "b"], 1.0, Repeat::One);
3741
3742 player.process_command(PlayerCommand::NextTrack);
3743 assert_eq!(playing_id(&player), Some(ids[1]));
3744 player.process_command(PlayerCommand::NextTrack);
3745 assert_eq!(
3746 playing_id(&player),
3747 Some(ids[0]),
3748 "round from the last, as repeating does"
3749 );
3750 player.process_command(PlayerCommand::Stop);
3751 }
3752
3753 #[test]
3754 fn a_repeat_of_nothing_heard_stops_rather_than_going_round() {
3755 let dir = tempfile::tempdir().unwrap();
3756 let (mut player, _) = wavs_in(dir.path(), &["a"], 1.0, Repeat::One);
3757
3758 player.process_command(PlayerCommand::DecodeFinished(player.session));
3761 assert!(matches!(player.transport, Transport::Idle));
3762 }
3763
3764 fn session_run(player: &Player) -> Option<Run> {
3767 player.session().map(|s| s.run)
3768 }
3769
3770 #[test]
3771 fn a_sleep_timer_fades_out_at_its_time_and_keeps_the_queue() {
3772 let dir = tempfile::tempdir().unwrap();
3773 let (mut player, ids) = wavs_playing(dir.path(), &["a", "b"], 1.0);
3774
3775 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::After {
3776 minutes: 30,
3777 })));
3778 let Some(Sleep::At { unix_ms }) = player.shared_state.sleep() else {
3779 panic!("published as a time");
3780 };
3781 let now = std::time::SystemTime::now()
3782 .duration_since(std::time::UNIX_EPOCH)
3783 .unwrap()
3784 .as_millis() as u64;
3785 assert!(unix_ms.abs_diff(now + 30 * 60_000) < 5_000);
3786 let at = player.sleep.and_then(|s| s.at).unwrap();
3787 assert!(player.next_wake().is_some_and(|w| w <= at), "woken for it");
3788
3789 player.sleep.as_mut().unwrap().at = Some(std::time::Instant::now());
3791 player.update_playback_state();
3792 assert_eq!(session_run(&player), Some(Run::Paused));
3793 assert!(
3794 !player.session().unwrap().engine().unwrap().is_running(),
3795 "stopped at silence: the fade did the fading"
3796 );
3797 assert!(
3798 player.sleep_fade.is_none(),
3799 "full level for what plays next"
3800 );
3801 assert_eq!(player.shared_state.sleep(), None, "spent");
3802 assert_eq!(playlist_ids(&player), ids, "the queue as it was");
3803 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
3804 player.process_command(PlayerCommand::Stop);
3805 }
3806
3807 fn fading_gain(player: &Player) -> Option<f32> {
3808 match player.sleep_fade {
3809 Some(sleep::SleepFade::Falling { gain, .. }) => Some(gain),
3810 _ => None,
3811 }
3812 }
3813
3814 fn db(gain: f32) -> f32 {
3815 20.0 * gain.log10()
3816 }
3817
3818 #[test]
3821 fn a_sleep_timer_fades_over_the_run_up_to_its_deadline() {
3822 let dir = tempfile::tempdir().unwrap();
3823 let (mut player, _) = wavs_playing(dir.path(), &["a"], 1.0);
3824 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::After {
3825 minutes: 15,
3826 })));
3827 let now = std::time::Instant::now();
3828 let secs = std::time::Duration::from_secs;
3829
3830 player.sleep.as_mut().unwrap().at = Some(now + secs(91));
3831 player.update_playback_state();
3832 assert_eq!(fading_gain(&player), None, "not yet: 91 s out");
3833 assert!(!player.shared_state.sleep_fading());
3834 let wake = player.next_wake().unwrap();
3835 assert!(
3836 wake <= now + secs(2),
3837 "woken for the fade's start, a second away"
3838 );
3839
3840 player.sleep.as_mut().unwrap().at = Some(now + secs(45));
3841 player.update_playback_state();
3842 let half = fading_gain(&player).expect("half way through the fade");
3843 assert!((db(half) + 30.0).abs() < 1.0, "{} dB", db(half));
3844 assert!(player.shared_state.sleep_fading(), "published as fading");
3845 assert!(player.next_wake().unwrap() <= std::time::Instant::now() + secs(1));
3846
3847 player.sleep.as_mut().unwrap().at = Some(now + std::time::Duration::from_millis(900));
3848 player.update_playback_state();
3849 let late = fading_gain(&player).unwrap();
3850 assert!(
3851 db(late) < -59.0,
3852 "all but silent at the end: {} dB",
3853 db(late)
3854 );
3855 player.process_command(PlayerCommand::Stop);
3856 }
3857
3858 #[test]
3860 fn cancelling_a_sleep_timer_mid_fade_brings_the_level_back() {
3861 let dir = tempfile::tempdir().unwrap();
3862 let (mut player, _) = wavs_playing(dir.path(), &["a"], 1.0);
3863 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::After {
3864 minutes: 15,
3865 })));
3866 player.sleep.as_mut().unwrap().at =
3867 Some(std::time::Instant::now() + std::time::Duration::from_secs(45));
3868 player.update_playback_state();
3869 assert!(fading_gain(&player).is_some());
3870
3871 player.process_command(PlayerCommand::SetSleepTimer(None));
3872 let Some(sleep::SleepFade::Restoring { from, .. }) = player.sleep_fade else {
3873 panic!("coming back up: {:?}", player.sleep_fade);
3874 };
3875 assert!((db(from) + 30.0).abs() < 1.0, "from where it had got to");
3876 assert!(!player.shared_state.sleep_fading());
3877 if let Some(sleep::SleepFade::Restoring { since, .. }) = player.sleep_fade.as_mut() {
3879 *since -= std::time::Duration::from_secs(1);
3880 }
3881 player.update_playback_state();
3882 assert!(player.sleep_fade.is_none(), "at full level, and left alone");
3883 assert_eq!(session_run(&player), Some(Run::Playing));
3884 player.process_command(PlayerCommand::Stop);
3885 }
3886
3887 #[test]
3890 fn pausing_during_the_fade_cancels_the_timer() {
3891 let dir = tempfile::tempdir().unwrap();
3892 let (mut player, _) = wavs_playing(dir.path(), &["a"], 1.0);
3893 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::After {
3894 minutes: 15,
3895 })));
3896 player.sleep.as_mut().unwrap().at =
3897 Some(std::time::Instant::now() + std::time::Duration::from_secs(45));
3898 player.update_playback_state();
3899 assert!(fading_gain(&player).is_some());
3900
3901 player.process_command(PlayerCommand::Pause);
3902 assert_eq!(player.shared_state.sleep(), None, "cancelled");
3903 assert!(player.sleep_fade.is_none());
3904 assert!(player.sleep_snap_on_resume);
3905 player.process_command(PlayerCommand::Resume);
3906 assert!(!player.sleep_snap_on_resume, "full level taken on resuming");
3907 player.process_command(PlayerCommand::Stop);
3908 }
3909
3910 #[test]
3914 fn the_end_of_a_track_or_record_fades_into_the_boundary() {
3915 let dir = tempfile::tempdir().unwrap();
3916 let (mut player, _) = queued_wavs(dir.path(), &["a", "b"]);
3917 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::EndOfTrack)));
3918 player.update_playback_state();
3919 assert!(
3920 fading_gain(&player).is_some(),
3921 "a one-second track fades whole"
3922 );
3923
3924 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::EndOfRecord)));
3927 if let Some(sleep::SleepFade::Restoring { since, .. }) = player.sleep_fade.as_mut() {
3928 *since -= std::time::Duration::from_secs(1);
3929 }
3930 player.update_playback_state();
3931 assert_eq!(fading_gain(&player), None, "the record goes on");
3932 player.process_command(PlayerCommand::Stop);
3933 }
3934
3935 #[test]
3936 fn a_sleep_timer_going_off_while_paused_only_ends() {
3937 let dir = tempfile::tempdir().unwrap();
3938 let (mut player, _) = wavs_playing(dir.path(), &["a"], 1.0);
3939 player.process_command(PlayerCommand::Pause);
3940 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::After {
3941 minutes: 1,
3942 })));
3943 player.sleep.as_mut().unwrap().at = Some(std::time::Instant::now());
3944 player.update_playback_state();
3945 assert_eq!(player.shared_state.sleep(), None);
3946 player.process_command(PlayerCommand::Resume);
3947 assert_eq!(
3948 session_run(&player),
3949 Some(Run::Playing),
3950 "nothing left to go off"
3951 );
3952 player.process_command(PlayerCommand::Stop);
3953 }
3954
3955 #[test]
3956 fn cancelling_a_sleep_timer_leaves_playback_alone() {
3957 let dir = tempfile::tempdir().unwrap();
3958 let (mut player, _) = wavs_playing(dir.path(), &["a"], 1.0);
3959 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::After {
3960 minutes: 1,
3961 })));
3962 player.process_command(PlayerCommand::SetSleepTimer(None));
3963 assert_eq!(player.shared_state.sleep(), None);
3964 assert!(player.sleep.is_none());
3965 player.update_playback_state();
3966 assert_eq!(session_run(&player), Some(Run::Playing));
3967 player.process_command(PlayerCommand::Stop);
3968 }
3969
3970 #[test]
3973 fn a_sleep_timer_for_the_end_of_the_track_stops_at_the_boundary() {
3974 let dir = tempfile::tempdir().unwrap();
3975 let (mut player, ids) = queued_wavs(dir.path(), &["a", "b"]);
3976 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::EndOfTrack)));
3977 assert_eq!(player.shared_state.sleep(), Some(Sleep::EndOfTrack));
3978
3979 player
3981 .timeline
3982 .samples_played
3983 .store(4_000, Ordering::Relaxed);
3984 player.update_playback_state();
3985 assert_eq!(session_run(&player), Some(Run::Playing));
3986 assert_eq!(player.shared_state.sleep(), Some(Sleep::EndOfTrack));
3987
3988 player
3990 .timeline
3991 .samples_played
3992 .store(8_400, Ordering::Relaxed);
3993 player.update_playback_state();
3994 assert_eq!(player.shared_state.cursor(), Some(ids[1]));
3995 assert_eq!(session_run(&player), Some(Run::Paused));
3996 assert!(
3997 !player.session().unwrap().engine().unwrap().is_running(),
3998 "stopped at once, nothing of the next track faded through"
3999 );
4000 assert_eq!(player.shared_state.sleep(), None);
4001 assert_eq!(playlist_ids(&player), ids);
4002 player.process_command(PlayerCommand::Stop);
4003 }
4004
4005 #[test]
4008 fn a_sleep_timer_for_the_end_of_the_track_stops_a_track_repeating() {
4009 let dir = tempfile::tempdir().unwrap();
4010 let (mut player, ids) = wavs_in(dir.path(), &["a", "b"], 1.0, Repeat::One);
4011 assert_eq!(queued_at_least(&player, 1)[0], ids[0]);
4012 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::EndOfTrack)));
4013 player
4014 .timeline
4015 .samples_played
4016 .store(8_400, Ordering::Relaxed);
4017 player.update_playback_state();
4018 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
4019 assert_eq!(session_run(&player), Some(Run::Paused));
4020 assert_eq!(player.shared_state.sleep(), None);
4021 player.process_command(PlayerCommand::Stop);
4022 }
4023
4024 #[test]
4027 fn a_sleep_timer_for_the_end_of_the_track_opens_the_next_paused() {
4028 let dir = tempfile::tempdir().unwrap();
4029 let (mut player, ids) = wavs_playing(dir.path(), &["a", "b"], 1.0);
4030 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::EndOfTrack)));
4031 player.process_command(PlayerCommand::DecodeFinished(player.session));
4032 assert_eq!(player.shared_state.cursor(), Some(ids[1]));
4033 assert_eq!(player.intent(), Some(Run::Paused));
4034 assert_eq!(player.shared_state.sleep(), None);
4035 player.process_command(PlayerCommand::Stop);
4036 }
4037
4038 #[test]
4039 fn a_sleep_timer_for_the_end_of_the_record_plays_the_record_out() {
4040 let dir = tempfile::tempdir().unwrap();
4041 let mut player = Player::new();
4042 player.backend = Box::new(StuckBackend {
4043 rate: 8_000.0,
4044 asked: Default::default(),
4045 starts: Default::default(),
4046 });
4047 let items: Vec<_> = [("a", "Low"), ("b", "Low"), ("c", "Heroes")]
4048 .iter()
4049 .map(|(name, album)| {
4050 let path = dir.path().join(format!("{name}.wav"));
4051 crate::test_utils::generate_wav(&path, 8_000, 1, 1.0, 16);
4052 PlaylistItem {
4053 path,
4054 album: album.to_string(),
4055 album_artist: "David Bowie".into(),
4056 ..make_item(name)
4057 }
4058 })
4059 .collect();
4060 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
4061 player.process_command(PlayerCommand::AddToPlaylist(items));
4062 player.process_command(PlayerCommand::Play(ids[0]));
4063 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::EndOfRecord)));
4064
4065 player.process_command(PlayerCommand::DecodeFinished(player.session));
4066 assert_eq!(player.shared_state.cursor(), Some(ids[1]));
4067 assert_eq!(player.intent(), Some(Run::Playing), "the same record");
4068 assert_eq!(player.shared_state.sleep(), Some(Sleep::EndOfRecord));
4069
4070 player.process_command(PlayerCommand::DecodeFinished(player.session));
4071 assert_eq!(player.shared_state.cursor(), Some(ids[2]));
4072 assert_eq!(player.intent(), Some(Run::Paused), "the next record waits");
4073 assert_eq!(player.shared_state.sleep(), None);
4074 player.process_command(PlayerCommand::Stop);
4075 }
4076
4077 #[test]
4078 fn shuffle_is_not_an_undo_step() {
4079 let mut player = Player::new();
4080 let ids = seed(&mut player, 10);
4081 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[4]));
4082 player.process_command(PlayerCommand::SetShuffle(true));
4083 assert_eq!(playlist_ids(&player).len(), 9, "the queue is as it was");
4084
4085 player.process_command(PlayerCommand::Undo);
4086 assert_eq!(playlist_ids(&player), ids, "the removal undone");
4087 assert!(player.shared_state.play_mode().shuffle, "shuffle left on");
4088 }
4089
4090 #[test]
4091 fn a_queue_replaced_while_shuffled_starts_where_asked_and_shuffles_the_rest() {
4092 let mut player = Player::new();
4093 seed(&mut player, 3);
4094 player.process_command(PlayerCommand::SetShuffle(true));
4095
4096 let items: Vec<_> = (0..20).map(|i| make_item(&format!("n{i}"))).collect();
4097 let given: Vec<_> = items.iter().map(|i| i.id).collect();
4098 player.process_command(PlayerCommand::ReplacePlaylist {
4099 items,
4100 start: 5,
4101 position_ms: 0,
4102 play: false,
4103 mode: QueueMode::Keep,
4104 });
4105 assert_eq!(playlist_ids(&player), given, "in the order given");
4106 assert_eq!(player.shared_state.cursor(), Some(given[5]));
4107
4108 let mut heard = vec![given[5]];
4109 while let Some(id) = player.shared_state.advance_cursor_loadable() {
4110 heard.push(id);
4111 }
4112 assert_ne!(heard, given);
4113 heard.sort_by_key(|id| given.iter().position(|i| i == id));
4114 assert_eq!(heard, given, "each once");
4115 }
4116
4117 #[test]
4118 fn the_pass_of_a_track_is_the_step_that_opened_it() {
4119 let step = |boundary, pass| state::Lookahead {
4120 after: QueueItemId::new(),
4121 next: None,
4122 chosen: None,
4123 wrapped: false,
4124 boundary,
4125 after_pass: 0,
4126 pass,
4127 };
4128 let steps = [step(0, 0), step(1, 0), step(1, 1), step(2, 1)];
4131 assert_eq!(step_pass(&steps, 1), Some(1));
4132 assert_eq!(step_pass(&steps, 3), None);
4133 }
4134
4135 fn played_out(player: &mut Player) -> Vec<QueueItemId> {
4138 let ids = seed(player, 4);
4139 player.process_command(PlayerCommand::SetShuffle(true));
4140 let state = &player.shared_state;
4141 state.advance_cursor_loadable();
4142 loop {
4143 state.mark_played(state.cursor().unwrap());
4144 if state.advance_cursor_loadable().is_none() {
4145 break;
4146 }
4147 }
4148 ids
4149 }
4150
4151 fn played_count(player: &Player) -> usize {
4152 player.shared_state.derive_visible_queue().finished_count
4153 }
4154
4155 #[test]
4156 fn a_session_restored_at_the_end_of_a_pass_does_not_start_another() {
4157 let mut player = Player::new();
4158 let ids = played_out(&mut player);
4159 let last = player.shared_state.cursor().unwrap();
4160
4161 player.process_command(PlayerCommand::Cue {
4162 id: last,
4163 position_ms: 0,
4164 play: false,
4165 });
4166 assert_eq!(played_count(&player), ids.len() - 1, "the marks kept");
4167 assert_eq!(
4168 player.shared_state.advance_cursor_loadable(),
4169 None,
4170 "repeat off: the queue ends"
4171 );
4172 }
4173
4174 #[test]
4175 fn a_track_repeating_keeps_the_marks() {
4176 let mut player = Player::new();
4177 let ids = played_out(&mut player);
4178 player.process_command(PlayerCommand::SetRepeat(Repeat::One));
4179 let last = player.shared_state.cursor().unwrap();
4180 player.cue(last, 0, Run::Paused);
4182 assert_eq!(played_count(&player), ids.len() - 1);
4183 player.process_command(PlayerCommand::Stop);
4184 }
4185
4186 #[test]
4187 fn picking_a_row_of_a_played_out_queue_starts_a_new_pass() {
4188 let mut player = Player::new();
4189 let ids = played_out(&mut player);
4190 player.process_command(PlayerCommand::Play(ids[1]));
4191 assert_eq!(played_count(&player), 0);
4192 assert_eq!(player.shared_state.upcoming().len(), ids.len() - 1);
4193 player.process_command(PlayerCommand::Stop);
4194 }
4195
4196 fn replace(player: &mut Player, n: usize, mode: QueueMode) -> Vec<QueueItemId> {
4197 let items: Vec<_> = (0..n).map(|i| make_item(&format!("r{i}"))).collect();
4198 let ids = items.iter().map(|i| i.id).collect();
4199 player.process_command(PlayerCommand::ReplacePlaylist {
4200 items,
4201 start: 0,
4202 position_ms: 0,
4203 play: false,
4204 mode,
4205 });
4206 ids
4207 }
4208
4209 fn shuffled_and_repeating() -> Player {
4210 let mut player = Player::new();
4211 seed(&mut player, 3);
4212 player.process_command(PlayerCommand::SetShuffle(true));
4213 player.process_command(PlayerCommand::SetRepeat(Repeat::Queue));
4214 player
4215 }
4216
4217 #[test]
4218 fn playing_from_a_play_button_turns_shuffle_and_repeat_off() {
4219 let mut player = shuffled_and_repeating();
4220 let ids = replace(&mut player, 8, QueueMode::InOrder);
4221 assert_eq!(player.shared_state.play_mode(), PlayMode::default());
4222 assert!(!player.shared_state.is_shuffled());
4223 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
4224 }
4225
4226 #[test]
4227 fn shuffling_from_a_play_button_turns_shuffle_on_and_repeat_off() {
4228 let mut player = Player::new();
4229 player.process_command(PlayerCommand::SetRepeat(Repeat::One));
4230 let ids = replace(&mut player, 12, QueueMode::Shuffled);
4231 assert_eq!(
4232 player.shared_state.play_mode(),
4233 PlayMode {
4234 shuffle: true,
4235 repeat: Repeat::Off
4236 }
4237 );
4238 assert_eq!(playlist_ids(&player), ids, "the queue in the order given");
4239
4240 let mut heard = vec![player.shared_state.cursor().unwrap()];
4241 while let Some(id) = player.shared_state.advance_cursor_loadable() {
4242 heard.push(id);
4243 }
4244 assert_ne!(heard, ids, "started and played in a shuffled order");
4245 heard.sort_by_key(|id| ids.iter().position(|i| i == id));
4246 assert_eq!(heard, ids);
4247 }
4248
4249 #[test]
4250 fn a_queue_restored_or_handed_over_keeps_the_modes() {
4251 let mut player = shuffled_and_repeating();
4252 let mode = player.shared_state.play_mode();
4253 replace(&mut player, 5, QueueMode::Keep);
4254 assert_eq!(player.shared_state.play_mode(), mode);
4255 assert!(player.shared_state.is_shuffled());
4256 }
4257
4258 #[test]
4259 fn adding_to_the_queue_keeps_the_modes() {
4260 let mut player = shuffled_and_repeating();
4261 let mode = player.shared_state.play_mode();
4262 seed(&mut player, 4);
4263 let ids = playlist_ids(&player);
4264 player.process_command(PlayerCommand::InsertInPlaylist {
4265 items: vec![make_item("next")],
4266 after: ids[0],
4267 });
4268 assert_eq!(player.shared_state.play_mode(), mode);
4269 }
4270
4271 #[test]
4272 fn removing_the_playing_track_while_shuffled_moves_on_in_the_play_order() {
4273 let mut player = Player::new();
4274 seed(&mut player, 10);
4275 player.process_command(PlayerCommand::SetShuffle(true));
4276 let playing = player.shared_state.advance_cursor_loadable().unwrap();
4277 pretend_playing(&mut player, playing);
4278 let next = player.shared_state.lookahead_after(playing).unwrap().next;
4279
4280 player.process_command(PlayerCommand::RemoveFromPlaylist(playing));
4281 assert_eq!(player.shared_state.cursor(), next);
4282 player.process_command(PlayerCommand::Stop);
4283 }
4284
4285 #[test]
4286 fn shuffled_the_decoder_queues_the_play_order() {
4287 let dir = tempfile::tempdir().unwrap();
4288 let names = ["a", "b", "c", "d", "e", "f"];
4289 let (mut player, ids) = wavs_playing(dir.path(), &names, 1.0);
4290 queued_at_least(&player, names.len() - 1);
4291
4292 player.process_command(PlayerCommand::SetShuffle(true));
4293 let order = player.shared_state.upcoming();
4294 assert_eq!(order.len(), names.len() - 1);
4295 assert_eq!(queued_at_least(&player, order.len()), order);
4296 assert_eq!(playlist_ids(&player), ids, "the queue never moved");
4297 player.process_command(PlayerCommand::Stop);
4298 }
4299
4300 #[test]
4301 fn removing_the_last_track_playing_carries_on_from_the_top_when_repeating() {
4302 let mut player = Player::new();
4303 let ids = seed(&mut player, 3);
4304 player.process_command(PlayerCommand::SetRepeat(Repeat::Queue));
4305 player.shared_state.set_cursor(Some(ids[2]));
4306
4307 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[2]));
4308 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
4309 }
4310
4311 #[test]
4312 fn removing_a_track_the_decoder_queued_takes_it_back() {
4313 let dir = tempfile::tempdir().unwrap();
4314 let (mut player, ids) = queued_wavs(dir.path(), &["a", "b", "c"]);
4315 assert_eq!(player.timeline.queued_after_playhead(), ids[1..]);
4316
4317 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[1]));
4318 assert_eq!(player.playback_starts, 2, "restarted at the playhead");
4319 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
4320 await_queued(&player);
4321 await_queued(&player);
4322 assert_eq!(player.timeline.queued_after_playhead(), vec![ids[2]]);
4323 player.process_command(PlayerCommand::Stop);
4324 }
4325
4326 #[test]
4327 fn a_track_inserted_to_play_next_is_not_skipped() {
4328 let dir = tempfile::tempdir().unwrap();
4329 let (mut player, ids) = queued_wavs(dir.path(), &["a", "b"]);
4330 let path = dir.path().join("next.wav");
4331 crate::test_utils::generate_wav(&path, 8_000, 1, 1.0, 16);
4332 let next = PlaylistItem {
4333 path,
4334 ..make_item("next")
4335 };
4336 let next_id = next.id;
4337
4338 player.process_command(PlayerCommand::InsertInPlaylist {
4339 items: vec![next],
4340 after: ids[0],
4341 });
4342 assert_eq!(player.playback_starts, 2);
4343 for _ in 0..3 {
4344 await_queued(&player);
4345 }
4346 assert_eq!(
4347 player.timeline.queued_after_playhead(),
4348 vec![next_id, ids[1]]
4349 );
4350 player.process_command(PlayerCommand::Stop);
4351 }
4352
4353 #[test]
4354 fn a_track_the_decoder_could_not_open_does_not_make_every_edit_restart() {
4355 let dir = tempfile::tempdir().unwrap();
4356 let mut player = Player::new();
4357 player.backend = Box::new(StuckBackend {
4358 rate: 8_000.0,
4359 asked: Default::default(),
4360 starts: Default::default(),
4361 });
4362 let wav = |name: &str| {
4363 let path = dir.path().join(format!("{name}.wav"));
4364 crate::test_utils::generate_wav(&path, 8_000, 1, 30.0, 16);
4365 PlaylistItem {
4366 path,
4367 ..make_item(name)
4368 }
4369 };
4370 let items = vec![wav("a"), make_item("missing"), wav("c")];
4372 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
4373 player.process_command(PlayerCommand::AddToPlaylist(items));
4374 player.process_command(PlayerCommand::Play(ids[0]));
4375 await_queued(&player);
4376 await_queued(&player);
4377
4378 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("later")]));
4379 assert_eq!(
4380 player.playback_starts, 1,
4381 "nothing the decoder decided changed"
4382 );
4383 assert_eq!(player.timeline.queued_after_playhead(), vec![ids[2]]);
4384 player.process_command(PlayerCommand::Stop);
4385 }
4386
4387 #[test]
4388 fn a_track_added_after_the_decoder_reached_the_end_follows_gaplessly() {
4389 let dir = tempfile::tempdir().unwrap();
4390 let (mut player, ids) = queued_wavs(dir.path(), &["a"]);
4391 wait_for_lookahead(&player);
4392 let path = dir.path().join("b.wav");
4393 crate::test_utils::generate_wav(&path, 8_000, 1, 1.0, 16);
4394 let b = PlaylistItem {
4395 path,
4396 ..make_item("b")
4397 };
4398 let b_id = b.id;
4399
4400 player.process_command(PlayerCommand::AddToPlaylist(vec![b]));
4401 assert_eq!(player.playback_starts, 2, "restarted to queue it");
4402 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
4403 await_queued(&player);
4404 await_queued(&player);
4405 assert_eq!(player.timeline.queued_after_playhead(), vec![b_id]);
4406 player.process_command(PlayerCommand::Stop);
4407 }
4408
4409 #[test]
4410 fn playing_next_a_track_still_downloading_takes_back_the_lookahead() {
4411 let dir = tempfile::tempdir().unwrap();
4412 let (mut player, ids) = queued_wavs(dir.path(), &["a", "b"]);
4413 let remote = pending_item("remote");
4414 let remote_id = remote.id;
4415
4416 player.process_command(PlayerCommand::InsertInPlaylist {
4417 items: vec![remote],
4418 after: ids[0],
4419 });
4420 assert_eq!(player.playback_starts, 2, "b is taken back out of the ring");
4421 await_queued(&player);
4422 wait_for_lookahead(&player);
4423 let session = player.session().unwrap();
4424 let step = session.lookahead.lock().last().cloned().unwrap();
4425 assert_eq!(step.next, Some(remote_id), "the decoder waits for it");
4426 assert!(player.timeline.queued_after_playhead().is_empty());
4427 player.process_command(PlayerCommand::Stop);
4428 }
4429
4430 #[test]
4431 fn gapless_playback_waits_for_a_track_still_downloading() {
4432 let dir = tempfile::tempdir().unwrap();
4433 let mut player = Player::new();
4434 player.backend = Box::new(StuckBackend {
4435 rate: 8_000.0,
4436 asked: Default::default(),
4437 starts: Default::default(),
4438 });
4439 let wav = |name: &str| {
4440 let path = dir.path().join(format!("{name}.wav"));
4441 crate::test_utils::generate_wav(&path, 8_000, 1, 1.0, 16);
4442 PlaylistItem {
4443 path,
4444 ..make_item(name)
4445 }
4446 };
4447 let items = vec![wav("a"), pending_item("arriving"), wav("c")];
4448 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
4449 player.process_command(PlayerCommand::AddToPlaylist(items));
4450 player.process_command(PlayerCommand::Play(ids[0]));
4451 await_queued(&player);
4452 wait_for_lookahead(&player);
4453
4454 assert!(
4455 player.timeline.queued_after_playhead().is_empty(),
4456 "c is not queued over the track before it"
4457 );
4458 player.process_command(PlayerCommand::DecodeFinished(player.session));
4460 assert_eq!(player.shared_state.cursor(), Some(ids[1]));
4461 assert!(player.waiting().is_some());
4462 player.process_command(PlayerCommand::Stop);
4463 }
4464
4465 fn wait_for_lookahead(player: &Player) {
4467 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
4468 while player
4469 .session()
4470 .is_some_and(|s| s.lookahead.lock().is_empty())
4471 {
4472 assert!(
4473 std::time::Instant::now() < deadline,
4474 "the decoder never looked ahead"
4475 );
4476 std::thread::yield_now();
4477 }
4478 }
4479
4480 #[test]
4481 fn an_edit_after_what_the_decoder_queued_leaves_playback_alone() {
4482 let dir = tempfile::tempdir().unwrap();
4483 let (mut player, ids) = wavs_playing(dir.path(), &["a", "b"], 30.0);
4484 await_queued(&player);
4485 await_queued(&player);
4486 let path = dir.path().join("later.wav");
4487 crate::test_utils::generate_wav(&path, 8_000, 1, 1.0, 16);
4488
4489 player.process_command(PlayerCommand::AddToPlaylist(vec![PlaylistItem {
4490 path,
4491 ..make_item("later")
4492 }]));
4493 assert_eq!(player.playback_starts, 1);
4494 assert_eq!(player.timeline.queued_after_playhead(), vec![ids[1]]);
4495 player.process_command(PlayerCommand::Stop);
4496 }
4497
4498 #[test]
4499 fn undoing_the_add_of_a_track_on_its_way_forgets_it() {
4500 let dir = tempfile::tempdir().unwrap();
4501 let (mut player, _, starts) = downloading_wav(dir.path());
4502 let path = dir.path().join("added.wav");
4503 crate::test_utils::generate_wav(&path, 8_000, 1, 1.0, 16);
4504 let added = PlaylistItem {
4505 path,
4506 state: ItemState::Pending,
4507 ..make_item("added")
4508 };
4509 let added_id = added.id;
4510 player.process_command(PlayerCommand::AddToPlaylist(vec![added]));
4511 player.process_command(PlayerCommand::Play(added_id));
4512
4513 player.process_command(PlayerCommand::Undo);
4514 assert!(player.waiting().is_none());
4515 assert_eq!(player.shared_state.playback_state(), PlaybackState::Stopped);
4516
4517 player.process_command(PlayerCommand::Redo);
4519 player
4520 .shared_state
4521 .update_item_state(added_id, ItemState::Ready);
4522 player.process_command(PlayerCommand::TrackReady(added_id));
4523 assert!(player.session().is_none());
4524 assert_eq!(starts.load(Ordering::Relaxed), 0);
4525 }
4526
4527 pub(super) struct Rng(pub(super) u64);
4530
4531 impl Rng {
4532 pub(super) fn next(&mut self) -> u64 {
4533 self.0 ^= self.0 << 13;
4534 self.0 ^= self.0 >> 7;
4535 self.0 ^= self.0 << 17;
4536 self.0
4537 }
4538
4539 pub(super) fn below(&mut self, n: usize) -> usize {
4540 (self.next() % n.max(1) as u64) as usize
4541 }
4542
4543 pub(super) fn coin(&mut self) -> bool {
4544 self.next() & 1 == 1
4545 }
4546 }
4547
4548 pub(super) fn asks_to_play(cmd: &PlayerCommand) -> bool {
4551 cmd.asks_to_play()
4552 }
4553
4554 pub(super) fn check_invariants(
4557 player: &Player,
4558 wanted_before: bool,
4559 asked: bool,
4560 ) -> Result<(), String> {
4561 let state = &player.shared_state;
4562 let ids = playlist_ids(player);
4563 let listed = |id: QueueItemId| ids.contains(&id);
4564 let mut seen = std::collections::HashSet::new();
4565 if !ids.iter().all(|id| seen.insert(*id)) {
4566 return Err("an item is in the playlist twice".into());
4567 }
4568 let cursor = state.cursor();
4569 if cursor.is_some_and(|c| !listed(c)) {
4570 return Err("the cursor is on an item not in the playlist".into());
4571 }
4572 let info = state.track_info();
4573 match (&player.transport, &info) {
4574 (Transport::Loaded(_), None) => return Err("loaded, but no track published".into()),
4575 (Transport::Loaded(_), Some(info)) => {
4576 if !listed(info.id) {
4577 return Err("playing an item not in the playlist".into());
4578 }
4579 if cursor != Some(info.id) {
4580 return Err("the cursor is not on what is playing".into());
4581 }
4582 if !matches!(
4583 state.playback_state(),
4584 PlaybackState::Playing | PlaybackState::Paused
4585 ) {
4586 return Err("loaded, but published as stopped".into());
4587 }
4588 }
4589 (_, Some(_)) => return Err("a track is published with nothing loaded".into()),
4590 (Transport::Waiting(w), None) => {
4591 if cursor != Some(w.id) || !listed(w.id) {
4592 return Err("waiting on an item that is not the cursor's".into());
4593 }
4594 let expected = match w.start {
4595 Run::Playing => PlaybackState::Stopped,
4596 Run::Paused => PlaybackState::Paused,
4597 };
4598 if state.playback_state() != expected {
4599 return Err("a wait published in the wrong state".into());
4600 }
4601 }
4602 (Transport::Idle, None) => {
4603 if state.playback_state() != PlaybackState::Stopped {
4604 return Err("idle, but not published as stopped".into());
4605 }
4606 }
4607 }
4608 if state.is_waiting() != player.waiting().is_some() {
4609 return Err("the published wait disagrees with the player".into());
4610 }
4611 if player
4612 .timeline
4613 .queued_after_playhead()
4614 .into_iter()
4615 .any(|id| !listed(id))
4616 {
4617 return Err("the decoder has queued an item no longer in the playlist".into());
4618 }
4619 if !asked && !wanted_before && state.wants_to_play() {
4620 return Err("started without being asked".into());
4621 }
4622 if state.play_mode() != player.mode {
4623 return Err("the published mode disagrees with the player".into());
4624 }
4625 if state.is_shuffled() != player.mode.shuffle {
4626 return Err("a play order kept, or not, against the mode".into());
4627 }
4628 if player.mode.repeat == Repeat::Off
4629 && let Some(session) = player.session()
4630 && let Some(playhead) = player.timeline.playhead()
4631 && session.track.id == playhead.id
4632 && session
4633 .lookahead
4634 .lock()
4635 .iter()
4636 .filter(|step| step.boundary > playhead.boundary)
4637 .any(|step| step.wrapped || step.next == Some(step.after))
4638 {
4639 return Err("repeat is off, but the decoder has queued a wrap".into());
4640 }
4641 Ok(())
4642 }
4643
4644 #[test]
4645 fn random_use_keeps_the_player_honest() {
4646 let dir = tempfile::tempdir().unwrap();
4647 let path = dir.path().join("t.wav");
4648 crate::test_utils::generate_wav(&path, 8_000, 1, 0.5, 16);
4649
4650 for seed in 1..=40u64 {
4651 let mut rng = Rng(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1);
4652 let mut player = Player::new();
4653 player.backend = Box::new(StuckBackend {
4654 rate: 8_000.0,
4655 asked: Default::default(),
4656 starts: Default::default(),
4657 });
4658 let fresh = |rng: &mut Rng| {
4659 let state = match rng.below(3) {
4660 0 => ItemState::Pending,
4661 _ => ItemState::Ready,
4662 };
4663 PlaylistItem {
4664 path: path.clone(),
4665 state,
4666 ..make_item("t")
4667 }
4668 };
4669 let start: Vec<_> = (0..5).map(|_| fresh(&mut rng)).collect();
4670 player.process_command(PlayerCommand::AddToPlaylist(start));
4671
4672 let mut history = Vec::new();
4673 for step in 0..150 {
4674 let ids = playlist_ids(&player);
4675 let pick = |rng: &mut Rng| ids.get(rng.below(ids.len())).copied();
4676 let cmd = match rng.below(23) {
4677 0 => pick(&mut rng).map(PlayerCommand::Play),
4678 1 => pick(&mut rng).map(|id| PlayerCommand::Cue {
4679 id,
4680 position_ms: if rng.coin() { 0 } else { 200 },
4681 play: rng.coin(),
4682 }),
4683 2 => Some(PlayerCommand::Pause),
4684 3 => Some(PlayerCommand::Resume),
4685 4 => Some(PlayerCommand::NextTrack),
4686 5 => Some(PlayerCommand::PrevTrack),
4687 6 => Some(PlayerCommand::Seek(100)),
4688 7 => pick(&mut rng).map(PlayerCommand::RemoveFromPlaylist),
4689 8 => Some(PlayerCommand::RemoveFromPlaylistBatch(
4690 (0..2).filter_map(|_| pick(&mut rng)).collect(),
4691 )),
4692 9 => pick(&mut rng).zip(pick(&mut rng)).map(|(id, target)| {
4693 PlayerCommand::MoveInPlaylist {
4694 id,
4695 target,
4696 after: rng.coin(),
4697 }
4698 }),
4699 10 => pick(&mut rng).map(|after| PlayerCommand::InsertInPlaylist {
4700 items: vec![fresh(&mut rng)],
4701 after,
4702 }),
4703 11 => Some(PlayerCommand::AddToPlaylist(vec![fresh(&mut rng)])),
4704 12 => Some(PlayerCommand::Undo),
4705 13 => Some(PlayerCommand::Redo),
4706 14 | 15 => {
4707 let (items, _) = player.shared_state.snapshot_playlist();
4708 let pending: Vec<_> = items
4709 .iter()
4710 .filter(|i| matches!(i.state, ItemState::Pending))
4711 .map(|i| i.id)
4712 .collect();
4713 pending.get(rng.below(pending.len())).map(|&id| {
4714 if rng.below(4) == 0 {
4715 player
4716 .shared_state
4717 .update_item_state(id, ItemState::Failed("gone".into()));
4718 PlayerCommand::TrackFailed(id)
4719 } else {
4720 player.shared_state.update_item_state(id, ItemState::Ready);
4721 PlayerCommand::TrackReady(id)
4722 }
4723 })
4724 }
4725 16 => Some(PlayerCommand::DecodeFinished(player.session)),
4726 17 => Some(PlayerCommand::DecodeFinished(
4727 player.session.wrapping_sub(1),
4728 )),
4729 18 => Some(PlayerCommand::ClearPlaylist),
4730 20 => Some(PlayerCommand::SetShuffle(rng.coin())),
4731 21 => Some(PlayerCommand::SetRepeat(
4732 [Repeat::Off, Repeat::Queue, Repeat::One][rng.below(3)],
4733 )),
4734 _ => Some(PlayerCommand::ReplacePlaylist {
4735 items: (0..3).map(|_| fresh(&mut rng)).collect(),
4736 start: rng.below(4),
4737 position_ms: if rng.coin() { 0 } else { 200 },
4738 play: rng.coin(),
4739 mode: [QueueMode::Keep, QueueMode::InOrder, QueueMode::Shuffled]
4740 [rng.below(3)],
4741 }),
4742 };
4743 let Some(cmd) = cmd else { continue };
4744 let label = format!("{cmd:?}");
4745 let asked = asks_to_play(&cmd);
4746 let wanted_before = player.shared_state.wants_to_play();
4747 player.process_command(cmd);
4748 while let Ok(sent) = player.commands.rx.try_recv() {
4752 player.process_command(sent);
4753 }
4754 player.update_playback_state();
4755 history.push(label);
4756 if let Err(broken) = check_invariants(&player, wanted_before, asked) {
4757 let tail = history[history.len().saturating_sub(8)..].join("\n ");
4758 panic!("seed {seed}, step {step}: {broken}\nlast commands:\n {tail}");
4759 }
4760 }
4761 player.process_command(PlayerCommand::Stop);
4762 }
4763 }
4764
4765 struct QuickEngine {
4766 silent: Arc<std::sync::atomic::AtomicBool>,
4767 quick_outs: Arc<std::sync::atomic::AtomicUsize>,
4768 outs: Arc<std::sync::atomic::AtomicUsize>,
4769 period: std::time::Duration,
4770 }
4771 impl AudioEngineHandle for QuickEngine {
4772 fn start(&self) -> Result<(), BackendError> {
4773 Ok(())
4774 }
4775 fn stop(&self) -> Result<(), BackendError> {
4776 Ok(())
4777 }
4778 fn is_running(&self) -> bool {
4779 true
4780 }
4781 fn fade_out(&self) {
4782 self.outs.fetch_add(1, Ordering::Relaxed);
4783 }
4784 fn fade_out_quickly(&self) {
4785 self.quick_outs.fetch_add(1, Ordering::Relaxed);
4786 }
4787 fn fade_in(&self) -> Result<(), BackendError> {
4788 Ok(())
4789 }
4790 fn is_silent(&self) -> bool {
4791 self.silent.load(Ordering::Relaxed)
4792 }
4793 fn period(&self) -> std::time::Duration {
4794 self.period
4795 }
4796 }
4797
4798 fn quick_player(
4801 frames: u32,
4802 rate: u32,
4803 ) -> (
4804 Player,
4805 Arc<std::sync::atomic::AtomicBool>,
4806 Arc<std::sync::atomic::AtomicUsize>,
4807 Arc<std::sync::atomic::AtomicUsize>,
4808 ) {
4809 let silent = Arc::new(std::sync::atomic::AtomicBool::new(false));
4810 let quick_outs = Arc::new(std::sync::atomic::AtomicUsize::new(0));
4811 let outs = Arc::new(std::sync::atomic::AtomicUsize::new(0));
4812 let mut player = Player::new();
4813 player.transport = Transport::Loaded(test_session(
4814 QueueItemId::new(),
4815 Box::new(QuickEngine {
4816 silent: silent.clone(),
4817 quick_outs: quick_outs.clone(),
4818 outs: outs.clone(),
4819 period: std::time::Duration::from_secs_f64(frames as f64 / rate as f64),
4820 }),
4821 ));
4822 (player, silent, quick_outs, outs)
4823 }
4824
4825 #[test]
4829 fn a_dsp_change_restarts_after_a_quick_fade_and_coalesces() {
4830 let (mut player, silent, quick_outs, outs) = quick_player(512, 44100);
4831
4832 player.restart_for_dsp();
4833 assert_eq!(quick_outs.load(Ordering::Relaxed), 1);
4834 assert_eq!(outs.load(Ordering::Relaxed), 0, "not the pause's fade");
4835
4836 player.restart_for_dsp();
4838 assert_eq!(quick_outs.load(Ordering::Relaxed), 1);
4839 player.update_playback_state();
4840 assert!(player.dsp_restart.is_some(), "not restarted while audible");
4841
4842 silent.store(true, Ordering::Relaxed);
4843 player.update_playback_state();
4844 assert!(player.dsp_restart.is_none(), "restarted once silent");
4845 assert!(
4846 !player.quick_start,
4847 "the quick start is the restart's alone"
4848 );
4849 }
4850
4851 #[test]
4856 fn a_dsp_change_wakes_a_buffer_period_after_its_fade() {
4857 use crate::audio::fade::QUICK_FADE;
4858 for (frames, rate) in [(512, 44100), (1024, 48000)] {
4859 let period = std::time::Duration::from_secs_f64(frames as f64 / rate as f64);
4860 let (mut player, silent, _, _) = quick_player(frames, rate);
4861 player.restart_for_dsp();
4862 let since = player.dsp_restart.expect("waiting on the fade");
4863
4864 let first = player.next_event().expect("a wake when the fade ends");
4865 assert_eq!(first, since + QUICK_FADE + period + BOUNDARY_SLACK);
4866 assert_eq!(dsp_wake(since, period, since), first);
4867
4868 player.update_playback_state();
4871 assert!(player.dsp_restart.is_some());
4872 let second = dsp_wake(since, period, first);
4873 assert_eq!(second, first + period);
4874 assert!(
4875 second < since + QUICK_FADE_LIMIT,
4876 "{frames} frames: inside the limit"
4877 );
4878 assert_eq!(dsp_wake(since, period, second), since + QUICK_FADE_LIMIT);
4879
4880 silent.store(true, Ordering::Relaxed);
4881 player.update_playback_state();
4882 assert!(
4883 player.dsp_restart.is_none(),
4884 "{frames} frames: restarted at silence"
4885 );
4886 }
4887 }
4888
4889 #[test]
4890 fn a_pause_reports_where_the_fade_went_silent() {
4891 use std::sync::atomic::AtomicBool;
4892
4893 struct FadingEngine {
4894 running: Arc<AtomicBool>,
4895 silent: Arc<AtomicBool>,
4896 }
4897 impl AudioEngineHandle for FadingEngine {
4898 fn start(&self) -> Result<(), BackendError> {
4899 self.running.store(true, Ordering::Relaxed);
4900 Ok(())
4901 }
4902 fn stop(&self) -> Result<(), BackendError> {
4903 self.running.store(false, Ordering::Relaxed);
4904 Ok(())
4905 }
4906 fn is_running(&self) -> bool {
4907 self.running.load(Ordering::Relaxed)
4908 }
4909 fn fade_out(&self) {}
4910 fn fade_in(&self) -> Result<(), BackendError> {
4911 Ok(())
4912 }
4913 fn is_silent(&self) -> bool {
4914 self.silent.load(Ordering::Relaxed)
4915 }
4916 }
4917
4918 let running = Arc::new(AtomicBool::new(true));
4919 let silent = Arc::new(AtomicBool::new(false));
4920 let mut player = Player::new();
4921 player.transport = Transport::Loaded(test_session(
4922 QueueItemId::new(),
4923 Box::new(FadingEngine {
4924 running: running.clone(),
4925 silent: silent.clone(),
4926 }),
4927 ));
4928 player
4929 .shared_state
4930 .set_playback_state(PlaybackState::Playing);
4931 player.shared_state.set_position_ms(5_000);
4932
4933 let (reply, answer) = crossbeam_channel::bounded(1);
4934 player.process_command(PlayerCommand::PauseAndReport(reply));
4935
4936 if crate::config::Config::cached().playback.fade_on_pause {
4937 assert!(answer.try_recv().is_err(), "not while the fade is audible");
4938 player.shared_state.set_position_ms(5_150);
4940 player.update_playback_state();
4941 assert!(answer.try_recv().is_err());
4942 silent.store(true, Ordering::Relaxed);
4943 player.update_playback_state();
4944 assert!(!running.load(Ordering::Relaxed));
4945 assert_eq!(answer.try_recv().unwrap(), 5_150);
4946 } else {
4947 assert_eq!(answer.try_recv().unwrap(), 5_000);
4948 }
4949 }
4950
4951 #[test]
4952 fn the_server_hears_each_turn_playback_takes() {
4953 use PlaybackReportState::{Paused, Playing, Stopped};
4954
4955 let dir = tempfile::tempdir().unwrap();
4956 let path = dir.path().join("t.wav");
4957 crate::test_utils::generate_wav(&path, 8_000, 1, 10.0, 16);
4958
4959 let mut player = Player::new();
4960 player.backend = Box::new(StuckBackend {
4961 rate: 8_000.0,
4962 asked: Default::default(),
4963 starts: Default::default(),
4964 });
4965 let (recorder, events) = PlayRecorder::capture();
4966 player.history = Some(recorder);
4967
4968 let item = PlaylistItem {
4969 db_id: Some(5),
4970 path,
4971 ..make_item("t")
4972 };
4973 let id = item.id;
4974 player.process_command(PlayerCommand::AddToPlaylist(vec![item]));
4975 player.process_command(PlayerCommand::Play(id));
4976 player.process_command(PlayerCommand::Pause);
4977 player.process_command(PlayerCommand::Seek(4_000));
4978 await_queued(&player);
4983 player.process_command(PlayerCommand::Resume);
4984 player.process_command(PlayerCommand::Stop);
4985
4986 let events: Vec<_> = events.try_iter().collect();
4987 let near_seek = |position_ms: u64| (3_750..=4_000).contains(&position_ms);
4988 let reports: Vec<_> = events
4989 .iter()
4990 .filter_map(|e| match e {
4991 PlayEvent::Playback(r) => Some((r.track_id, r.state, r.position_ms)),
4992 _ => None,
4993 })
4994 .collect();
4995 assert_eq!(
4996 events.first(),
4997 Some(&PlayEvent::Started {
4998 track_id: 5,
4999 position_ms: 0
5000 })
5001 );
5002 assert_eq!(reports.len(), 4, "{events:?}");
5003 assert_eq!(reports[0], (5, Paused, 0));
5004 for (report, state) in reports[1..].iter().zip([Paused, Playing, Stopped]) {
5005 assert_eq!((report.0, report.1), (5, state), "{events:?}");
5006 assert!(near_seek(report.2), "{events:?}");
5007 }
5008 assert_eq!(
5009 events.last(),
5010 Some(&PlayEvent::Finished {
5011 track_id: 5,
5012 listened_ms: 0
5013 })
5014 );
5015 }
5016
5017 #[test]
5018 fn removing_the_playing_track_resumes_at_its_successor() {
5019 let mut player = Player::new();
5020 let ids = seed(&mut player, 5);
5021 pretend_playing(&mut player, ids[2]);
5022
5023 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[2]));
5024
5025 assert_eq!(
5026 player.shared_state.cursor(),
5027 Some(ids[3]),
5028 "playback must continue at the next track, not restart the queue"
5029 );
5030 assert_eq!(player.playback_starts, 1);
5031 }
5032
5033 #[test]
5034 fn removing_the_paused_track_moves_on_paused() {
5035 let mut player = Player::new();
5036 let ids = seed(&mut player, 3);
5037 pretend_playing(&mut player, ids[1]);
5038 if let Transport::Loaded(session) = &mut player.transport {
5039 session.run = Run::Paused;
5040 }
5041
5042 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[1]));
5043 assert_eq!(player.shared_state.cursor(), Some(ids[2]));
5044 assert!(!player.shared_state.wants_to_play(), "still paused");
5045 }
5046
5047 #[test]
5048 fn removing_the_cursor_with_nothing_loaded_starts_nothing() {
5049 let mut player = Player::new();
5050 let ids = seed(&mut player, 3);
5051 player.shared_state.set_cursor(Some(ids[1]));
5052
5053 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[1]));
5054 assert_eq!(player.shared_state.cursor(), Some(ids[2]));
5055 assert_eq!(player.playback_starts, 0);
5056 }
5057
5058 #[test]
5059 fn removing_the_first_playing_track_resumes_at_the_new_first() {
5060 let mut player = Player::new();
5061 let ids = seed(&mut player, 3);
5062 pretend_playing(&mut player, ids[0]);
5063
5064 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[0]));
5065
5066 assert_eq!(player.shared_state.cursor(), Some(ids[1]));
5067 }
5068
5069 #[test]
5070 fn next_track_parks_on_a_track_that_has_not_downloaded_yet() {
5071 let mut player = Player::new();
5072 let playing = make_item("playing");
5073 let waiting = pending_item("waiting");
5074 let later = make_item("later");
5075 let (playing_id, waiting_id) = (playing.id, waiting.id);
5076 player.process_command(PlayerCommand::AddToPlaylist(vec![playing, waiting, later]));
5077 pretend_playing(&mut player, playing_id);
5078
5079 player.process_command(PlayerCommand::DecodeFinished(player.session));
5080
5081 assert_eq!(
5082 player.shared_state.cursor(),
5083 Some(waiting_id),
5084 "the cursor parks on the track being fetched"
5085 );
5086 assert_eq!(
5087 player.playback_starts, 0,
5088 "nothing to play until its bytes land"
5089 );
5090
5091 player
5094 .shared_state
5095 .update_item_state(waiting_id, ItemState::Ready);
5096 player.process_command(PlayerCommand::TrackReady(waiting_id));
5097
5098 assert_eq!(player.playback_starts, 1);
5099 assert_eq!(player.shared_state.cursor(), Some(waiting_id));
5100 }
5101
5102 #[test]
5106 fn an_album_starts_at_its_first_track_however_much_is_cached() {
5107 let mut player = Player::new();
5108 let remote = pending_item("remote");
5109 let cached = [make_item("cached 2"), make_item("cached 3")];
5110 let first = remote.id;
5111 player.process_command(PlayerCommand::ReplacePlaylist {
5112 items: [vec![remote], cached.to_vec()].concat(),
5113 start: 0,
5114 position_ms: 0,
5115 play: true,
5116 mode: QueueMode::Keep,
5117 });
5118
5119 assert_eq!(player.shared_state.cursor(), Some(first));
5120 assert_eq!(player.playback_starts, 0, "nothing decodes before it lands");
5121
5122 player
5123 .shared_state
5124 .update_item_state(first, ItemState::Ready);
5125 player.process_command(PlayerCommand::TrackReady(first));
5126 assert_eq!(player.playback_starts, 1);
5127 assert_eq!(
5128 player.shared_state.cursor(),
5129 Some(first),
5130 "the first track decodes first"
5131 );
5132 }
5133
5134 #[test]
5137 fn a_stream_that_ends_unheard_mid_download_waits_for_its_track() {
5138 let mut player = Player::new();
5139 let streaming = pending_item("streaming");
5140 let next = make_item("next");
5141 let id = streaming.id;
5142 player.process_command(PlayerCommand::AddToPlaylist(vec![streaming, next]));
5143 pretend_playing(&mut player, id);
5144
5145 player.process_command(PlayerCommand::DecodeFinished(player.session));
5146 assert_eq!(player.shared_state.cursor(), Some(id), "not passed over");
5147 assert_eq!(player.playback_starts, 0);
5148
5149 player.shared_state.update_item_state(id, ItemState::Ready);
5150 player.process_command(PlayerCommand::TrackReady(id));
5151 assert_eq!(player.playback_starts, 1);
5152 assert_eq!(player.shared_state.cursor(), Some(id));
5153 }
5154
5155 #[test]
5156 fn a_download_that_cannot_land_moves_the_cursor_on() {
5157 let mut player = Player::new();
5158 let waiting = pending_item("waiting");
5159 let later = make_item("later");
5160 let (waiting_id, later_id) = (waiting.id, later.id);
5161 player.process_command(PlayerCommand::AddToPlaylist(vec![waiting, later]));
5162
5163 player.process_command(PlayerCommand::Play(waiting_id));
5164 assert_eq!(player.playback_starts, 0, "nothing to play yet");
5165
5166 player
5168 .shared_state
5169 .update_item_state(waiting_id, ItemState::Failed("remote unavailable".into()));
5170 player.process_command(PlayerCommand::TrackFailed(waiting_id));
5171
5172 assert_eq!(
5173 player.shared_state.cursor(),
5174 Some(later_id),
5175 "the queue moves past a track that can never load"
5176 );
5177 assert_eq!(player.playback_starts, 1);
5178 }
5179
5180 #[test]
5181 fn a_queue_that_can_never_load_stops_rather_than_waiting() {
5182 let mut player = Player::new();
5183 let first = pending_item("first");
5184 let second = pending_item("second");
5185 let (first_id, second_id) = (first.id, second.id);
5186 player.process_command(PlayerCommand::AddToPlaylist(vec![first, second]));
5187
5188 player.process_command(PlayerCommand::Play(first_id));
5189 for id in [first_id, second_id] {
5190 player
5191 .shared_state
5192 .update_item_state(id, ItemState::Failed("remote unavailable".into()));
5193 player.process_command(PlayerCommand::TrackFailed(id));
5194 }
5195
5196 assert_eq!(player.playback_starts, 0);
5197 assert_eq!(
5198 player.shared_state.playback_state(),
5199 PlaybackState::Stopped,
5200 "a stop the UI can see, not an indefinite wait for TrackReady"
5201 );
5202 }
5203
5204 #[test]
5205 fn a_failure_elsewhere_in_the_queue_leaves_the_cursor_alone() {
5206 let mut player = Player::new();
5207 let waiting = pending_item("waiting");
5208 let other = pending_item("other");
5209 let (waiting_id, other_id) = (waiting.id, other.id);
5210 player.process_command(PlayerCommand::AddToPlaylist(vec![waiting, other]));
5211 player.process_command(PlayerCommand::Play(waiting_id));
5212
5213 player
5214 .shared_state
5215 .update_item_state(other_id, ItemState::Failed("remote unavailable".into()));
5216 player.process_command(PlayerCommand::TrackFailed(other_id));
5217
5218 assert_eq!(
5219 player.shared_state.cursor(),
5220 Some(waiting_id),
5221 "a track still downloading keeps the cursor"
5222 );
5223 }
5224
5225 #[test]
5226 fn batch_delete_containing_the_cursor_restarts_the_engine_once() {
5227 let mut player = Player::new();
5228 let ids = seed(&mut player, 5);
5229 pretend_playing(&mut player, ids[2]);
5230
5231 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![
5232 ids[1], ids[2], ids[3],
5233 ]));
5234
5235 assert_eq!(playlist_titles(&player), vec!["t0", "t4"]);
5236 assert_eq!(player.shared_state.cursor(), Some(ids[4]));
5237 assert_eq!(
5238 player.playback_starts, 1,
5239 "one resume for the whole selection, not one per deleted track"
5240 );
5241 }
5242
5243 #[test]
5244 fn batch_delete_below_the_cursor_leaves_playback_alone() {
5245 let mut player = Player::new();
5246 let ids = seed(&mut player, 4);
5247 player.shared_state.set_cursor(Some(ids[0]));
5248
5249 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![ids[2], ids[3]]));
5250
5251 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
5252 assert_eq!(player.playback_starts, 0);
5253 }
5254
5255 #[test]
5256 fn undo_of_a_batch_delete_restores_the_original_order() {
5257 let mut player = Player::new();
5261 let items = vec![
5262 make_item("A"),
5263 make_item("B"),
5264 make_item("C"),
5265 make_item("D"),
5266 ];
5267 let (b_id, c_id) = (items[1].id, items[2].id);
5268 player.process_command(PlayerCommand::AddToPlaylist(items));
5269
5270 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![c_id, b_id]));
5271 assert_eq!(playlist_titles(&player), vec!["A", "D"]);
5272
5273 player.process_command(PlayerCommand::Undo);
5274 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
5275 }
5276
5277 #[test]
5280 fn undo_add_removes_items() {
5281 let mut player = Player::new();
5282 let items = vec![make_item("A"), make_item("B")];
5283 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
5284
5285 player.process_command(PlayerCommand::AddToPlaylist(items));
5286 assert_eq!(playlist_ids(&player), ids);
5287 assert!(player.undo_stack().can_undo());
5288
5289 player.process_command(PlayerCommand::Undo);
5290 assert!(playlist_ids(&player).is_empty());
5291 assert!(player.undo_stack().can_redo());
5292 }
5293
5294 #[test]
5295 fn redo_add_restores_items() {
5296 let mut player = Player::new();
5297 let items = vec![make_item("A"), make_item("B")];
5298
5299 player.process_command(PlayerCommand::AddToPlaylist(items));
5300 player.process_command(PlayerCommand::Undo);
5301 assert!(playlist_ids(&player).is_empty());
5302
5303 player.process_command(PlayerCommand::Redo);
5304 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
5305 }
5306
5307 #[test]
5310 fn undo_remove_restores_item_at_position() {
5311 let mut player = Player::new();
5312 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5313 let b_id = items[1].id;
5314
5315 player.process_command(PlayerCommand::AddToPlaylist(items));
5316 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
5317 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
5318
5319 player.process_command(PlayerCommand::Undo);
5320 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5321 }
5322
5323 #[test]
5324 fn undo_remove_first_item() {
5325 let mut player = Player::new();
5326 let items = vec![make_item("A"), make_item("B")];
5327 let a_id = items[0].id;
5328
5329 player.process_command(PlayerCommand::AddToPlaylist(items));
5330 player.process_command(PlayerCommand::RemoveFromPlaylist(a_id));
5331 assert_eq!(playlist_titles(&player), vec!["B"]);
5332
5333 player.process_command(PlayerCommand::Undo);
5334 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
5335 }
5336
5337 #[test]
5338 fn undo_batch_remove_restores_all() {
5339 let mut player = Player::new();
5340 let items = vec![
5341 make_item("A"),
5342 make_item("B"),
5343 make_item("C"),
5344 make_item("D"),
5345 ];
5346 let b_id = items[1].id;
5347 let c_id = items[2].id;
5348
5349 player.process_command(PlayerCommand::AddToPlaylist(items));
5350 let version_before = player.shared_state.playlist_version();
5351 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![b_id, c_id]));
5352 assert_eq!(playlist_titles(&player), vec!["A", "D"]);
5353 assert_eq!(
5356 player.shared_state.playlist_version(),
5357 version_before + 1,
5358 "batch removal must bump the playlist version exactly once"
5359 );
5360
5361 player.process_command(PlayerCommand::Undo);
5363 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
5364 }
5365
5366 #[test]
5367 fn redo_batch_remove() {
5368 let mut player = Player::new();
5369 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5370 let a_id = items[0].id;
5371 let b_id = items[1].id;
5372
5373 player.process_command(PlayerCommand::AddToPlaylist(items));
5374 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![a_id, b_id]));
5375 player.process_command(PlayerCommand::Undo);
5376 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5377
5378 player.process_command(PlayerCommand::Redo);
5379 assert_eq!(playlist_titles(&player), vec!["C"]);
5380 }
5381
5382 #[test]
5383 fn redo_remove() {
5384 let mut player = Player::new();
5385 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5386 let b_id = items[1].id;
5387
5388 player.process_command(PlayerCommand::AddToPlaylist(items));
5389 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
5390 player.process_command(PlayerCommand::Undo);
5391 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5392
5393 player.process_command(PlayerCommand::Redo);
5394 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
5395 }
5396
5397 #[test]
5400 fn undo_insert_removes_inserted_items() {
5401 let mut player = Player::new();
5402 let items = vec![make_item("A"), make_item("C")];
5403 let a_id = items[0].id;
5404
5405 player.process_command(PlayerCommand::AddToPlaylist(items));
5406
5407 let inserted = vec![make_item("B")];
5408 player.process_command(PlayerCommand::InsertInPlaylist {
5409 items: inserted,
5410 after: a_id,
5411 });
5412 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5413
5414 player.process_command(PlayerCommand::Undo);
5415 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
5416 }
5417
5418 #[test]
5421 fn undo_move_restores_position() {
5422 let mut player = Player::new();
5423 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5424 let a_id = items[0].id;
5425 let c_id = items[2].id;
5426
5427 player.process_command(PlayerCommand::AddToPlaylist(items));
5428
5429 player.process_command(PlayerCommand::MoveInPlaylist {
5431 id: a_id,
5432 target: c_id,
5433 after: true,
5434 });
5435 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
5436
5437 player.process_command(PlayerCommand::Undo);
5438 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5439 }
5440
5441 #[test]
5442 fn redo_move() {
5443 let mut player = Player::new();
5444 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5445 let a_id = items[0].id;
5446 let c_id = items[2].id;
5447
5448 player.process_command(PlayerCommand::AddToPlaylist(items));
5449 player.process_command(PlayerCommand::MoveInPlaylist {
5450 id: a_id,
5451 target: c_id,
5452 after: true,
5453 });
5454 player.process_command(PlayerCommand::Undo);
5455 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5456
5457 player.process_command(PlayerCommand::Redo);
5458 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
5459 }
5460
5461 #[test]
5464 fn undo_batch_move() {
5465 let mut player = Player::new();
5466 let items = vec![
5467 make_item("A"),
5468 make_item("B"),
5469 make_item("C"),
5470 make_item("D"),
5471 ];
5472 let a_id = items[0].id;
5473 let b_id = items[1].id;
5474 let d_id = items[3].id;
5475
5476 player.process_command(PlayerCommand::AddToPlaylist(items));
5477
5478 player.process_command(PlayerCommand::MoveItemsInPlaylist {
5480 ids: vec![a_id, b_id],
5481 target: d_id,
5482 after: true,
5483 });
5484 assert_eq!(playlist_titles(&player), vec!["C", "D", "A", "B"]);
5485
5486 player.process_command(PlayerCommand::Undo);
5487 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
5488 }
5489
5490 #[test]
5493 fn undo_clear_restores_playlist() {
5494 let mut player = Player::new();
5495 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5496
5497 player.process_command(PlayerCommand::AddToPlaylist(items));
5498 player.process_command(PlayerCommand::ClearPlaylist);
5499 assert!(playlist_ids(&player).is_empty());
5500
5501 player.process_command(PlayerCommand::Undo);
5502 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5503 }
5504
5505 #[test]
5510 fn undoing_a_replace_does_not_leave_the_engine_on_an_orphaned_track() {
5511 let mut player = Player::new();
5512 let original = seed(&mut player, 3);
5513 player.shared_state.set_cursor(Some(original[0]));
5514 pretend_playing(&mut player, original[0]);
5515
5516 let replacement = vec![make_item("something else")];
5517 let orphan = replacement[0].id;
5518 player.process_command(PlayerCommand::ReplacePlaylist {
5519 items: replacement,
5520 start: 0,
5521 position_ms: 0,
5522 play: true,
5523 mode: QueueMode::Keep,
5524 });
5525 pretend_playing(&mut player, orphan);
5527
5528 player.process_command(PlayerCommand::Undo);
5529
5530 assert_eq!(playlist_ids(&player), original, "the queue comes back");
5531 assert!(
5532 player.shared_state.get_item(orphan).is_none(),
5533 "and the replacement is gone from it"
5534 );
5535 assert!(
5536 playing_id(&player).is_none_or(|id| player.shared_state.get_item(id).is_some()),
5537 "so nothing may still be playing out of it"
5538 );
5539 }
5540
5541 #[test]
5543 fn undoing_an_add_does_not_leave_the_engine_on_a_removed_track() {
5544 let mut player = Player::new();
5545 seed(&mut player, 2);
5546 let added = seed(&mut player, 1);
5547 pretend_playing(&mut player, added[0]);
5548
5549 player.process_command(PlayerCommand::Undo);
5550
5551 assert!(player.shared_state.get_item(added[0]).is_none());
5552 assert!(
5553 playing_id(&player).is_none_or(|id| player.shared_state.get_item(id).is_some()),
5554 "the engine cannot be left on the item the undo removed"
5555 );
5556 }
5557
5558 #[test]
5560 fn undoing_a_move_leaves_playback_alone() {
5561 let mut player = Player::new();
5562 let ids = seed(&mut player, 3);
5563 player.shared_state.set_cursor(Some(ids[0]));
5564 pretend_playing(&mut player, ids[0]);
5565 let starts = player.playback_starts;
5566
5567 player.process_command(PlayerCommand::MoveInPlaylist {
5568 id: ids[2],
5569 target: ids[0],
5570 after: false,
5571 });
5572 player.process_command(PlayerCommand::Undo);
5573
5574 assert_eq!(playlist_ids(&player), ids);
5575 assert_eq!(playing_id(&player), Some(ids[0]), "still on the same track");
5576 assert_eq!(player.playback_starts, starts, "and not restarted");
5577 }
5578
5579 #[test]
5580 fn redo_clear() {
5581 let mut player = Player::new();
5582 let items = vec![make_item("A"), make_item("B")];
5583
5584 player.process_command(PlayerCommand::AddToPlaylist(items));
5585 player.process_command(PlayerCommand::ClearPlaylist);
5586 player.process_command(PlayerCommand::Undo);
5587 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
5588
5589 player.process_command(PlayerCommand::Redo);
5590 assert!(playlist_ids(&player).is_empty());
5591 }
5592
5593 #[test]
5596 fn multiple_undos_in_sequence() {
5597 let mut player = Player::new();
5598
5599 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("A")]));
5600 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
5601 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("C")]));
5602 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5603
5604 player.process_command(PlayerCommand::Undo);
5605 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
5606
5607 player.process_command(PlayerCommand::Undo);
5608 assert_eq!(playlist_titles(&player), vec!["A"]);
5609
5610 player.process_command(PlayerCommand::Undo);
5611 assert!(playlist_ids(&player).is_empty());
5612 }
5613
5614 #[test]
5615 fn undo_redo_undo_cycle() {
5616 let mut player = Player::new();
5617 let items = vec![make_item("A"), make_item("B")];
5618
5619 player.process_command(PlayerCommand::AddToPlaylist(items));
5620 player.process_command(PlayerCommand::Undo);
5621 assert!(playlist_ids(&player).is_empty());
5622
5623 player.process_command(PlayerCommand::Redo);
5624 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
5625
5626 player.process_command(PlayerCommand::Undo);
5627 assert!(playlist_ids(&player).is_empty());
5628 }
5629
5630 #[test]
5631 fn new_action_clears_redo_stack() {
5632 let mut player = Player::new();
5633 let items = vec![make_item("A")];
5634
5635 player.process_command(PlayerCommand::AddToPlaylist(items));
5636 player.process_command(PlayerCommand::Undo);
5637 assert!(player.undo_stack().can_redo());
5638
5639 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
5641 assert!(!player.undo_stack().can_redo());
5642 }
5643
5644 #[test]
5645 fn undo_on_empty_stack_is_noop() {
5646 let mut player = Player::new();
5647 player.process_command(PlayerCommand::Undo);
5648 assert!(playlist_ids(&player).is_empty());
5649 }
5650
5651 #[test]
5652 fn redo_on_empty_stack_is_noop() {
5653 let mut player = Player::new();
5654 player.process_command(PlayerCommand::Redo);
5655 assert!(playlist_ids(&player).is_empty());
5656 }
5657
5658 #[test]
5661 fn playback_commands_not_undoable() {
5662 let mut player = Player::new();
5663 player.process_command(PlayerCommand::Pause);
5664 player.process_command(PlayerCommand::Resume);
5665 player.process_command(PlayerCommand::NextTrack);
5666 player.process_command(PlayerCommand::PrevTrack);
5667 assert!(!player.undo_stack().can_undo());
5668 }
5669
5670 #[test]
5671 fn update_paths_not_undoable() {
5672 let mut player = Player::new();
5673 let items = vec![make_item("A")];
5674 let id = items[0].id;
5675 player.process_command(PlayerCommand::AddToPlaylist(items));
5676
5677 let undo_count = player.undo_stack().undo_len();
5678 player.process_command(PlayerCommand::UpdatePaths(vec![(
5679 id,
5680 PathBuf::from("/new/path.flac"),
5681 )]));
5682 assert_eq!(player.undo_stack().undo_len(), undo_count);
5683 }
5684
5685 #[test]
5688 fn add_remove_undo_undo_produces_original() {
5689 let mut player = Player::new();
5690 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5691 let b_id = items[1].id;
5692 let original_titles = vec!["A", "B", "C"];
5693
5694 player.process_command(PlayerCommand::AddToPlaylist(items));
5695 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
5696 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
5697
5698 player.process_command(PlayerCommand::Undo);
5700 assert_eq!(playlist_titles(&player), original_titles);
5701
5702 player.process_command(PlayerCommand::Undo);
5704 assert!(playlist_ids(&player).is_empty());
5705 }
5706
5707 #[test]
5708 fn interleaved_adds_and_moves_undo() {
5709 let mut player = Player::new();
5710 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5711 let a_id = items[0].id;
5712 let c_id = items[2].id;
5713
5714 player.process_command(PlayerCommand::AddToPlaylist(items));
5715
5716 player.process_command(PlayerCommand::MoveInPlaylist {
5718 id: a_id,
5719 target: c_id,
5720 after: true,
5721 });
5722 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
5723
5724 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("D")]));
5726 assert_eq!(playlist_titles(&player), vec!["B", "C", "A", "D"]);
5727
5728 player.process_command(PlayerCommand::Undo);
5730 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
5731
5732 player.process_command(PlayerCommand::Undo);
5734 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5735 }
5736
5737 #[test]
5742 fn stop_engine_drops_engine_synchronously() {
5743 use std::sync::atomic::{AtomicBool, Ordering};
5744
5745 struct MockEngine {
5746 dropped: Arc<AtomicBool>,
5747 }
5748 impl AudioEngineHandle for MockEngine {
5749 fn start(&self) -> Result<(), BackendError> {
5750 Ok(())
5751 }
5752 fn stop(&self) -> Result<(), BackendError> {
5753 Ok(())
5754 }
5755 fn is_running(&self) -> bool {
5756 false
5757 }
5758 fn fade_out(&self) {}
5759 fn fade_in(&self) -> Result<(), BackendError> {
5760 Ok(())
5761 }
5762 fn is_silent(&self) -> bool {
5763 false
5764 }
5765 }
5766 impl Drop for MockEngine {
5767 fn drop(&mut self) {
5768 self.dropped.store(true, Ordering::SeqCst);
5769 }
5770 }
5771
5772 let dropped = Arc::new(AtomicBool::new(false));
5773
5774 let mut player = Player::new();
5775 player.transport = Transport::Loaded(test_session(
5776 QueueItemId::new(),
5777 Box::new(MockEngine {
5778 dropped: dropped.clone(),
5779 }),
5780 ));
5781
5782 player.stop_engine();
5783
5784 assert!(
5788 dropped.load(Ordering::SeqCst),
5789 "AudioEngine must be dropped synchronously in stop_engine (GitHub #89)"
5790 );
5791 }
5792
5793 #[test]
5794 fn abandoning_a_stream_wakes_a_reader_parked_at_the_write_head() {
5795 let live = LiveStream {
5796 feed: crate::remote::downloads::ByteFeed::new(),
5797 abandoned: Default::default(),
5798 };
5799 let feed = live.feed.clone();
5800 let started = std::time::Instant::now();
5801 let reader = thread::spawn(move || {
5802 feed.wait_past(
5803 0,
5804 std::time::Instant::now() + std::time::Duration::from_secs(30),
5805 )
5806 });
5807 thread::sleep(std::time::Duration::from_millis(50));
5808 live.abandon();
5809 reader.join().unwrap();
5810
5811 assert!(live.abandoned.load(std::sync::atomic::Ordering::Acquire));
5812 assert!(started.elapsed() < std::time::Duration::from_secs(5));
5813 }
5814
5815 pub(super) struct StuckBackend {
5820 pub(super) rate: f64,
5821 pub(super) asked: Arc<std::sync::Mutex<Option<(f64, u32)>>>,
5822 pub(super) starts: Arc<std::sync::atomic::AtomicUsize>,
5824 }
5825
5826 struct NullEngine {
5827 starts: Arc<std::sync::atomic::AtomicUsize>,
5828 running: std::sync::atomic::AtomicBool,
5829 lead_in: Arc<AtomicU64>,
5830 }
5831 impl AudioEngineHandle for NullEngine {
5832 fn start(&self) -> Result<(), BackendError> {
5833 self.starts.fetch_add(1, Ordering::Relaxed);
5834 self.running.store(true, Ordering::Relaxed);
5835 Ok(())
5836 }
5837 fn stop(&self) -> Result<(), BackendError> {
5838 self.running.store(false, Ordering::Relaxed);
5839 Ok(())
5840 }
5841 fn is_running(&self) -> bool {
5842 self.running.load(Ordering::Relaxed)
5843 }
5844 fn fade_out(&self) {}
5845 fn fade_in(&self) -> Result<(), BackendError> {
5846 Ok(())
5847 }
5848 fn is_silent(&self) -> bool {
5849 false
5850 }
5851 fn lead_in(&self, frames: u64) {
5852 self.lead_in.store(frames, Ordering::Relaxed);
5853 }
5854 }
5855
5856 impl AudioBackend for StuckBackend {
5857 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
5858 Ok(vec![self.default_device()?])
5859 }
5860 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
5861 Ok(backend::DeviceInfo {
5862 name: "Stuck DAC".into(),
5863 sample_rates: vec![self.rate],
5864 platform_id: 0,
5865 kind: Default::default(),
5866 })
5867 }
5868 fn supported_sample_rates(
5869 &self,
5870 _device: &backend::DeviceInfo,
5871 ) -> Result<Vec<f64>, BackendError> {
5872 Ok(vec![self.rate])
5873 }
5874 fn get_device_sample_rate(
5875 &self,
5876 _device: &backend::DeviceInfo,
5877 ) -> Result<f64, BackendError> {
5878 Ok(self.rate)
5879 }
5880 fn set_device_sample_rate(
5881 &self,
5882 _device: &backend::DeviceInfo,
5883 rate: f64,
5884 ) -> Result<f64, BackendError> {
5885 Err(BackendError::UnsupportedSampleRate(rate))
5886 }
5887 fn create_engine(
5888 &self,
5889 _device: &backend::DeviceInfo,
5890 sample_rate: f64,
5891 channels: u32,
5892 _consumer: rtrb::Consumer<f32>,
5893 _samples_played: Arc<AtomicU64>,
5894 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
5895 *self.asked.lock().unwrap() = Some((sample_rate, channels));
5896 Ok(Box::new(NullEngine {
5897 starts: self.starts.clone(),
5898 running: Default::default(),
5899 lead_in: Default::default(),
5900 }))
5901 }
5902 }
5903
5904 struct CaptureBackend {
5907 consumers: Arc<std::sync::Mutex<Vec<rtrb::Consumer<f32>>>>,
5908 }
5909
5910 impl AudioBackend for CaptureBackend {
5911 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
5912 Ok(vec![self.default_device()?])
5913 }
5914 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
5915 Ok(backend::DeviceInfo {
5916 name: "Capture DAC".into(),
5917 sample_rates: vec![44100.0],
5918 platform_id: 0,
5919 kind: Default::default(),
5920 })
5921 }
5922 fn supported_sample_rates(
5923 &self,
5924 _device: &backend::DeviceInfo,
5925 ) -> Result<Vec<f64>, BackendError> {
5926 Ok(vec![44100.0])
5927 }
5928 fn get_device_sample_rate(
5929 &self,
5930 _device: &backend::DeviceInfo,
5931 ) -> Result<f64, BackendError> {
5932 Ok(44100.0)
5933 }
5934 fn set_device_sample_rate(
5935 &self,
5936 _device: &backend::DeviceInfo,
5937 rate: f64,
5938 ) -> Result<f64, BackendError> {
5939 Ok(rate)
5940 }
5941 fn create_engine(
5942 &self,
5943 _device: &backend::DeviceInfo,
5944 _sample_rate: f64,
5945 _channels: u32,
5946 consumer: rtrb::Consumer<f32>,
5947 _samples_played: Arc<AtomicU64>,
5948 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
5949 self.consumers.lock().unwrap().push(consumer);
5950 Ok(Box::new(NullEngine {
5951 starts: Default::default(),
5952 running: Default::default(),
5953 lead_in: Default::default(),
5954 }))
5955 }
5956 }
5957
5958 fn peak_reaching_the_device(
5960 consumers: &std::sync::Mutex<Vec<rtrb::Consumer<f32>>>,
5961 want: usize,
5962 ) -> f32 {
5963 let mut consumer = consumers.lock().unwrap().pop().expect("an engine was made");
5964 let mut peak = 0.0f32;
5965 let mut got = 0;
5966 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
5967 while got < want && std::time::Instant::now() < deadline {
5968 let n = consumer.slots();
5969 if n == 0 {
5970 std::thread::sleep(std::time::Duration::from_millis(2));
5971 continue;
5972 }
5973 let chunk = consumer.read_chunk(n).unwrap();
5974 let (a, b) = chunk.as_slices();
5975 peak = a.iter().chain(b).fold(peak, |p, s| p.max(s.abs()));
5976 got += n;
5977 chunk.commit_all();
5978 }
5979 assert!(got >= want, "only {got} of {want} samples arrived");
5980 peak
5981 }
5982
5983 #[test]
5987 fn a_profile_processes_files_and_streams_alike() {
5988 let dir = tempfile::tempdir().unwrap();
5989 let tone = dir.path().join("tone.wav");
5990 crate::test_utils::generate_wav_tone(&tone, 44100, 440.0, 0.5);
5991 let want = 22050;
5992
5993 let consumers = Arc::new(std::sync::Mutex::new(Vec::new()));
5994 let mut player = Player::new();
5995 player.backend = Box::new(CaptureBackend {
5996 consumers: consumers.clone(),
5997 });
5998 let mut item = make_item("tone");
5999 item.path = tone.clone();
6000 let id = item.id;
6001 player.shared_state.add_items(vec![item]);
6002 player.shared_state.set_cursor(Some(id));
6003
6004 let stream = || {
6005 let feed = crate::remote::downloads::ByteFeed::new();
6006 feed.set(std::fs::metadata(&tone).unwrap().len());
6007 Source::Stream(StreamSource {
6008 path: tone.clone(),
6009 bytes_written: feed,
6010 total: 0,
6011 mode: streaming::ProbeMode::Full,
6012 })
6013 };
6014 let peaks = |player: &mut Player| {
6015 let mut out = Vec::new();
6016 for source in [Source::File(tone.clone()), stream()] {
6017 player
6018 .try_open_session(id, source, None, 0, Run::Playing)
6019 .unwrap();
6020 player.publish();
6021 out.push((
6022 peak_reaching_the_device(&consumers, want),
6023 player.shared_state.dsp().map(|d| d.profile),
6024 ));
6025 player.stop_engine();
6026 }
6027 out
6028 };
6029
6030 let untouched = peaks(&mut player);
6031
6032 player.dsp_override = Some(Arc::new(
6033 crate::audio::dsp::Setup::new(vec![], vec![]).with_preamp(-6.0206),
6034 ));
6035 let processed = peaks(&mut player);
6036
6037 for (kind, ((before, none), (after, half))) in ["file", "stream"]
6038 .iter()
6039 .zip(untouched.iter().zip(&processed))
6040 {
6041 assert_eq!(
6042 none, &None,
6043 "{kind}: nothing is published without a profile"
6044 );
6045 assert_eq!(half.as_deref(), Some("test"), "{kind}: the badge names it");
6046 assert!(*before > 0.1, "{kind}: the tone reached the device");
6047 assert!(
6048 (after / before - 0.5).abs() < 0.01,
6049 "{kind}: {before} → {after}, not halved"
6050 );
6051 }
6052 }
6053
6054 #[test]
6060 fn a_replaced_queue_never_sounds_the_track_it_replaced() {
6061 let dir = tempfile::tempdir().unwrap();
6062 let a = dir.path().join("a.wav");
6063 crate::test_utils::generate_wav_tone(&a, 44100, 440.0, 5.0);
6064 let consumers = Arc::new(std::sync::Mutex::new(Vec::new()));
6065 let mut player = Player::new();
6066 player.backend = Box::new(CaptureBackend {
6067 consumers: consumers.clone(),
6068 });
6069 let engines = || consumers.lock().unwrap().len();
6070
6071 let item_a = PlaylistItem {
6072 path: a,
6073 ..make_item("paused")
6074 };
6075 let a_id = item_a.id;
6076 player.process_command(PlayerCommand::AddToPlaylist(vec![item_a]));
6077 player.process_command(PlayerCommand::Play(a_id));
6078 player.process_command(PlayerCommand::Pause);
6079 assert_eq!(engines(), 1);
6080
6081 let landed = dir.path().join("long.opus");
6082 let item_b = PlaylistItem {
6083 db_id: Some(77),
6084 state: ItemState::Pending,
6085 duration_ms: Some(32_523_781),
6086 path: landed.clone(),
6087 ..make_item("long")
6088 };
6089 let b_id = item_b.id;
6090 player.process_command(PlayerCommand::ReplacePlaylist {
6091 items: vec![item_b],
6092 start: 0,
6093 position_ms: 0,
6094 play: true,
6095 mode: QueueMode::Keep,
6096 });
6097 player.publish();
6098 let state = player.shared_state.clone();
6099 assert_eq!(state.cursor(), Some(b_id), "the new track, at once");
6100 assert!(
6101 state.is_waiting(),
6102 "waiting for it, which a client shows as loading"
6103 );
6104 assert_eq!(state.playback_state(), PlaybackState::Stopped);
6105 assert_eq!(
6106 state.track_info(),
6107 None,
6108 "the paused track is no longer published"
6109 );
6110 assert_eq!(engines(), 1, "nothing opened while it is on its way");
6111
6112 let store = state.downloads().clone();
6114 store.claim(77, Some(b_id));
6115 std::fs::write(&landed, include_bytes!("testdata/thirty-seconds.opus")).unwrap();
6116 let _settled = crate::remote::downloads::settle(&state, 77, &Ok(landed));
6118 player.process_command(PlayerCommand::TrackReady(b_id));
6119 player.publish();
6120
6121 assert_eq!(
6122 engines(),
6123 2,
6124 "one engine for the new track, none for the old"
6125 );
6126 assert_eq!(state.track_info().map(|t| t.id), Some(b_id));
6127 assert_eq!(state.playback_state(), PlaybackState::Playing);
6128 player.process_command(PlayerCommand::Stop);
6129 }
6130
6131 #[test]
6136 fn a_stream_opened_without_a_length_takes_the_librarys_duration() {
6137 let dir = tempfile::tempdir().unwrap();
6138 let bytes = include_bytes!("testdata/thirty-seconds.opus");
6139 let half = bytes.len() / 2;
6140 let part = dir.path().join("long.opus.part");
6141 std::fs::write(&part, &bytes[..half]).unwrap();
6142 let feed = crate::remote::downloads::ByteFeed::new();
6143 feed.set(half as u64);
6144
6145 let mut player = Player::new();
6146 player.backend = Box::new(CaptureBackend {
6147 consumers: Default::default(),
6148 });
6149 let item = PlaylistItem {
6150 db_id: Some(9),
6151 state: ItemState::Pending,
6152 duration_ms: Some(32_523_781),
6153 path: dir.path().join("long.opus"),
6154 ..make_item("long")
6155 };
6156 let id = item.id;
6157 player.shared_state.add_items(vec![item]);
6158 player.shared_state.set_cursor(Some(id));
6159
6160 let lengthless = buffer::StreamInfo {
6161 codec: "Opus".into(),
6162 sample_rate: 48_000,
6163 channels: 1,
6164 bit_depth: None,
6165 bitrate_kbps: None,
6166 duration_ms: 0,
6167 };
6168 player
6169 .try_open_session(
6170 id,
6171 Source::Stream(StreamSource {
6172 path: part,
6173 bytes_written: feed,
6174 total: bytes.len() as u64,
6175 mode: streaming::ProbeMode::Lengthless,
6176 }),
6177 Some(lengthless),
6178 0,
6179 Run::Playing,
6180 )
6181 .unwrap();
6182 player.publish();
6183
6184 assert_eq!(player.shared_state.duration_ms(), 32_523_781);
6185 assert_eq!(
6186 player.shared_state.track_info().map(|t| t.duration_ms),
6187 Some(32_523_781)
6188 );
6189 player.stop_engine();
6190 }
6191
6192 fn engine_format_for(source_rate: u32, channels: u16, device_rate: f64) -> (f64, u32) {
6193 let asked = Arc::new(std::sync::Mutex::new(None));
6194 let mut player = Player::new();
6195 player.backend = Box::new(StuckBackend {
6196 rate: device_rate,
6197 asked: asked.clone(),
6198 starts: Default::default(),
6199 });
6200
6201 let info = buffer::StreamInfo {
6202 codec: "MP3".into(),
6203 sample_rate: source_rate,
6204 channels,
6205 bit_depth: Some(16),
6206 bitrate_kbps: None,
6207 duration_ms: 1000,
6208 };
6209 let (_producer, consumer) = rtrb::RingBuffer::new(16);
6210 player
6211 .create_engine_for(&info, consumer)
6212 .expect("engine creation should succeed");
6213 let asked = *asked.lock().unwrap();
6214 asked.expect("engine was never created")
6215 }
6216
6217 #[test]
6218 fn engine_uses_source_rate_when_device_refuses_switch() {
6219 assert_eq!(engine_format_for(22050, 2, 48000.0), (22050.0, 2));
6222 assert_eq!(engine_format_for(32000, 2, 44100.0), (32000.0, 2));
6223 }
6224
6225 #[test]
6226 fn engine_uses_source_channel_count() {
6227 assert_eq!(engine_format_for(44100, 1, 44100.0), (44100.0, 1));
6228 }
6229
6230 fn settled_rate_for(source_rate: u32, device_rate: f64) -> Option<u32> {
6232 let mut player = Player::new();
6233 player.backend = Box::new(StuckBackend {
6234 rate: device_rate,
6235 asked: Arc::new(std::sync::Mutex::new(None)),
6236 starts: Default::default(),
6237 });
6238 let state = player.shared_state.clone();
6239
6240 let info = buffer::StreamInfo {
6241 codec: "MP3".into(),
6242 sample_rate: source_rate,
6243 channels: 2,
6244 bit_depth: Some(16),
6245 bitrate_kbps: None,
6246 duration_ms: 1000,
6247 };
6248 let (_producer, consumer) = rtrb::RingBuffer::new(16);
6249 player
6250 .create_engine_for(&info, consumer)
6251 .expect("engine creation should succeed");
6252 state.output_sample_rate()
6253 }
6254
6255 #[test]
6256 fn settled_device_rate_reaches_the_shared_state() {
6257 assert_eq!(settled_rate_for(22050, 48000.0), Some(48000));
6261 assert_eq!(settled_rate_for(44100, 44100.0), Some(44100));
6263 }
6264
6265 struct SlowBackend {
6267 observed: Arc<std::sync::Mutex<Vec<Option<u32>>>>,
6268 state: Arc<SharedPlayerState>,
6269 lead_in: Arc<AtomicU64>,
6270 }
6271
6272 impl AudioBackend for SlowBackend {
6273 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
6274 Ok(vec![self.default_device()?])
6275 }
6276 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
6277 Ok(backend::DeviceInfo {
6278 name: "Slow DAC".into(),
6279 sample_rates: vec![44100.0, 48000.0],
6280 platform_id: 0,
6281 kind: Default::default(),
6282 })
6283 }
6284 fn supported_sample_rates(
6285 &self,
6286 _device: &backend::DeviceInfo,
6287 ) -> Result<Vec<f64>, BackendError> {
6288 Ok(vec![44100.0, 48000.0])
6289 }
6290 fn get_device_sample_rate(
6291 &self,
6292 _device: &backend::DeviceInfo,
6293 ) -> Result<f64, BackendError> {
6294 Ok(48000.0)
6295 }
6296 fn set_device_sample_rate(
6297 &self,
6298 _device: &backend::DeviceInfo,
6299 rate: f64,
6300 ) -> Result<f64, BackendError> {
6301 self.observed
6303 .lock()
6304 .unwrap()
6305 .push(self.state.output_sample_rate());
6306 Ok(rate)
6307 }
6308 fn create_engine(
6309 &self,
6310 _device: &backend::DeviceInfo,
6311 _sample_rate: f64,
6312 _channels: u32,
6313 _consumer: rtrb::Consumer<f32>,
6314 _samples_played: Arc<AtomicU64>,
6315 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
6316 Ok(Box::new(NullEngine {
6317 starts: Default::default(),
6318 running: Default::default(),
6319 lead_in: self.lead_in.clone(),
6320 }))
6321 }
6322 }
6323
6324 #[test]
6325 fn the_previous_rate_is_not_published_while_the_device_reclocks() {
6326 let mut player = Player::new();
6332 let state = player.shared_state.clone();
6333 state.set_output_sample_rate(48000);
6334
6335 let observed = Arc::new(std::sync::Mutex::new(Vec::new()));
6336 player.backend = Box::new(SlowBackend {
6337 observed: observed.clone(),
6338 state: state.clone(),
6339 lead_in: Default::default(),
6340 });
6341
6342 let info = buffer::StreamInfo {
6343 codec: "FLAC".into(),
6344 sample_rate: 44100,
6345 channels: 2,
6346 bit_depth: Some(16),
6347 bitrate_kbps: None,
6348 duration_ms: 1000,
6349 };
6350 let (_producer, consumer) = rtrb::RingBuffer::new(16);
6351 player
6352 .create_engine_for(&info, consumer)
6353 .expect("engine creation should succeed");
6354
6355 assert_eq!(
6356 *observed.lock().unwrap(),
6357 vec![None],
6358 "mid-switch the output rate must read as unknown, not as the last track's"
6359 );
6360 assert_eq!(state.output_sample_rate(), Some(44100));
6361 }
6362
6363 fn lead_in_for(source_rate: u32) -> u64 {
6366 let mut player = Player::new();
6367 let lead_in = Arc::new(AtomicU64::new(0));
6368 player.backend = Box::new(SlowBackend {
6369 observed: Default::default(),
6370 state: player.shared_state.clone(),
6371 lead_in: lead_in.clone(),
6372 });
6373 let info = buffer::StreamInfo {
6374 codec: "FLAC".into(),
6375 sample_rate: source_rate,
6376 channels: 2,
6377 bit_depth: Some(16),
6378 bitrate_kbps: None,
6379 duration_ms: 1000,
6380 };
6381 let (_producer, consumer) = rtrb::RingBuffer::new(16);
6382 player
6383 .create_engine_for(&info, consumer)
6384 .expect("engine creation should succeed");
6385 lead_in.load(Ordering::Relaxed)
6386 }
6387
6388 #[test]
6389 fn an_engine_made_inside_the_silence_keeps_the_rest_of_it() {
6390 let mut player = Player::new();
6393 let lead_in = Arc::new(AtomicU64::new(0));
6394 player.backend = Box::new(SlowBackend {
6395 observed: Default::default(),
6396 state: player.shared_state.clone(),
6397 lead_in: lead_in.clone(),
6398 });
6399 let info = |sample_rate| buffer::StreamInfo {
6400 codec: "FLAC".into(),
6401 sample_rate,
6402 channels: 2,
6403 bit_depth: Some(16),
6404 bitrate_kbps: None,
6405 duration_ms: 1000,
6406 };
6407 let (_p, consumer) = rtrb::RingBuffer::new(16);
6408 player.create_engine_for(&info(44100), consumer).unwrap();
6409 let first = lead_in.swap(0, Ordering::Relaxed);
6410 assert!(first > 0);
6411
6412 let (_p, consumer) = rtrb::RingBuffer::new(16);
6413 player.create_engine_for(&info(48000), consumer).unwrap();
6414 let carried = lead_in.load(Ordering::Relaxed);
6415 assert!(
6417 carried > 0 && carried < first * 48000 / 44100,
6418 "carried {carried} of {first}"
6419 );
6420 }
6421
6422 #[test]
6423 fn silence_leads_in_only_after_the_device_changed_rate() {
6424 assert!(
6425 lead_in_for(44100) > 0,
6426 "the device is relocking, so the start of the track would be lost"
6427 );
6428 assert_eq!(lead_in_for(48000), 0, "no switch, nothing to wait for");
6429 }
6430
6431 #[cfg(target_os = "macos")]
6433 struct IosRates {
6434 lead_in: Arc<AtomicU64>,
6435 }
6436
6437 #[cfg(target_os = "macos")]
6438 impl AudioBackend for IosRates {
6439 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
6440 crate::audio::ios_backend::IosAudioBackend.list_devices()
6441 }
6442 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
6443 crate::audio::ios_backend::IosAudioBackend.default_device()
6444 }
6445 fn supported_sample_rates(
6446 &self,
6447 device: &backend::DeviceInfo,
6448 ) -> Result<Vec<f64>, BackendError> {
6449 crate::audio::ios_backend::IosAudioBackend.supported_sample_rates(device)
6450 }
6451 fn get_device_sample_rate(
6452 &self,
6453 device: &backend::DeviceInfo,
6454 ) -> Result<f64, BackendError> {
6455 crate::audio::ios_backend::IosAudioBackend.get_device_sample_rate(device)
6456 }
6457 fn set_device_sample_rate(
6458 &self,
6459 device: &backend::DeviceInfo,
6460 rate: f64,
6461 ) -> Result<f64, BackendError> {
6462 crate::audio::ios_backend::IosAudioBackend.set_device_sample_rate(device, rate)
6463 }
6464 fn create_engine(
6465 &self,
6466 _device: &backend::DeviceInfo,
6467 _sample_rate: f64,
6468 _channels: u32,
6469 _consumer: rtrb::Consumer<f32>,
6470 _samples_played: Arc<AtomicU64>,
6471 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
6472 Ok(Box::new(NullEngine {
6473 starts: Default::default(),
6474 running: Default::default(),
6475 lead_in: self.lead_in.clone(),
6476 }))
6477 }
6478 }
6479
6480 #[cfg(target_os = "macos")]
6482 struct SwitchingDac;
6483
6484 #[cfg(target_os = "macos")]
6485 impl crate::audio::ios_backend::AudioSession for SwitchingDac {
6486 fn activate(&self, sample_rate: f64) -> Option<f64> {
6487 Some(self.follow(sample_rate))
6488 }
6489 fn follow(&self, sample_rate: f64) -> f64 {
6490 if sample_rate == 44100.0 {
6491 44100.0
6492 } else {
6493 48000.0
6494 }
6495 }
6496 fn release(&self) {}
6497 }
6498
6499 #[test]
6500 #[cfg(target_os = "macos")]
6501 fn a_session_let_go_is_no_rate_switch_to_wait_out() {
6502 use crate::audio::ios_backend;
6503 let _session = ios_backend::TEST_SESSION.lock();
6504 ios_backend::set_session(Arc::new(SwitchingDac));
6505 let lead_in_at = |sample_rate| {
6506 let mut player = Player::new();
6507 let lead_in = Arc::new(AtomicU64::new(0));
6508 player.backend = Box::new(IosRates {
6509 lead_in: lead_in.clone(),
6510 });
6511 let info = buffer::StreamInfo {
6512 codec: "FLAC".into(),
6513 sample_rate,
6514 channels: 2,
6515 bit_depth: Some(16),
6516 bitrate_kbps: None,
6517 duration_ms: 1000,
6518 };
6519 let (_producer, consumer) = rtrb::RingBuffer::new(16);
6520 player.create_engine_for(&info, consumer).unwrap();
6521 lead_in.load(Ordering::Relaxed)
6522 };
6523
6524 assert!(ios_backend::activate_session(48000.0));
6526 assert!(lead_in_at(44100) > 0);
6527
6528 assert!(ios_backend::activate_session(48000.0));
6532 ios_backend::release_session();
6533 assert_eq!(lead_in_at(44100), 0);
6534 }
6535
6536 struct WatchedBackend {
6538 inner: StuckBackend,
6539 #[allow(clippy::type_complexity)]
6540 captured: Arc<std::sync::Mutex<Option<Box<dyn Fn(f64) + Send + Sync>>>>,
6541 }
6542
6543 struct NullWatch;
6544 impl backend::SampleRateWatch for NullWatch {}
6545
6546 impl AudioBackend for WatchedBackend {
6547 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
6548 self.inner.list_devices()
6549 }
6550 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
6551 self.inner.default_device()
6552 }
6553 fn supported_sample_rates(
6554 &self,
6555 device: &backend::DeviceInfo,
6556 ) -> Result<Vec<f64>, BackendError> {
6557 self.inner.supported_sample_rates(device)
6558 }
6559 fn get_device_sample_rate(
6560 &self,
6561 device: &backend::DeviceInfo,
6562 ) -> Result<f64, BackendError> {
6563 self.inner.get_device_sample_rate(device)
6564 }
6565 fn set_device_sample_rate(
6566 &self,
6567 device: &backend::DeviceInfo,
6568 rate: f64,
6569 ) -> Result<f64, BackendError> {
6570 self.inner.set_device_sample_rate(device, rate)
6571 }
6572 fn watch_device_sample_rate(
6573 &self,
6574 _device: &backend::DeviceInfo,
6575 on_change: Box<dyn Fn(f64) + Send + Sync>,
6576 ) -> Option<Box<dyn backend::SampleRateWatch>> {
6577 *self.captured.lock().unwrap() = Some(on_change);
6578 Some(Box::new(NullWatch))
6579 }
6580 fn create_engine(
6581 &self,
6582 device: &backend::DeviceInfo,
6583 sample_rate: f64,
6584 channels: u32,
6585 consumer: rtrb::Consumer<f32>,
6586 samples_played: Arc<AtomicU64>,
6587 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
6588 self.inner
6589 .create_engine(device, sample_rate, channels, consumer, samples_played)
6590 }
6591 }
6592
6593 #[test]
6594 fn external_rate_change_reaches_the_shared_state() {
6595 let captured = Arc::new(std::sync::Mutex::new(None));
6599 let mut player = Player::new();
6600 player.backend = Box::new(WatchedBackend {
6601 inner: StuckBackend {
6602 rate: 44100.0,
6603 asked: Arc::new(std::sync::Mutex::new(None)),
6604 starts: Default::default(),
6605 },
6606 captured: captured.clone(),
6607 });
6608 let state = player.shared_state.clone();
6609
6610 let info = buffer::StreamInfo {
6611 codec: "FLAC".into(),
6612 sample_rate: 44100,
6613 channels: 2,
6614 bit_depth: Some(16),
6615 bitrate_kbps: None,
6616 duration_ms: 1000,
6617 };
6618 let (_producer, consumer) = rtrb::RingBuffer::new(16);
6619 player
6620 .create_engine_for(&info, consumer)
6621 .expect("engine creation should succeed");
6622 assert_eq!(state.output_sample_rate(), Some(44100));
6623
6624 let on_change = captured.lock().unwrap().take().expect("watch registered");
6625 on_change(48000.0);
6626 assert_eq!(state.output_sample_rate(), Some(48000));
6627 }
6628}