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 OUTPUT_START_CHECK: std::time::Duration = std::time::Duration::from_millis(400);
46
47const FADE_CHECK: std::time::Duration = std::time::Duration::from_millis(50);
50const QUICK_FADE_LIMIT: std::time::Duration = std::time::Duration::from_millis(100);
53
54const SESSION_GRACE: std::time::Duration = std::time::Duration::from_secs(5);
58
59fn dsp_wake(
65 since: std::time::Instant,
66 period: std::time::Duration,
67 now: std::time::Instant,
68) -> std::time::Instant {
69 let faded = since + crate::audio::fade::QUICK_FADE + period + BOUNDARY_SLACK;
70 [faded, faded + period, since + QUICK_FADE_LIMIT]
71 .into_iter()
72 .find(|&t| t > now)
73 .unwrap_or(now)
74}
75
76#[derive(Debug, Error)]
77pub enum PlayerError {
78 #[error("backend error: {0}")]
79 Backend(#[from] BackendError),
80 #[error("decode error: {0}")]
81 Decode(#[from] buffer::DecodeError),
82 #[error("renderer: {0}")]
83 Renderer(String),
84 #[error("{0}")]
87 Unplayable(String),
88}
89
90#[derive(Clone)]
93struct StreamSource {
94 path: PathBuf,
95 bytes_written: Arc<crate::remote::downloads::ByteFeed>,
96 total: u64,
97 mode: streaming::ProbeMode,
98}
99
100fn media_extension(path: &Path) -> Option<String> {
103 crate::remote::download::strip_part_suffix(path)
104 .extension()
105 .and_then(|e| e.to_str())
106 .map(str::to_ascii_lowercase)
107}
108
109fn hint_for(path: &Path) -> symphonia::core::formats::probe::Hint {
111 let mut hint = symphonia::core::formats::probe::Hint::new();
112 if let Some(ext) = media_extension(path) {
113 hint.with_extension(&ext);
114 }
115 hint
116}
117
118fn lengthless_mode_for(path: &Path) -> streaming::ProbeMode {
128 match media_extension(path).as_deref() {
129 Some("ogg" | "oga" | "opus" | "spx" | "m4a" | "m4b" | "mp4" | "mov") => {
130 streaming::ProbeMode::LengthlessWholeEnd
131 }
132 _ => streaming::ProbeMode::Lengthless,
133 }
134}
135
136pub struct Player {
138 shared_state: Arc<SharedPlayerState>,
139 commands: CommandChannel,
140 transport: Transport,
142 timeline: Arc<PlaybackTimeline>,
143 viz_buffer: Arc<VizBuffer>,
144 viz_snapshot: Arc<VizSnapshot>,
145 _viz_analyzer: VizAnalyzer,
147 undo_stack: UndoStack,
148 batch_buffer: Option<Vec<UndoEntry>>,
151 output_device_name: Option<String>,
153 backend: Box<dyn AudioBackend>,
155 stream_mode: streaming::ProbeMode,
158 history: Option<PlayRecorder>,
161 downloads: Option<crate::remote::queue::DownloadQueue>,
165 in_flight: Option<InFlight>,
167 lead_in_ends: Option<std::time::Instant>,
169 output_check: Option<(std::time::Instant, u64)>,
172 session_release_at: Option<std::time::Instant>,
175 session: u64,
178 silence_waiters: Vec<crossbeam_channel::Sender<u64>>,
180 dsp: Option<DspCache>,
183 dsp_restart: Option<std::time::Instant>,
187 quick_start: bool,
190 renderer: Option<renderer::RendererLink>,
193 mode: PlayMode,
196 sleep: Option<SleepSet>,
198 sleep_fade: Option<sleep::SleepFade>,
200 sleep_snap_on_resume: bool,
202 #[cfg(test)]
205 playback_starts: usize,
206 #[cfg(test)]
210 dsp_override: Option<Arc<crate::audio::dsp::Setup>>,
211 #[cfg(test)]
212 renderer_dsp_override: Option<Arc<crate::audio::dsp::Setup>>,
213 resume_renderer: bool,
216 held_run: Option<Run>,
220}
221
222#[derive(Clone, Copy)]
225struct SleepSet {
226 sleep: Sleep,
227 at: Option<std::time::Instant>,
228 fade: Option<std::time::Duration>,
230}
231
232struct DspCache {
233 config: Arc<crate::config::Config>,
234 device: String,
235 setup: Option<Arc<crate::audio::dsp::Setup>>,
236}
237
238#[derive(Clone, Copy)]
240enum Fade {
241 Cut,
242 Short,
243}
244
245#[derive(Clone, Copy, Debug, PartialEq, Eq)]
248enum Run {
249 Playing,
250 Paused,
251}
252
253enum Transport {
257 Idle,
259 Waiting(Waiting),
262 Loaded(Session),
264}
265
266#[derive(Clone, Copy)]
271struct Waiting {
272 id: QueueItemId,
273 position_ms: u64,
274 start: Run,
275}
276
277impl Waiting {
278 fn may_stream(&self, id: QueueItemId) -> bool {
279 self.id == id && self.position_ms == 0
280 }
281}
282
283struct Session {
286 track: TrackInfo,
289 run: Run,
290 lookahead: Arc<parking_lot::Mutex<Vec<state::Lookahead>>>,
293 output: Output,
294}
295
296enum Output {
298 Local(Local),
301 Renderer(Box<renderer::Play>),
305}
306
307struct Local {
308 engine: Box<dyn AudioEngineHandle>,
309 decode_handle: buffer::DecodeHandle,
310 stream: Option<LiveStream>,
312 _rate_watch: Option<Box<dyn SampleRateWatch>>,
315 dsp: Option<crate::audio::dsp::DspStatus>,
317}
318
319impl Session {
320 fn engine(&self) -> Option<&dyn AudioEngineHandle> {
322 match &self.output {
323 Output::Local(local) => Some(local.engine.as_ref()),
324 Output::Renderer(_) => None,
325 }
326 }
327}
328
329enum Source {
331 File(PathBuf),
332 Stream(StreamSource),
333}
334
335impl Source {
336 fn path(&self) -> &Path {
337 match self {
338 Source::File(path) => path,
339 Source::Stream(source) => &source.path,
340 }
341 }
342}
343
344struct LiveStream {
348 feed: Arc<crate::remote::downloads::ByteFeed>,
349 abandoned: Arc<std::sync::atomic::AtomicBool>,
350}
351
352impl LiveStream {
353 fn abandon(&self) {
354 self.abandoned
355 .store(true, std::sync::atomic::Ordering::Release);
356 self.feed.done();
357 }
358}
359
360impl Default for Player {
361 fn default() -> Self {
362 Self::new()
363 }
364}
365
366impl Player {
367 pub fn new() -> Self {
368 let viz_buffer = VizBuffer::new();
369 let viz_snapshot = VizSnapshot::new();
370 let timeline = PlaybackTimeline::new();
371 let cfg = crate::config::Config::cached();
372 let viz_analyzer = VizAnalyzer::spawn_with_snapshot(
373 Arc::clone(&viz_buffer),
374 &cfg.visualizer,
375 Arc::clone(&viz_snapshot),
376 timeline.samples_played_counter(),
377 );
378
379 let shared_state = SharedPlayerState::new();
380 shared_state.attach_timeline(timeline.clone());
381 let commands = CommandChannel::new();
382 let tx = commands.tx.clone();
383 timeline.on_queued(move || {
384 let _ = tx.try_send(PlayerCommand::TrackQueued);
385 });
386
387 Self {
388 shared_state,
389 commands,
390 transport: Transport::Idle,
391 lead_in_ends: None,
392 output_check: None,
393 session_release_at: None,
394 dsp: None,
395 dsp_restart: None,
396 quick_start: false,
397 session: 0,
398 silence_waiters: Vec::new(),
399 renderer: None,
400 mode: PlayMode::default(),
401 sleep: None,
402 sleep_fade: None,
403 sleep_snap_on_resume: false,
404 timeline,
405 viz_buffer,
406 viz_snapshot,
407 _viz_analyzer: viz_analyzer,
408 undo_stack: UndoStack::new(),
409 batch_buffer: None,
410 output_device_name: cfg.playback.output_device.clone(),
411 backend: crate::audio::platform_backend(),
412 stream_mode: streaming::ProbeMode::Full,
413 history: None,
414 downloads: None,
415 in_flight: None,
416 #[cfg(test)]
417 playback_starts: 0,
418 #[cfg(test)]
419 dsp_override: None,
420 #[cfg(test)]
421 renderer_dsp_override: None,
422 resume_renderer: false,
423 held_run: None,
424 }
425 }
426
427 pub fn shared_state(&self) -> Arc<SharedPlayerState> {
429 self.shared_state.clone()
430 }
431
432 pub fn timeline(&self) -> Arc<PlaybackTimeline> {
434 self.timeline.clone()
435 }
436
437 pub fn viz_buffer(&self) -> Arc<VizBuffer> {
439 self.viz_buffer.clone()
440 }
441
442 pub fn viz_snapshot(&self) -> Arc<VizSnapshot> {
445 self.viz_snapshot.clone()
446 }
447
448 pub fn undo_stack(&self) -> &UndoStack {
450 &self.undo_stack
451 }
452
453 #[allow(clippy::type_complexity)]
461 fn create_engine_for(
462 &mut self,
463 info: &buffer::StreamInfo,
464 consumer: rtrb::Consumer<f32>,
465 ) -> Result<(Box<dyn AudioEngineHandle>, Option<Box<dyn SampleRateWatch>>), PlayerError> {
466 let device = self.resolve_device()?;
467 let device_rate = self.backend.get_device_sample_rate(&device)?;
468 let dsp = match &self.dsp {
471 Some(cache) if cache.device == device.name => cache.setup.clone(),
472 _ => self.dsp_for(&device.name),
473 };
474 let source_rate =
477 dsp.as_ref()
478 .map_or(info.sample_rate, |d| d.output_rate(info.sample_rate)) as f64;
479
480 self.shared_state.clear_output_sample_rate();
484
485 let gained = crate::config::Config::cached().playback.replaygain
488 != crate::config::ReplayGainMode::Off;
489 let bit_depth = if dsp.is_some() || gained {
490 None
491 } else {
492 info.bit_depth
493 };
494 if (device_rate - source_rate).abs() > 0.1 {
495 log::info!(
496 "switching device sample rate: {}Hz → {}Hz",
497 device_rate,
498 source_rate
499 );
500 }
501 let settled = match self.backend.prepare_output(
502 &device,
503 device_rate,
504 source_rate,
505 info.channels,
506 bit_depth,
507 ) {
508 Ok(rate) => rate,
509 Err(e) => {
510 log::warn!("failed to set device sample rate: {}", e);
511 device_rate
512 }
513 };
514 if (settled - source_rate).abs() > 0.1 {
515 log::warn!(
516 "device stayed at {}Hz (wanted {}Hz) — output is resampled, not bit-perfect",
517 settled,
518 source_rate
519 );
520 }
521
522 self.shared_state
525 .set_output_sample_rate(settled.round() as u32);
526
527 let watch_state = self.shared_state.clone();
531 let watch_name = device.name.clone();
532 let rate_watch = self.backend.watch_device_sample_rate(
533 &device,
534 Box::new(move |rate| {
535 log::info!("device sample rate changed externally: {rate}Hz on '{watch_name}'");
536 watch_state.set_output_sample_rate(rate.round() as u32);
537 }),
538 );
539
540 let engine = self.backend.create_engine(
541 &device,
542 source_rate,
543 info.channels as u32,
544 consumer,
545 self.timeline.samples_played_counter(),
546 )?;
547 let mixed = self.backend.output_mixed();
550 self.shared_state.set_output_mixed(mixed);
551 if mixed == Some(true)
552 && let Ok(rate) = self.backend.get_device_sample_rate(&device)
553 {
554 self.shared_state
555 .set_output_sample_rate(rate.round() as u32);
556 }
557 let now = std::time::Instant::now();
561 let lead_in = if device_rate > 0.0 && (settled - device_rate).abs() > 0.1 {
562 std::time::Duration::from_millis(
563 crate::config::Config::cached()
564 .playback
565 .rate_switch_lead_in_ms as u64,
566 )
567 } else {
568 self.lead_in_ends
569 .map(|end| end.saturating_duration_since(now))
570 .unwrap_or_default()
571 };
572 self.lead_in_ends = None;
573 if !lead_in.is_zero() {
574 engine.lead_in((settled * lead_in.as_secs_f64()) as u64);
575 self.lead_in_ends = Some(now + lead_in);
576 }
577
578 Ok((engine, rate_watch))
579 }
580
581 fn dsp_for(&mut self, device: &str) -> Option<Arc<crate::audio::dsp::Setup>> {
583 let config = crate::config::Config::cached();
584 #[cfg(test)]
585 if let Some(setup) = if self
586 .renderer
587 .as_ref()
588 .is_some_and(|l| l.device_name() == device)
589 {
590 self.renderer_dsp_override.clone()
591 } else {
592 self.dsp_override.clone()
593 } {
594 self.dsp = Some(DspCache {
595 config,
596 device: device.to_string(),
597 setup: Some(setup.clone()),
598 });
599 return Some(setup);
600 }
601 if let Some(cache) = &self.dsp
602 && Arc::ptr_eq(&cache.config, &config)
603 && cache.device == device
604 {
605 return cache.setup.clone();
606 }
607 let setup = crate::audio::dsp::profiles::for_playing(|| {
608 let chain = crate::audio::dsp::output_chain(&config.dsp, device)?;
609 crate::audio::dsp::Setup::load(&chain.profile, &chain.all, &crate::config::config_dir())
610 .inspect_err(|e| {
611 log::error!(
612 "dsp: profile '{}' not loaded, playing without it: {e}",
613 chain.name
614 )
615 })
616 .ok()
617 .flatten()
618 .map(|mut setup| {
619 setup.name = chain.name;
620 Arc::new(setup)
621 })
622 });
623 self.dsp = Some(DspCache {
624 config,
625 device: device.to_string(),
626 setup: setup.clone(),
627 });
628 setup
629 }
630
631 fn reload_dsp(&mut self) {
637 let device = match &self.renderer {
638 Some(link) => Ok(link.device_name().to_string()),
639 None => self.resolve_device().map(|d| d.name),
640 };
641 let Ok(device) = device else {
642 self.dsp = None;
643 return;
644 };
645 let was = self
646 .dsp
647 .take()
648 .filter(|c| c.device == device)
649 .map(|c| c.setup);
650 let now = self.dsp_for(&device);
651 let changed = match was {
652 Some(was) => was != now,
653 None => now.is_some(),
654 };
655 if changed {
656 self.restart_for_dsp();
657 }
658 }
659
660 fn restart_for_dsp(&mut self) {
667 if self.dsp_restart.is_some() {
668 return;
669 }
670 if let Some(session) = self.session()
671 && session.run == Run::Playing
672 && let Output::Local(local) = &session.output
673 && local.engine.is_running()
674 {
675 local.engine.fade_out_quickly();
676 self.dsp_restart = Some(std::time::Instant::now());
677 return;
678 }
679 self.restart_on_current_track();
680 }
681
682 fn processing(&mut self) -> buffer::Processing {
684 let cfg = crate::config::Config::cached();
685 let dsp = match self.resolve_device() {
686 Ok(device) => self.dsp_for(&device.name),
687 Err(_) => None,
688 };
689 buffer::Processing {
690 rg_mode: cfg.playback.replaygain,
691 pre_amp_db: cfg.playback.pre_amp_db,
692 dsp,
693 depth_limit: Arc::default(),
694 }
695 }
696
697 fn resolve_device(&self) -> Result<backend::DeviceInfo, PlayerError> {
700 if let Some(ref name) = self.output_device_name {
701 match self.backend.list_devices() {
702 Ok(devices) => {
703 if let Some(dev) = devices.into_iter().find(|d| d.name == *name) {
704 return Ok(dev);
705 }
706 log::warn!(
707 "configured output device '{}' not found, falling back to default",
708 name,
709 );
710 }
711 Err(e) => {
712 log::warn!("failed to list devices while resolving '{}': {}", name, e);
713 }
714 }
715 }
716 Ok(self.backend.default_device()?)
717 }
718
719 pub fn set_output_device(&mut self, name: String) {
722 log::info!("switching output device to: {}", name);
723 self.output_device_name = Some(name.clone());
724
725 if let Err(e) = crate::config::Config::persist(|cfg| {
726 cfg.playback.output_device = Some(name);
727 cfg.playback.renderer = None;
728 cfg.playback.renderer_name = None;
729 }) {
730 log::error!("failed to save output device config: {}", e);
731 }
732
733 if self.renderer.is_some() {
734 self.use_renderer(None);
735 } else {
736 self.restart_on_current_track();
737 }
738 }
739
740 pub fn clear_output_device(&mut self) {
742 log::info!("reverting to system default output device");
743 self.output_device_name = None;
744
745 if let Err(e) = crate::config::Config::persist(|cfg| {
746 cfg.playback.output_device = None;
747 cfg.playback.renderer = None;
748 cfg.playback.renderer_name = None;
749 }) {
750 log::error!("failed to save output device config: {}", e);
751 }
752
753 if self.renderer.is_some() {
754 self.use_renderer(None);
755 } else {
756 self.restart_on_current_track();
757 }
758 }
759
760 fn restart_on_current_track(&mut self) {
763 let position_ms = self.shared_state.position_ms();
764 if let Err(e) = self.restart_current(position_ms) {
765 log::error!("failed to restart playback on device switch: {}", e);
766 }
767 }
768
769 pub fn output_device_name(&self) -> Option<&str> {
771 self.output_device_name.as_deref()
772 }
773
774 pub fn command_sender(&self) -> crossbeam_channel::Sender<PlayerCommand> {
776 self.commands.tx.clone()
777 }
778
779 fn session(&self) -> Option<&Session> {
780 match &self.transport {
781 Transport::Loaded(session) => Some(session),
782 _ => None,
783 }
784 }
785
786 fn waiting(&self) -> Option<Waiting> {
787 match self.transport {
788 Transport::Waiting(waiting) => Some(waiting),
789 _ => None,
790 }
791 }
792
793 fn may_stream(&self, waiting: &Waiting, id: QueueItemId) -> bool {
796 waiting.may_stream(id) && self.renderer.is_none()
797 }
798
799 fn forget_waiting(&mut self) {
800 if matches!(self.transport, Transport::Waiting(_)) {
801 self.transport = Transport::Idle;
802 }
803 }
804
805 fn publish(&self) {
809 let state = &self.shared_state;
810 let (playback, waiting, track) = match &self.transport {
811 Transport::Idle => (PlaybackState::Stopped, false, None),
812 Transport::Waiting(waiting) => (
813 match waiting.start {
814 Run::Playing => PlaybackState::Stopped,
815 Run::Paused => PlaybackState::Paused,
816 },
817 true,
818 None,
819 ),
820 Transport::Loaded(session) => (
821 match session.run {
822 Run::Playing => PlaybackState::Playing,
823 Run::Paused => PlaybackState::Paused,
824 },
825 false,
826 Some(&session.track),
827 ),
828 };
829 if state.track_info().as_ref() != track {
830 state.set_track_info(track.cloned());
831 }
832 state.set_transport(playback, waiting);
833 let dsp = match &self.transport {
836 Transport::Loaded(Session {
837 output: Output::Local(local),
838 ..
839 }) => local.dsp.clone(),
840 Transport::Loaded(Session {
841 output: Output::Renderer(play),
842 ..
843 }) => play.dsp(),
844 _ => None,
845 };
846 if state.dsp() != dsp {
847 state.set_dsp(dsp);
848 }
849 state.set_play_mode(self.mode);
850 state.set_sleep(self.sleep.map(|s| s.sleep));
851 state.set_sleep_fading(self.sleep_fading());
852 }
853
854 fn intent(&self) -> Option<Run> {
859 match &self.transport {
860 Transport::Idle => None,
861 Transport::Waiting(waiting) => Some(waiting.start),
862 Transport::Loaded(session) => Some(session.run),
863 }
864 }
865
866 fn carry_on(&mut self, to: Option<QueueItemId>, intent: Option<Run>) {
869 match (to, intent) {
870 (Some(id), Some(start)) => self.cue(id, 0, start),
871 (Some(id), None) => {
872 self.stop_playback_and_clear_state();
873 self.shared_state.set_cursor(Some(id));
874 }
875 (None, _) => self.stop_playback_and_clear_state(),
876 }
877 }
878
879 pub fn play(&mut self, id: QueueItemId) {
882 if self.in_flight.as_ref().is_some_and(|f| f.item == id) {
885 self.finish_play();
886 }
887 self.forget_waiting();
888 self.shared_state.set_cursor(Some(id));
889
890 match self.shared_state.item_playback_source(id) {
891 Some(PlaybackSource::Ready(path)) => {
892 if let Err(e) = self.start_playback(id, &path, 0, Run::Playing) {
893 self.start_failed("play", &e);
894 }
895 }
896 Some(PlaybackSource::Streaming {
897 path,
898 bytes_written,
899 total,
900 }) => {
901 self.park(id, 0, Run::Playing);
906 if self.renderer.is_none() {
907 self.probe_stream_for_playback(id, &path, bytes_written, total);
908 }
909 }
910 None => {
911 self.park(id, 0, Run::Playing);
912 log::info!("play: item {:?} not ready, waiting for TrackReady", id);
913 }
914 }
915 }
916
917 fn cue(&mut self, id: QueueItemId, position_ms: u64, start: Run) {
922 self.forget_waiting();
923 self.shared_state.set_cursor(Some(id));
924 let Some(PlaybackSource::Ready(path)) = self.shared_state.item_playback_source(id) else {
925 if position_ms == 0 && start == Run::Playing {
926 self.play(id);
927 return;
928 }
929 self.park(id, position_ms, start);
930 log::info!("cue: {id:?} not on disk yet, opening at {position_ms}ms once it is");
931 return;
932 };
933 if let Err(e) = self.start_playback(id, &path, position_ms, start) {
934 self.start_failed("cue", &e);
935 return;
936 }
937 self.report(match start {
938 Run::Playing => PlaybackReportState::Playing,
939 Run::Paused => PlaybackReportState::Paused,
940 });
941 }
942
943 fn park(&mut self, id: QueueItemId, position_ms: u64, start: Run) {
947 self.report(PlaybackReportState::Stopped);
948 self.finish_play();
949 self.stop_engine();
950 self.timeline.reset();
951 self.shared_state.set_position_ms(position_ms);
952 self.transport = Transport::Waiting(Waiting {
953 id,
954 position_ms,
955 start,
956 });
957 }
958
959 fn start_playback(
960 &mut self,
961 id: QueueItemId,
962 path: &Path,
963 seek_ms: u64,
964 start: Run,
965 ) -> Result<(), PlayerError> {
966 self.open_session(id, Source::File(path.to_path_buf()), None, seek_ms, start)
967 }
968
969 fn open_session(
977 &mut self,
978 id: QueueItemId,
979 source: Source,
980 info: Option<buffer::StreamInfo>,
981 seek_ms: u64,
982 start: Run,
983 ) -> Result<(), PlayerError> {
984 #[cfg(test)]
985 {
986 self.playback_starts += 1;
987 }
988 self.forget_waiting();
989 let mut result = self.try_open_session(id, source, info, seek_ms, start);
990 let mut skipped = id;
996 while matches!(result, Err(PlayerError::Unplayable(_)))
997 && self.shared_state.is_cursor(skipped)
998 {
999 let Some(next) = self.shared_state.advance_cursor_loadable() else {
1000 log::info!("upnp: nothing further the renderer can play");
1001 self.stop_playback_and_clear_state();
1002 return Ok(());
1003 };
1004 match self.shared_state.item_playback_source(next) {
1005 Some(PlaybackSource::Ready(path)) => {
1006 skipped = next;
1007 result = self.try_open_session(next, Source::File(path), None, 0, start);
1008 }
1009 _ => {
1011 self.cue(next, 0, start);
1012 return Ok(());
1013 }
1014 }
1015 }
1016 if result.is_err() {
1017 self.stop_playback_and_clear_state();
1018 } else {
1019 self.shared_state.set_start_failure(None);
1020 }
1021 self.wake_analyzer();
1022 result
1023 }
1024
1025 fn start_failed(&self, during: &str, e: &PlayerError) {
1028 log::error!("{during}: playback failed: {e}");
1029 self.shared_state
1030 .set_start_failure(Some(format!("Couldn't play this track: {e}")));
1031 }
1032
1033 fn try_open_session(
1034 &mut self,
1035 id: QueueItemId,
1036 source: Source,
1037 info: Option<buffer::StreamInfo>,
1038 seek_ms: u64,
1039 start: Run,
1040 ) -> Result<(), PlayerError> {
1041 if self.renderer.is_some() {
1042 return self.try_open_on_renderer(id, source, info, seek_ms, start);
1043 }
1044 self.stop_engine();
1045 let mut info = match info {
1046 Some(info) => info,
1047 None => buffer::probe_file(source.path())?,
1048 };
1049 if info.duration_ms == 0
1052 && let Some(known) = self.shared_state.get_item(id).and_then(|i| i.duration_ms)
1053 {
1054 info.duration_ms = known;
1055 }
1056 let path = source.path().to_path_buf();
1057 let streaming = matches!(source, Source::Stream(_));
1058
1059 let (first, stream) = match source {
1060 Source::File(path) => (buffer::SourceEntry::from_file(id, path), None),
1061 Source::Stream(source) => {
1062 self.stream_mode = source.mode;
1064 let live = LiveStream {
1065 feed: source.bytes_written.clone(),
1066 abandoned: Default::default(),
1067 };
1068 let status = {
1069 let downloading = self.stream_status_fn(id);
1070 let abandoned = live.abandoned.clone();
1071 Arc::new(move || {
1072 if abandoned.load(std::sync::atomic::Ordering::Acquire) {
1073 streaming::StreamStatus::Failed
1074 } else {
1075 downloading()
1076 }
1077 }) as Arc<dyn Fn() -> streaming::StreamStatus + Send + Sync>
1078 };
1079 let entry = buffer::SourceEntry {
1080 id,
1081 path: source.path.clone(),
1082 hint: hint_for(&source.path),
1083 make_mss: Box::new(move || {
1084 let partial = streaming::PartialFileSource::open(
1085 &source.path,
1086 source.bytes_written.clone(),
1087 source.total,
1088 status.clone(),
1089 source.mode,
1090 )?;
1091 Ok(symphonia::core::io::MediaSourceStream::new(
1092 Box::new(partial),
1093 Default::default(),
1094 ))
1095 }),
1096 };
1097 (entry, Some(live))
1098 }
1099 };
1100
1101 let track = TrackInfo {
1102 id,
1103 path: path.clone(),
1104 codec: info.codec.clone(),
1105 sample_rate: info.sample_rate,
1106 bit_depth: info.bit_depth,
1107 bitrate_kbps: info.bitrate_kbps,
1108 channels: info.channels,
1109 duration_ms: info.duration_ms,
1110 };
1111 self.shared_state.set_position_ms(seek_ms);
1114 self.on_track_changed(id, seek_ms);
1115 log::info!(
1116 "{}: {} ({:?}) — {} {}Hz/{}ch, {}ms{}",
1117 if streaming { "streaming" } else { "playing" },
1118 path.display(),
1119 id,
1120 info.codec,
1121 info.sample_rate,
1122 info.channels,
1123 info.duration_ms,
1124 if seek_ms > 0 {
1125 format!(" @{}ms", seek_ms)
1126 } else {
1127 String::new()
1128 }
1129 );
1130
1131 let (producer, consumer) = rtrb::RingBuffer::new(RING_BUFFER_SIZE);
1132 self.timeline.reset();
1133 let lookahead = Arc::new(parking_lot::Mutex::new(Vec::new()));
1134 let next_track = self.decode_cursor(id, lookahead.clone());
1135 let processing = self.processing();
1138 let depth_limit = processing.depth_limit.clone();
1139 depth_limit.store(
1143 self.assumed_depth(&info),
1144 std::sync::atomic::Ordering::Relaxed,
1145 );
1146 self.session += 1;
1147 let session = self.session;
1148 let finish_tx = self.commands.tx.clone();
1149 let decode_handle = buffer::start_decode(
1150 first,
1151 producer,
1152 seek_ms,
1153 move || {
1154 let (next_id, next_path) = next_track()?;
1155 Some(buffer::SourceEntry::from_file(next_id, next_path))
1156 },
1157 self.timeline.clone(),
1158 Some(self.viz_buffer.clone()),
1159 processing,
1160 move |stop| {
1161 commands::send_unless_stopped(
1162 &finish_tx,
1163 PlayerCommand::DecodeFinished(session),
1164 stop,
1165 );
1166 },
1167 )?;
1168
1169 let (engine, rate_watch) = self.create_engine_for(&info, consumer)?;
1170 self.timeline.set_presentation(engine.presentation());
1171 depth_limit.store(
1172 self.backend.output_depth().unwrap_or(0),
1173 std::sync::atomic::Ordering::Relaxed,
1174 );
1175 let dsp = self
1177 .dsp
1178 .as_ref()
1179 .and_then(|c| c.setup.as_ref())
1180 .map(|s| s.status(info.sample_rate));
1181 if start == Run::Playing {
1184 if std::mem::take(&mut self.quick_start) {
1185 engine.fade_in_quickly()?;
1186 } else {
1187 engine.start()?;
1188 }
1189 self.check_output_started();
1190 }
1191 self.transport = Transport::Loaded(Session {
1192 track,
1193 run: start,
1194 lookahead,
1195 output: Output::Local(Local {
1196 engine,
1197 decode_handle,
1198 stream,
1199 _rate_watch: rate_watch,
1200 dsp,
1201 }),
1202 });
1203 Ok(())
1204 }
1205
1206 fn decode_cursor(
1216 &self,
1217 id: QueueItemId,
1218 steps: Arc<parking_lot::Mutex<Vec<state::Lookahead>>>,
1219 ) -> impl Fn() -> Option<(QueueItemId, PathBuf)> + Send + 'static {
1220 let state = self.shared_state.clone();
1221 let timeline = self.timeline.clone();
1222 move || {
1223 let mut steps = steps.lock();
1224 let mut step = match steps.last() {
1225 None => state.lookahead_after(id)?,
1226 Some(step) => state.lookahead_from(step)?,
1227 };
1228 step.boundary = timeline.boundary_count();
1229 let next = step.chosen.clone();
1230 steps.push(step);
1231 next
1232 }
1233 }
1234
1235 fn probe_stream_for_playback(
1250 &self,
1251 id: QueueItemId,
1252 path: &Path,
1253 bytes_written: Arc<crate::remote::downloads::ByteFeed>,
1254 total: u64,
1255 ) {
1256 let path = path.to_path_buf();
1257 let tx = self.commands.tx.clone();
1258 let hint = hint_for(&path);
1259
1260 let status = {
1265 let downloading = self.stream_status_fn(id);
1266 let state = self.shared_state.clone();
1267 Arc::new(move || {
1268 if state.is_cursor(id) {
1269 downloading()
1270 } else {
1271 streaming::StreamStatus::Failed
1272 }
1273 }) as Arc<dyn Fn() -> streaming::StreamStatus + Send + Sync>
1274 };
1275
1276 let spawned = thread::Builder::new()
1277 .name("koan-stream-probe".into())
1278 .spawn(move || {
1279 let attempt = |mode, wait: bool| {
1286 let open = if wait {
1287 streaming::PartialFileSource::open(
1288 &path,
1289 bytes_written.clone(),
1290 total,
1291 status.clone(),
1292 mode,
1293 )
1294 } else {
1295 streaming::PartialFileSource::open_for_probe(
1296 &path,
1297 bytes_written.clone(),
1298 total,
1299 status.clone(),
1300 mode,
1301 )
1302 };
1303 open.map_err(buffer::DecodeError::Io).and_then(|source| {
1304 let mss = symphonia::core::io::MediaSourceStream::new(
1305 Box::new(source),
1306 Default::default(),
1307 );
1308 buffer::probe_source(mss, &hint)
1309 })
1310 };
1311
1312 let info = match attempt(streaming::ProbeMode::Full, false) {
1316 Ok(info) => Some((info, streaming::ProbeMode::Full)),
1317 Err(e) => {
1318 log::info!(
1324 "stream probe: {} needs more than has arrived ({}), opening without a length",
1325 path.display(),
1326 e
1327 );
1328 let lengthless = lengthless_mode_for(&path);
1329 attempt(lengthless, true)
1330 .ok()
1331 .map(|info| (info, lengthless))
1332 }
1333 };
1334
1335 match info {
1336 Some((info, mode)) => {
1337 tx.send(PlayerCommand::StreamProbed {
1338 id,
1339 info: Box::new(info),
1340 mode,
1341 })
1342 .ok();
1343 }
1344 None => log::info!(
1347 "stream probe: {} cannot start early, waiting for the download",
1348 path.display()
1349 ),
1350 }
1351 });
1352
1353 if let Err(e) = spawned {
1354 log::warn!("stream probe: could not spawn for {:?}: {}", id, e);
1355 }
1356 }
1357
1358 fn stream_probed(
1361 &mut self,
1362 id: QueueItemId,
1363 info: buffer::StreamInfo,
1364 mode: streaming::ProbeMode,
1365 ) {
1366 let Some(waiting) = self.waiting().filter(|w| self.may_stream(w, id)) else {
1369 return;
1370 };
1371 if !self.shared_state.is_cursor(id) {
1372 return;
1373 }
1374
1375 match self.shared_state.item_playback_source(id) {
1376 Some(PlaybackSource::Ready(path)) => {
1378 if let Err(e) = self.start_playback(id, &path, 0, waiting.start) {
1379 self.start_failed("stream probe", &e);
1380 }
1381 }
1382 Some(PlaybackSource::Streaming {
1383 path,
1384 bytes_written,
1385 total,
1386 }) => {
1387 let source = StreamSource {
1388 path,
1389 bytes_written,
1390 total,
1391 mode,
1392 };
1393 if let Err(e) =
1394 self.open_session(id, Source::Stream(source), Some(info), 0, waiting.start)
1395 {
1396 log::error!("stream probe: streaming playback failed: {}", e);
1397 }
1398 }
1399 None => {}
1400 }
1401 }
1402
1403 fn stream_status_fn(
1407 &self,
1408 id: QueueItemId,
1409 ) -> Arc<dyn Fn() -> streaming::StreamStatus + Send + Sync> {
1410 let state = self.shared_state.clone();
1414 Arc::new(move || match state.item_state(id) {
1415 Some(ItemState::Ready) => streaming::StreamStatus::Complete,
1416 Some(ItemState::Failed(_)) => streaming::StreamStatus::Failed,
1417 _ => streaming::StreamStatus::Downloading,
1418 })
1419 }
1420
1421 pub fn seek(&mut self, position_ms: u64) {
1428 let Some(id) = self.session().map(|s| s.track.id) else {
1429 return;
1430 };
1431 let seekable = self.shared_state.seekable_ms();
1433 if seekable == 0 {
1434 log::debug!("seek declined: {:?} is not seekable yet", id);
1438 return;
1439 }
1440 let ceiling = seekable.min(
1441 self.shared_state
1442 .duration_ms()
1443 .saturating_sub(SEEK_END_GUARD_MS),
1444 );
1445 let clamped = position_ms.min(ceiling);
1446
1447 if self.seek_on_renderer(clamped) {
1450 return;
1451 }
1452 if let Err(e) = self.restart_current(clamped) {
1453 log::error!("seek failed: {}", e);
1454 }
1455 }
1456
1457 fn restart_current(&mut self, position_ms: u64) -> Result<(), PlayerError> {
1465 let Some(session) = self.session() else {
1466 return Ok(());
1467 };
1468 let (info, start) = (session.track.clone(), session.run);
1469
1470 match self.shared_state.item_playback_source(info.id) {
1471 Some(PlaybackSource::Streaming { .. }) if self.renderer.is_some() => {
1474 self.park(info.id, position_ms, start);
1475 return Ok(());
1476 }
1477 Some(PlaybackSource::Streaming {
1478 path,
1479 bytes_written,
1480 total,
1481 }) => {
1482 let known = buffer::StreamInfo {
1486 codec: info.codec.clone(),
1487 sample_rate: info.sample_rate,
1488 channels: info.channels,
1489 bit_depth: info.bit_depth,
1490 bitrate_kbps: info.bitrate_kbps,
1491 duration_ms: info.duration_ms,
1492 };
1493 let source = StreamSource {
1494 path,
1495 bytes_written,
1496 total,
1497 mode: self.stream_mode,
1498 };
1499 self.open_session(
1500 info.id,
1501 Source::Stream(source),
1502 Some(known),
1503 position_ms,
1504 start,
1505 )?;
1506 }
1507 Some(PlaybackSource::Ready(path)) => {
1508 self.start_playback(info.id, &path, position_ms, start)?;
1509 }
1510 None => return Ok(()),
1511 }
1512
1513 self.report(match start {
1514 Run::Playing => PlaybackReportState::Playing,
1515 Run::Paused => PlaybackReportState::Paused,
1516 });
1517 Ok(())
1518 }
1519
1520 pub fn next_track(&mut self) {
1522 match self.shared_state.advance_cursor_loadable() {
1523 Some(id) => self.play(id),
1524 None => {
1525 log::info!("no more tracks in playlist");
1526 self.stop_playback_and_clear_state();
1527 }
1528 }
1529 }
1530
1531 pub fn prev_track(&mut self) {
1535 if self.session().is_some() && self.shared_state.position_ms() > 1000 {
1536 if let Err(e) = self.restart_current(0) {
1537 log::error!("restart failed: {}", e);
1538 }
1539 return;
1540 }
1541 match self.shared_state.retreat_cursor() {
1542 Some((id, _)) => self.play(id),
1543 None => {
1544 if let Err(e) = self.restart_current(0) {
1546 log::error!("restart failed: {}", e);
1547 }
1548 }
1549 }
1550 }
1551
1552 pub fn pause(&mut self) {
1558 let fade = crate::config::Config::cached().playback.fade_on_pause;
1559 self.pause_with(if fade { Fade::Short } else { Fade::Cut });
1560 }
1561
1562 fn pause_with(&mut self, fade: Fade) {
1563 match &mut self.transport {
1564 Transport::Idle => return,
1565 Transport::Waiting(waiting) => {
1566 waiting.start = Run::Paused;
1567 return;
1568 }
1569 Transport::Loaded(session) => {
1570 if let Output::Local(local) = &session.output {
1571 match fade {
1572 Fade::Short if local.engine.is_running() => local.engine.fade_out(),
1577 _ => {
1578 if let Err(e) = local.engine.stop() {
1579 log::error!("pause failed: {}", e);
1580 return;
1581 }
1582 }
1583 }
1584 }
1585 session.run = Run::Paused;
1586 }
1587 }
1588 self.pause_renderer();
1589 self.report(PlaybackReportState::Paused);
1590 }
1591
1592 pub fn resume(&mut self) {
1599 self.answer_silence();
1600 if std::mem::take(&mut self.sleep_snap_on_resume) {
1601 self.set_sleep_gain(1.0, true);
1602 }
1603 let session = match &mut self.transport {
1604 Transport::Idle => {
1605 if let Some(id) = self.shared_state.cursor() {
1606 self.play(id);
1607 }
1608 return;
1609 }
1610 Transport::Waiting(waiting) => {
1611 let waiting = *waiting;
1612 self.cue(waiting.id, waiting.position_ms, Run::Playing);
1613 return;
1614 }
1615 Transport::Loaded(session) => session,
1616 };
1617 let Output::Local(local) = &session.output else {
1618 self.resume_renderer();
1619 return;
1620 };
1621 let engine = &local.engine;
1622 let resumed = if engine.is_running() || engine.is_faded_out() {
1625 self.lead_in_ends = None;
1626 engine.fade_in()
1627 } else {
1628 engine.start()
1629 };
1630 if let Err(e) = resumed {
1631 log::error!("resume failed: {}", e);
1632 return;
1633 }
1634 session.run = Run::Playing;
1635 self.check_output_started();
1636 self.wake_analyzer();
1637 self.report(PlaybackReportState::Playing);
1638 }
1639
1640 fn wake_analyzer(&self) {
1647 self.viz_snapshot.wake();
1648 }
1649
1650 pub fn stop(&mut self) {
1652 self.shared_state.clear_playlist();
1653 self.stop_playback_and_clear_state();
1654 }
1655
1656 fn assumed_depth(&self, info: &buffer::StreamInfo) -> u16 {
1664 if self.backend.narrows_depth() {
1665 info.bit_depth.unwrap_or(24)
1666 } else {
1667 0
1668 }
1669 }
1670
1671 fn check_output_started(&mut self) {
1673 self.output_check = Some((
1674 std::time::Instant::now() + OUTPUT_START_CHECK,
1675 self.timeline
1676 .samples_played_counter()
1677 .load(std::sync::atomic::Ordering::Relaxed),
1678 ));
1679 }
1680
1681 fn stop_engine(&mut self) {
1682 self.output_check = None;
1683 let playback = match std::mem::replace(&mut self.transport, Transport::Idle) {
1684 Transport::Loaded(playback) => playback,
1685 other => {
1686 self.transport = other;
1687 return;
1688 }
1689 };
1690 self.session += 1;
1691 self.dsp_restart = None;
1693 self.bank_listening();
1694 let local = match playback.output {
1695 Output::Local(local) => local,
1696 Output::Renderer(play) => {
1697 self.halt_renderer(*play);
1698 self.answer_silence();
1699 return;
1700 }
1701 };
1702 let Local {
1703 engine,
1704 mut decode_handle,
1705 stream,
1706 ..
1707 } = local;
1708
1709 let _ = engine.stop();
1710 decode_handle.signal_stop();
1713 if let Some(stream) = stream {
1714 stream.abandon();
1715 }
1716 decode_handle.stop();
1717 self.timeline.set_presentation(None);
1718 drop(engine);
1719 self.answer_silence();
1720 }
1721
1722 fn answer_silence(&mut self) {
1724 let position_ms = self.shared_state.position_ms();
1725 for reply in self.silence_waiters.drain(..) {
1726 let _ = reply.send(position_ms);
1727 }
1728 }
1729
1730 fn stop_playback_and_clear_state(&mut self) {
1732 self.forget_waiting();
1733 self.report(PlaybackReportState::Stopped);
1734 self.finish_play();
1735 self.stop_engine();
1736 self.timeline.reset();
1737 self.shared_state.set_position_ms(0);
1738 }
1739
1740 pub fn remove_from_playlist(&mut self, id: QueueItemId) {
1747 let was_cursor = self.shared_state.is_cursor(id);
1748 let resume_after = was_cursor
1749 .then(|| self.shared_state.item_before(id))
1750 .flatten();
1751 self.shared_state.remove_item(id);
1752 if was_cursor {
1753 self.resume_from(resume_after);
1754 let next = self.shared_state.advance_cursor_loadable();
1755 self.carry_on(next, self.intent());
1756 }
1757 }
1758
1759 fn resume_from(&self, before: Option<QueueItemId>) {
1764 if !self.shared_state.is_shuffled() {
1765 self.shared_state.set_cursor(before);
1766 }
1767 }
1768
1769 pub fn track_ready(&mut self, id: QueueItemId) {
1777 if !self.shared_state.is_cursor(id) {
1778 return;
1779 }
1780
1781 if let Some(waiting) = self.waiting().filter(|w| w.id == id) {
1782 log::info!("track_ready: opening {:?}", id);
1783 self.cue(id, waiting.position_ms, waiting.start);
1784 return;
1785 }
1786
1787 if self.session().is_some_and(|s| s.track.id == id) {
1788 log::info!(
1789 "track_ready: download complete while streaming {:?}, refreshing metadata",
1790 id
1791 );
1792 self.refresh_track_metadata(id);
1793 }
1794 }
1795
1796 pub fn track_stream_ready(&mut self, id: QueueItemId) {
1799 let Some(waiting) = self.waiting().filter(|w| self.may_stream(w, id)) else {
1800 return;
1801 };
1802 if !self.shared_state.is_cursor(id) {
1803 return;
1804 }
1805
1806 match self.shared_state.item_playback_source(id) {
1807 Some(PlaybackSource::Streaming {
1808 path,
1809 bytes_written,
1810 total,
1811 }) => {
1812 log::info!("track_stream_ready: probing partial file for {:?}", id);
1813 self.probe_stream_for_playback(id, &path, bytes_written, total);
1814 }
1815 Some(PlaybackSource::Ready(path)) => {
1816 log::info!(
1818 "track_stream_ready: track already ready, starting normal playback for {:?}",
1819 id
1820 );
1821 if let Err(e) = self.start_playback(id, &path, 0, waiting.start) {
1822 self.start_failed("stream ready", &e);
1823 }
1824 }
1825 None => {} }
1827 }
1828
1829 fn refresh_track_metadata(&mut self, id: QueueItemId) {
1833 use crate::index::metadata;
1834
1835 let path = match self.shared_state.item_path_if_ready(id) {
1836 Some(p) => p,
1837 None => return,
1838 };
1839
1840 match metadata::read_metadata(&path) {
1841 Ok(meta) => {
1842 self.shared_state.update_item_metadata(
1843 id,
1844 meta.title,
1845 meta.artist,
1846 meta.album_artist.unwrap_or_default(),
1847 meta.album,
1848 meta.duration_ms.map(|d| d as u64),
1849 );
1850
1851 if let Transport::Loaded(session) = &mut self.transport
1860 && session.track.id == id
1861 {
1862 let current = &mut session.track;
1863 let probed = buffer::probe_file(&path).ok();
1864 let duration_ms = probed
1865 .as_ref()
1866 .map(|s| s.duration_ms)
1867 .filter(|d| *d > current.duration_ms)
1868 .unwrap_or(current.duration_ms);
1869 if duration_ms != current.duration_ms {
1870 log::info!(
1871 "track_ready: duration corrected {}ms → {}ms",
1872 current.duration_ms,
1873 duration_ms
1874 );
1875 }
1876 current.duration_ms = duration_ms;
1877 current.path = path.clone();
1878 }
1879
1880 self.shared_state.signal_metadata_refresh();
1882 log::info!("track_ready: metadata refreshed for {:?}", id);
1883 }
1884 Err(e) => {
1885 log::warn!("track_ready: metadata refresh failed for {:?}: {}", id, e);
1886 }
1887 }
1888 }
1889
1890 fn on_track_changed(&mut self, id: QueueItemId, position_ms: u64) {
1897 if let Some(f) = self.in_flight.as_mut().filter(|f| f.item == id) {
1898 f.jump(position_ms);
1899 f.boundary = 0;
1901 return;
1902 }
1903 self.begin_play(id, position_ms, 0);
1904 }
1905
1906 fn begin_play(&mut self, id: QueueItemId, position_ms: u64, boundary: usize) {
1911 self.finish_play();
1912 self.shared_state.mark_played(id);
1913 let track_id = self.shared_state.item_db_id(id);
1914 let mut flight = InFlight::new(id, track_id, position_ms);
1915 flight.boundary = boundary;
1916 self.in_flight = Some(flight);
1917 if let (Some(track_id), Some(recorder)) = (track_id, self.history.as_ref()) {
1918 recorder.record(PlayEvent::Started {
1919 track_id,
1920 position_ms,
1921 });
1922 }
1923 }
1924
1925 fn bank_listening(&mut self) {
1929 let renderer_at = self
1931 .shared_state
1932 .renderer_clock()
1933 .map(|_| self.shared_state.position_ms());
1934 if let Some(f) = self.in_flight.as_mut()
1935 && let Some(at) = self.timeline.position_in(f.boundary).or(renderer_at)
1936 {
1937 f.advance(at);
1938 }
1939 }
1940
1941 fn report(&self, state: PlaybackReportState) {
1945 let Some(track_id) = self.in_flight.as_ref().and_then(InFlight::track_id) else {
1946 return;
1947 };
1948 if let Some(recorder) = self.history.as_ref() {
1949 recorder.record(PlayEvent::Playback(PlaybackReport {
1950 track_id,
1951 state,
1952 position_ms: self.shared_state.position_ms(),
1953 }));
1954 }
1955 }
1956
1957 fn finish_play(&mut self) -> Option<PlayEvent> {
1960 self.bank_listening();
1961 let flight = self.in_flight.take()?;
1962 let event = PlayEvent::Finished {
1963 track_id: flight.track_id()?,
1964 listened_ms: flight.listened_ms(),
1965 };
1966 if let Some(recorder) = self.history.as_ref() {
1967 recorder.record(event);
1968 }
1969 Some(event)
1970 }
1971
1972 pub fn update_playback_state(&mut self) {
1976 self.sleep_tick();
1977 if let Some(SleepSet { at: Some(at), .. }) = self.sleep
1978 && std::time::Instant::now() >= at
1979 {
1980 self.sleep = None;
1981 log::info!("sleep timer: time's up");
1982 if self.intent() == Some(Run::Playing) {
1984 self.pause_with(Fade::Cut);
1985 }
1986 self.end_sleep_fade();
1987 }
1988
1989 if self.session().is_some()
1990 && self
1991 .lead_in_ends
1992 .is_some_and(|end| std::time::Instant::now() >= end)
1993 {
1994 self.lead_in_ends = None;
1998 self.shared_state.changed();
1999 }
2000
2001 if let Some((at, from)) = self.output_check
2002 && std::time::Instant::now() >= at
2003 {
2004 self.shared_state.changed();
2007 let moved = self
2008 .timeline
2009 .samples_played_counter()
2010 .load(std::sync::atomic::Ordering::Relaxed)
2011 > from;
2012 self.output_check = (!moved && self.intent() == Some(Run::Playing))
2013 .then(|| (std::time::Instant::now() + OUTPUT_START_CHECK, from));
2014 }
2015
2016 if let Some(session) = self.session()
2017 && session.run == Run::Paused
2018 && let Some(engine) = session.engine()
2019 && engine.is_running()
2020 && engine.is_silent()
2021 {
2022 if let Err(e) = engine.stop() {
2023 log::error!("stopping after fade failed: {}", e);
2024 }
2025 self.answer_silence();
2026 }
2027
2028 if let Some(since) = self.dsp_restart {
2029 let faded = self
2030 .session()
2031 .and_then(Session::engine)
2032 .is_none_or(|e| e.is_silent());
2033 if faded || since.elapsed() >= QUICK_FADE_LIMIT {
2034 self.dsp_restart = None;
2035 self.quick_start = true;
2036 self.restart_on_current_track();
2037 self.quick_start = false;
2038 }
2039 }
2040
2041 self.release_idle_session();
2042 self.follow_playhead();
2043 self.renderer_tick();
2044 self.publish();
2045 }
2046
2047 fn follow_playhead(&mut self) {
2056 let before = self.in_flight.as_ref().map(|f| (f.item, f.boundary));
2057 self.move_with_playhead();
2058 let after = self.in_flight.as_ref().map(|f| (f.item, f.boundary));
2059 if let (Some((ended, _)), Some((next, _))) = (before, after)
2060 && before != after
2061 && self.sleeps_between(Some(ended), Some(next))
2062 {
2063 self.pause_with(Fade::Cut);
2064 self.end_sleep_fade();
2065 }
2066 }
2067
2068 fn move_with_playhead(&mut self) {
2069 if self.session().is_none() {
2070 return;
2071 }
2072 let Some(playhead) = self.timeline.playhead() else {
2073 return;
2074 };
2075 let id = playhead.id;
2076 if self
2077 .in_flight
2078 .as_ref()
2079 .is_none_or(|f| f.item != id || f.boundary != playhead.boundary)
2080 {
2081 self.begin_play(id, playhead.position_ms, playhead.boundary);
2082 }
2083 let Transport::Loaded(session) = &mut self.transport else {
2084 return;
2085 };
2086 let pass = step_pass(&session.lookahead.lock(), playhead.boundary);
2087 if session.track.id == id {
2088 return;
2089 }
2090 let Some((id, path, info, _)) = self.timeline.current_playback() else {
2091 return;
2092 };
2093 log::info!("timeline: now playing {:?}", id);
2094 session.track = TrackInfo {
2095 id,
2096 path,
2097 codec: info.codec,
2098 sample_rate: info.sample_rate,
2099 bit_depth: info.bit_depth,
2100 bitrate_kbps: info.bitrate_kbps,
2101 channels: info.channels,
2102 duration_ms: info.duration_ms,
2103 };
2104 self.shared_state.move_on_to(id, pass);
2105 }
2106
2107 fn sleeps_between(&mut self, ended: Option<QueueItemId>, next: Option<QueueItemId>) -> bool {
2111 let record = |id: Option<QueueItemId>| {
2112 id.and_then(|id| self.shared_state.get_item(id))
2113 .map(|i| (i.album, i.album_artist))
2114 };
2115 let due = match self.sleep.map(|s| s.sleep) {
2116 Some(Sleep::EndOfTrack) => true,
2117 Some(Sleep::EndOfRecord) => next.is_none() || record(ended) != record(next),
2118 _ => false,
2119 };
2120 if due {
2121 log::info!(
2122 "sleep timer: stopping at the end of the {}",
2123 match self.sleep {
2124 Some(SleepSet {
2125 sleep: Sleep::EndOfTrack,
2126 ..
2127 }) => "track",
2128 _ => "record",
2129 }
2130 );
2131 self.sleep = None;
2132 }
2133 due
2134 }
2135
2136 fn set_sleep_timer(&mut self, timer: Option<SleepTimer>) {
2138 self.restore_sleep_fade();
2139 self.sleep = timer.map(|timer| match timer {
2140 SleepTimer::After { minutes } => {
2141 let after = std::time::Duration::from_secs(u64::from(minutes) * 60);
2142 let unix_ms = std::time::SystemTime::now()
2143 .duration_since(std::time::UNIX_EPOCH)
2144 .unwrap_or_default()
2145 + after;
2146 SleepSet {
2147 sleep: Sleep::At {
2148 unix_ms: unix_ms.as_millis() as u64,
2149 },
2150 at: Some(std::time::Instant::now() + after),
2151 fade: Some(sleep::fade_length(minutes)),
2152 }
2153 }
2154 SleepTimer::EndOfTrack => SleepSet {
2155 sleep: Sleep::EndOfTrack,
2156 at: None,
2157 fade: None,
2158 },
2159 SleepTimer::EndOfRecord => SleepSet {
2160 sleep: Sleep::EndOfRecord,
2161 at: None,
2162 fade: None,
2163 },
2164 });
2165 log::info!("sleep timer: {:?}", self.sleep.map(|s| s.sleep));
2166 }
2167
2168 pub fn track_failed(&mut self, id: QueueItemId) {
2176 let Some(waiting) = self.waiting().filter(|w| w.id == id) else {
2177 return;
2178 };
2179 if !self.shared_state.is_cursor(id) {
2180 return;
2181 }
2182 log::info!("track {:?} cannot load, moving on", id);
2183 let next = self.shared_state.advance_cursor_loadable();
2184 self.carry_on(next, Some(waiting.start));
2185 }
2186
2187 fn on_decode_finished(&mut self, session: u64) {
2193 if session != self.session || self.renderer_stream_finished() {
2194 return;
2195 }
2196 log::info!("decode finished, checking for next track");
2197 self.track_ended();
2198 }
2199
2200 fn track_ended(&mut self) {
2214 self.bank_listening();
2215 let ended = self.in_flight.as_ref().map(|f| f.item);
2216 let heard = self.in_flight.as_ref().is_some_and(|f| f.listened_ms() > 0);
2217 if !heard
2223 && let Some(id) = self.session().map(|s| s.track.id)
2224 && self.shared_state.is_cursor(id)
2225 && matches!(self.shared_state.item_state(id), Some(ItemState::Pending))
2226 {
2227 log::info!("track {id:?} ended unheard before its download did; waiting for it");
2228 let start = self.intent().unwrap_or(Run::Playing);
2229 self.park(id, 0, start);
2230 return;
2231 }
2232 if self.mode.repeat != Repeat::Off && !heard {
2233 log::info!("track ended with nothing heard; not repeating it");
2234 self.stop_playback_and_clear_state();
2235 return;
2236 }
2237 let again = (self.mode.repeat == Repeat::One)
2238 .then(|| self.shared_state.cursor())
2239 .flatten()
2240 .filter(|id| {
2241 self.shared_state
2242 .item_state(*id)
2243 .is_some_and(|s| !matches!(s, ItemState::Failed(_)))
2244 });
2245 let next = again.or_else(|| self.shared_state.advance_cursor_loadable());
2246 if next.is_some() && next == ended {
2247 self.finish_play();
2248 }
2249 let sleeps = self.intent().is_some() && self.sleeps_between(ended, next);
2250 let intent = if sleeps {
2251 Some(Run::Paused)
2252 } else {
2253 self.intent()
2254 };
2255 self.carry_on(next, intent);
2256 if sleeps {
2257 self.end_sleep_fade();
2258 }
2259 }
2260
2261 fn revoke_stale_lookahead(&mut self) {
2271 if self.session().is_none() {
2272 return;
2273 }
2274 self.update_playback_state();
2275 let Some(playhead) = self.timeline.playhead() else {
2276 return;
2277 };
2278 let position_ms = playhead.position_ms;
2279 let Some(session) = self.session().filter(|s| s.track.id == playhead.id) else {
2280 return;
2281 };
2282 let stale = {
2283 let steps = session.lookahead.lock();
2284 steps
2288 .iter()
2289 .filter(|step| step.boundary > playhead.boundary)
2290 .any(|step| !self.shared_state.still_follows(step))
2291 };
2292 if !stale {
2293 return;
2294 }
2295 log::info!("queue changed under the lookahead, restarting at {position_ms}ms");
2296 if let Err(e) = self.restart_current(position_ms) {
2297 log::error!("restart after a queue edit failed: {}", e);
2298 }
2299 }
2300
2301 fn snapshot_for_undo(
2305 &self,
2306 ids: &[QueueItemId],
2307 ) -> Vec<(Box<state::PlaylistItem>, Option<QueueItemId>)> {
2308 self.shared_state
2309 .items_before(ids)
2310 .into_iter()
2311 .filter_map(|(id, after)| Some((Box::new(self.shared_state.get_item(id)?), after)))
2312 .collect()
2313 }
2314
2315 fn push_undo(&mut self, entry: UndoEntry) {
2317 if let Some(ref mut batch) = self.batch_buffer {
2318 batch.push(entry);
2319 } else {
2320 self.undo_stack.push(entry);
2321 }
2322 }
2323
2324 pub fn process_command(&mut self, cmd: PlayerCommand) {
2326 if cmd.asks_to_play() && !matches!(cmd, PlayerCommand::Cue { .. })
2333 || matches!(
2334 cmd,
2335 PlayerCommand::UseRenderer(_)
2336 | PlayerCommand::SetOutputDevice(_)
2337 | PlayerCommand::ClearOutputDevice
2338 | PlayerCommand::Pause
2339 | PlayerCommand::PauseAndReport(_)
2340 | PlayerCommand::Stop
2341 | PlayerCommand::ClearPlaylist
2342 | PlayerCommand::ReplacePlaylist { .. }
2343 )
2344 {
2345 self.resume_renderer = false;
2346 self.held_run = None;
2347 }
2348 let cmd = match cmd {
2351 PlayerCommand::Cue {
2352 id,
2353 position_ms,
2354 play: true,
2355 } if self.resume_renderer => {
2356 self.held_run = Some(Run::Playing);
2357 PlayerCommand::Cue {
2358 id,
2359 position_ms,
2360 play: false,
2361 }
2362 }
2363 cmd => cmd,
2364 };
2365 let edits_queue = matches!(
2366 cmd,
2367 PlayerCommand::AddToPlaylist(_)
2368 | PlayerCommand::InsertInPlaylist { .. }
2369 | PlayerCommand::RemoveFromPlaylist(_)
2370 | PlayerCommand::RemoveFromPlaylistBatch(_)
2371 | PlayerCommand::MoveInPlaylist { .. }
2372 | PlayerCommand::MoveItemsInPlaylist { .. }
2373 | PlayerCommand::ReorderPlaylist(_)
2374 | PlayerCommand::Undo
2375 | PlayerCommand::Redo
2376 | PlayerCommand::SetShuffle(_)
2377 | PlayerCommand::SetRepeat(_)
2378 | PlayerCommand::RestorePlayMode(_)
2379 );
2380 self.apply_command(cmd);
2381 if edits_queue {
2382 self.revoke_stale_lookahead();
2383 }
2384 self.publish();
2385 }
2386
2387 fn apply_command(&mut self, cmd: PlayerCommand) {
2388 match cmd {
2389 PlayerCommand::Play(id) => {
2390 self.shared_state.pick(id);
2391 self.play(id);
2392 }
2393 PlayerCommand::Cue {
2394 id,
2395 position_ms,
2396 play,
2397 } => self.cue(
2398 id,
2399 position_ms,
2400 if play { Run::Playing } else { Run::Paused },
2401 ),
2402 PlayerCommand::Pause => {
2403 self.paused_in_sleep_fade();
2404 self.pause();
2405 }
2406 PlayerCommand::Barrier(reply) => {
2409 let _ = reply.send(());
2410 }
2411 PlayerCommand::PauseAndReport(reply) => {
2412 self.pause();
2413 self.silence_waiters.push(reply);
2414 if self
2415 .session()
2416 .is_none_or(|p| p.engine().is_none_or(|e| !e.is_running()))
2417 {
2418 self.answer_silence();
2419 }
2420 }
2421 PlayerCommand::Resume => self.resume(),
2422 PlayerCommand::Stop => self.stop(),
2423 PlayerCommand::Seek(pos) => self.seek(pos),
2424 PlayerCommand::NextTrack => self.next_track(),
2425 PlayerCommand::PrevTrack => self.prev_track(),
2426 PlayerCommand::AddToPlaylist(items) => {
2427 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
2428 self.shared_state.add_items(items);
2429 self.push_undo(UndoEntry::Added { ids });
2430 }
2431 PlayerCommand::AddAndPlay(items) => {
2432 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
2433 self.shared_state.add_items(items);
2434 self.push_undo(UndoEntry::Added { ids: ids.clone() });
2435 self.revoke_stale_lookahead();
2436 if let Some(&first) = ids.first() {
2437 self.shared_state.pick(first);
2438 self.play(first);
2439 }
2440 }
2441 PlayerCommand::UpdatePaths(updates) => {
2442 self.shared_state.update_paths(&updates);
2443 if let Transport::Loaded(session) = &mut self.transport
2444 && let Some((_, new_path)) =
2445 updates.iter().find(|(id, _)| *id == session.track.id)
2446 {
2447 session.track.path = new_path.clone();
2448 }
2449 }
2450 PlayerCommand::InsertInPlaylist { items, after } => {
2451 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
2452 self.shared_state.insert_items_after(items, after);
2453 self.push_undo(UndoEntry::Inserted { ids });
2454 }
2455 PlayerCommand::ClearPlaylist => self.clear_playlist(),
2456 PlayerCommand::ReplacePlaylist {
2457 items,
2458 start,
2459 position_ms,
2460 play,
2461 mode,
2462 } => {
2463 if items.is_empty() {
2464 self.clear_playlist();
2465 return;
2466 }
2467 let start_id = items.get(start).unwrap_or(&items[0]).id;
2468 self.stop_playback_and_clear_state();
2471 if let Some(mode) = mode.play_mode() {
2472 self.mode = mode;
2473 self.shared_state.set_play_mode(mode);
2479 self.shared_state.set_shuffled(mode.shuffle);
2480 }
2481 let (old_items, cursor) = self.shared_state.replace_playlist(items);
2482 let start_id = match mode {
2483 QueueMode::Shuffled => self
2484 .shared_state
2485 .advance_cursor_loadable()
2486 .unwrap_or(start_id),
2487 _ => start_id,
2488 };
2489 self.push_undo(UndoEntry::Replaced {
2490 items: old_items,
2491 cursor,
2492 });
2493 self.cue(
2494 start_id,
2495 position_ms,
2496 if play { Run::Playing } else { Run::Paused },
2497 );
2498 }
2499 PlayerCommand::RemoveFromPlaylist(id) => {
2500 let item = self.shared_state.get_item(id);
2501 let after = self.shared_state.item_before(id);
2502 self.remove_from_playlist(id);
2503 if let Some(item) = item {
2504 self.push_undo(UndoEntry::Removed {
2505 items: vec![(Box::new(item), after)],
2506 });
2507 }
2508 }
2509 PlayerCommand::RemoveFromPlaylistBatch(ids) => {
2510 let items_with_pos = self.snapshot_for_undo(&ids);
2514 let intent = self.intent();
2515 let resume_after = match self.shared_state.cursor() {
2516 Some(cursor) if ids.contains(&cursor) => {
2517 Some(self.shared_state.surviving_item_before(cursor, &ids))
2518 }
2519 _ => None,
2520 };
2521
2522 self.shared_state.remove_items(&ids);
2523
2524 if let Some(resume_after) = resume_after {
2525 self.resume_from(resume_after);
2526 let next = self.shared_state.advance_cursor_loadable();
2527 self.carry_on(next, intent);
2528 }
2529
2530 if !items_with_pos.is_empty() {
2531 self.push_undo(UndoEntry::Removed {
2532 items: items_with_pos,
2533 });
2534 }
2535 }
2536 PlayerCommand::MoveInPlaylist { id, target, after } => {
2537 let was_after = self.shared_state.item_before(id);
2538 self.shared_state.move_item(id, target, after);
2539 self.push_undo(UndoEntry::Moved { id, was_after });
2540 }
2541 PlayerCommand::MoveItemsInPlaylist { ids, target, after } => {
2542 let entries = self.shared_state.items_before(&ids);
2543 self.shared_state.move_items(&ids, target, after);
2544 self.push_undo(UndoEntry::MovedBatch { entries });
2545 }
2546 PlayerCommand::ReorderPlaylist(order) => {
2547 let entries = self.shared_state.items_before(&order);
2551 self.shared_state.reorder_to(&order);
2552 self.push_undo(UndoEntry::MovedBatch { entries });
2553 }
2554 PlayerCommand::TrackReady(id) => self.track_ready(id),
2555 PlayerCommand::DecodeFinished(session) => self.on_decode_finished(session),
2556 PlayerCommand::TrackQueued => {}
2559 PlayerCommand::TrackStreamReady(id) => self.track_stream_ready(id),
2560 PlayerCommand::StreamProbed { id, info, mode } => self.stream_probed(id, *info, mode),
2561 PlayerCommand::TrackFailed(id) => self.track_failed(id),
2562 PlayerCommand::CacheTracks(ids) => {
2563 if let Some(downloads) = &self.downloads {
2564 downloads.cache(ids);
2565 }
2566 }
2567 PlayerCommand::Undo => self.execute_undo(),
2568 PlayerCommand::Redo => self.execute_redo(),
2569 PlayerCommand::BeginUndoBatch => {
2570 self.batch_buffer = Some(Vec::new());
2571 }
2572 PlayerCommand::EndUndoBatch => {
2573 if let Some(entries) = self.batch_buffer.take() {
2574 if entries.len() == 1 {
2575 self.undo_stack.push(entries.into_iter().next().unwrap());
2577 } else if !entries.is_empty() {
2578 self.undo_stack.push(UndoEntry::Batch(entries));
2579 }
2580 }
2581 }
2582 PlayerCommand::SetOutputDevice(name) => self.set_output_device(name),
2583 PlayerCommand::RestartOutput => {
2584 log::info!("restarting audio output");
2585 self.restart_on_current_track();
2586 }
2587 PlayerCommand::ClearOutputDevice => self.clear_output_device(),
2588 PlayerCommand::ReloadDsp => self.reload_dsp(),
2589 PlayerCommand::RouteChanged => {
2590 crate::audio::follow_route();
2591 self.reload_dsp();
2592 }
2593 PlayerCommand::UseRenderer(connection) => {
2594 self.remember_renderer(connection.as_deref());
2595 self.use_renderer(connection);
2596 }
2597 PlayerCommand::ResumeRenderer(connection) => {
2598 if std::mem::take(&mut self.resume_renderer) && self.renderer.is_none() {
2599 log::info!(
2600 "upnp: back to {}, as last time",
2601 connection.session.renderer().name
2602 );
2603 self.use_renderer(Some(connection));
2604 if self.held_run.take() == Some(Run::Playing) {
2605 self.resume();
2606 }
2607 } else {
2608 log::info!(
2609 "upnp: not going back to {}: playback or the output moved first",
2610 connection.session.renderer().name
2611 );
2612 }
2613 }
2614 PlayerCommand::ResumeRendererMissed => {
2615 if std::mem::take(&mut self.resume_renderer)
2616 && self.held_run.take() == Some(Run::Playing)
2617 {
2618 log::info!("upnp: playing here what was held for the renderer");
2619 self.resume();
2620 }
2621 }
2622 PlayerCommand::ReleaseRenderer(reply) => {
2623 self.release_renderer();
2624 let _ = reply.send(());
2625 }
2626 PlayerCommand::SetRendererVolume(volume) => self.set_renderer_volume(volume),
2627 PlayerCommand::Renderer { session, event } => self.on_renderer_event(session, event),
2628 PlayerCommand::SetShuffle(on) => self.set_shuffle(on),
2629 PlayerCommand::SetRepeat(repeat) => self.mode.repeat = repeat,
2630 PlayerCommand::RestorePlayMode(mode) => {
2631 self.mode = mode;
2632 self.shared_state.set_shuffled(mode.shuffle);
2633 }
2634 PlayerCommand::SetSleepTimer(timer) => self.set_sleep_timer(timer),
2635 }
2636 }
2637
2638 fn set_shuffle(&mut self, on: bool) {
2643 self.mode.shuffle = on;
2644 self.shared_state.set_shuffled(on);
2645 }
2646
2647 fn clear_playlist(&mut self) {
2651 self.stop_playback_and_clear_state();
2652 let (items, cursor) = self.shared_state.snapshot_playlist();
2653 self.shared_state.clear_playlist();
2654 self.push_undo(UndoEntry::Replaced { items, cursor });
2655 }
2656
2657 fn apply_entry(&mut self, entry: UndoEntry) -> UndoEntry {
2659 match entry {
2660 UndoEntry::Added { ids } | UndoEntry::Inserted { ids } => {
2661 let items_with_pos = self.snapshot_for_undo(&ids);
2663 self.shared_state.remove_items(&ids);
2664 UndoEntry::Removed {
2665 items: items_with_pos,
2666 }
2667 }
2668 UndoEntry::Removed { items } => {
2669 let mut ids = Vec::with_capacity(items.len());
2671 for (item, after) in items {
2672 ids.push(item.id);
2673 self.shared_state.insert_item_at(*item, after);
2674 }
2675 UndoEntry::Added { ids }
2676 }
2677 UndoEntry::Moved { id, was_after } => {
2678 let current_after = self.shared_state.item_before(id);
2679 self.shared_state.move_item_to(id, was_after);
2680 UndoEntry::Moved {
2681 id,
2682 was_after: current_after,
2683 }
2684 }
2685 UndoEntry::MovedBatch { entries } => {
2686 let ids: Vec<QueueItemId> = entries.iter().map(|(id, _)| *id).collect();
2687 let current_positions = self.shared_state.items_before(&ids);
2688 self.shared_state.move_items_to(&entries);
2689 UndoEntry::MovedBatch {
2690 entries: current_positions,
2691 }
2692 }
2693 UndoEntry::Replaced { items, cursor } => {
2694 let (current_items, current_cursor) = self.shared_state.snapshot_playlist();
2695 self.shared_state.restore_playlist(items, cursor);
2696 UndoEntry::Replaced {
2697 items: current_items,
2698 cursor: current_cursor,
2699 }
2700 }
2701 UndoEntry::Batch(entries) => {
2702 let mut inverses: Vec<_> = entries
2704 .into_iter()
2705 .rev()
2706 .map(|e| self.apply_entry(e))
2707 .collect();
2708 inverses.reverse();
2709 UndoEntry::Batch(inverses)
2710 }
2711 }
2712 }
2713
2714 fn reconcile_playback(&mut self) {
2733 let orphaned = match &self.transport {
2734 Transport::Idle => false,
2735 Transport::Waiting(waiting) => self.shared_state.get_item(waiting.id).is_none(),
2736 Transport::Loaded(session) => self.shared_state.get_item(session.track.id).is_none(),
2737 };
2738 if !orphaned {
2739 return;
2740 }
2741 let cursor = self.shared_state.cursor();
2745 self.carry_on(cursor, self.intent());
2746 }
2747
2748 fn execute_undo(&mut self) {
2750 let Some(entry) = self.undo_stack.pop_undo() else {
2751 return;
2752 };
2753 let inverse = self.apply_entry(entry);
2754 self.undo_stack.push_redo(inverse);
2755 self.reconcile_playback();
2756 }
2757
2758 fn execute_redo(&mut self) {
2760 let Some(entry) = self.undo_stack.pop_redo() else {
2761 return;
2762 };
2763 let inverse = self.apply_entry(entry);
2764 self.undo_stack.push_undo_keep_redo(inverse);
2765 self.reconcile_playback();
2766 }
2767
2768 pub fn run(&mut self) {
2774 use crossbeam_channel::RecvTimeoutError;
2775
2776 let rx = self.commands.rx.clone();
2777 loop {
2778 let received = match self.next_wake() {
2779 Some(at) => rx.recv_deadline(at),
2780 None => rx.recv().map_err(|_| RecvTimeoutError::Disconnected),
2781 };
2782 match received {
2783 Ok(cmd) => self.process_command(cmd),
2784 Err(RecvTimeoutError::Timeout) => {}
2785 Err(RecvTimeoutError::Disconnected) => break,
2786 }
2787 self.update_playback_state();
2788 }
2789 self.stop();
2790 }
2791
2792 fn release_idle_session(&mut self) {
2797 if self.session_release_due(
2798 self.wants_output(),
2799 crate::audio::session_held(),
2800 std::time::Instant::now(),
2801 ) {
2802 log::info!("output idle: releasing the audio session");
2803 crate::audio::release_session();
2804 }
2805 }
2806
2807 fn wants_output(&self) -> bool {
2809 self.intent() == Some(Run::Playing)
2810 || self
2811 .session()
2812 .and_then(Session::engine)
2813 .is_some_and(|e| e.is_running())
2814 }
2815
2816 fn session_release_due(&mut self, playing: bool, held: bool, now: std::time::Instant) -> bool {
2819 if playing || !held {
2820 self.session_release_at = None;
2821 return false;
2822 }
2823 match self.session_release_at {
2824 None => {
2825 self.session_release_at = Some(now + SESSION_GRACE);
2826 false
2827 }
2828 Some(at) if now >= at => {
2829 self.session_release_at = None;
2830 true
2831 }
2832 Some(_) => false,
2833 }
2834 }
2835
2836 fn next_wake(&self) -> Option<std::time::Instant> {
2840 [
2841 self.next_event(),
2842 self.sleep.and_then(|s| s.at),
2843 self.sleep_wake(),
2844 self.session_release_at,
2845 ]
2846 .into_iter()
2847 .flatten()
2848 .min()
2849 }
2850
2851 fn next_event(&self) -> Option<std::time::Instant> {
2854 let session = self.session()?;
2855 let now = std::time::Instant::now();
2856 let Output::Local(local) = &session.output else {
2857 let next_track = (session.run == Run::Playing && self.streaming_to_renderer())
2859 .then(|| self.timeline.until_next_track())
2860 .flatten()
2861 .map(|left| now + left + BOUNDARY_SLACK);
2862 return match (next_track, self.renderer_deadline()) {
2863 (Some(a), Some(b)) => Some(a.min(b)),
2864 (a, b) => a.or(b),
2865 };
2866 };
2867 if let Some(since) = self.dsp_restart {
2868 return Some(dsp_wake(since, local.engine.period(), now));
2869 }
2870 match session.run {
2871 Run::Playing => {
2872 let next_track = self
2873 .timeline
2874 .until_next_track()
2875 .map(|left| now + left + BOUNDARY_SLACK);
2876 [
2877 next_track,
2878 self.lead_in_ends,
2879 self.output_check.map(|(at, _)| at),
2880 ]
2881 .into_iter()
2882 .flatten()
2883 .min()
2884 }
2885 Run::Paused if local.engine.is_running() => Some(now + FADE_CHECK),
2886 Run::Paused => None,
2887 }
2888 }
2889
2890 fn remember_renderer(&self, connection: Option<&crate::upnp::Connection>) {
2896 let renderer = connection.map(|c| c.session.renderer());
2897 if let Err(e) = crate::config::Config::persist(|cfg| {
2898 cfg.playback.renderer = renderer.map(|r| r.udn.clone());
2899 cfg.playback.renderer_name = renderer.map(|r| r.name.clone());
2900 }) {
2901 log::error!("failed to save the output: {e}");
2902 }
2903 }
2904
2905 pub fn spawn() -> (
2908 Arc<SharedPlayerState>,
2909 Arc<PlaybackTimeline>,
2910 Arc<VizSnapshot>,
2911 crossbeam_channel::Sender<PlayerCommand>,
2912 ) {
2913 Self::spawn_with(false)
2914 }
2915
2916 pub fn spawn_for_listening() -> (
2922 Arc<SharedPlayerState>,
2923 Arc<PlaybackTimeline>,
2924 Arc<VizSnapshot>,
2925 crossbeam_channel::Sender<PlayerCommand>,
2926 ) {
2927 Self::spawn_with(true)
2928 }
2929
2930 fn renderer_to_resume(listening: bool) -> Option<String> {
2933 listening
2934 .then(|| crate::config::Config::cached().playback.renderer.clone())
2935 .flatten()
2936 }
2937
2938 fn spawn_with(
2939 listening: bool,
2940 ) -> (
2941 Arc<SharedPlayerState>,
2942 Arc<PlaybackTimeline>,
2943 Arc<VizSnapshot>,
2944 crossbeam_channel::Sender<PlayerCommand>,
2945 ) {
2946 if let Err(e) = crate::audio::dsp::profiles::migrate() {
2949 log::warn!("dsp: not brought up to date: {e}");
2950 }
2951 let mut player = Self::new();
2952 player.history = PlayRecorder::spawn();
2953 let state = player.shared_state();
2954 let timeline = player.timeline();
2955 let viz_snapshot = player.viz_snapshot();
2956 let tx = player.command_sender();
2957 {
2958 let tx = tx.clone();
2959 player.backend.on_disconnect(Arc::new(move || {
2960 let _ = tx.try_send(PlayerCommand::RestartOutput);
2961 }));
2962 }
2963 player.downloads = Some(crate::remote::queue::DownloadQueue::spawn(
2966 tx.clone(),
2967 state.clone(),
2968 ));
2969
2970 if let Some(udn) = Self::renderer_to_resume(listening) {
2971 player.resume_renderer = true;
2972 crate::upnp::resume(udn, &tx);
2973 }
2974
2975 thread::Builder::new()
2976 .name("koan-player".into())
2977 .spawn(move || player.run())
2978 .expect("failed to spawn player thread");
2979
2980 (state, timeline, viz_snapshot, tx)
2981 }
2982}
2983
2984fn step_pass(steps: &[state::Lookahead], boundary: usize) -> Option<u64> {
2989 steps
2990 .iter()
2991 .rfind(|step| step.boundary == boundary)
2992 .map(|step| step.pass)
2993}
2994
2995#[cfg(test)]
2996mod tests {
2997 #[test]
3000 fn a_headless_player_does_not_go_back_to_a_renderer() {
3001 let _guard = crate::config::tests::PERSIST_LOCK
3002 .lock()
3003 .unwrap_or_else(|e| e.into_inner());
3004 let dir = tempfile::tempdir().unwrap();
3005 crate::config::set_config_dir(dir.path());
3006 crate::config::Config::persist(|cfg| {
3007 cfg.playback.renderer = Some("uuid:headless-test".into());
3008 })
3009 .unwrap();
3010 assert_eq!(Player::renderer_to_resume(false), None);
3011 }
3012
3013 #[test]
3014 fn a_download_in_progress_is_known_by_its_own_extension() {
3015 use std::path::Path;
3016 assert_eq!(
3018 lengthless_mode_for(Path::new("/c/t.m4a.part")),
3019 streaming::ProbeMode::LengthlessWholeEnd
3020 );
3021 assert_eq!(
3022 lengthless_mode_for(Path::new("/c/t.OPUS.part")),
3023 streaming::ProbeMode::LengthlessWholeEnd
3024 );
3025 assert_eq!(
3026 lengthless_mode_for(Path::new("/c/t.flac.part")),
3027 streaming::ProbeMode::Lengthless
3028 );
3029 assert_eq!(
3030 media_extension(Path::new("/c/t.m4a.part")).as_deref(),
3031 Some("m4a")
3032 );
3033 assert_eq!(
3034 media_extension(Path::new("/c/t.mp3")).as_deref(),
3035 Some("mp3")
3036 );
3037 }
3038
3039 use super::*;
3040 use state::PlaylistItem;
3041 use std::path::PathBuf;
3042 use std::sync::atomic::{AtomicU64, Ordering};
3043
3044 fn make_item(title: &str) -> PlaylistItem {
3045 PlaylistItem {
3046 uid: None,
3047 playlist_entry_id: None,
3048 id: QueueItemId::new(),
3049 db_id: None,
3050 path: PathBuf::from(format!("/music/{title}.flac")),
3051 title: title.to_string(),
3052 artist: String::new(),
3053 album_artist: String::new(),
3054 album: String::new(),
3055 year: None,
3056 codec: None,
3057 track_number: None,
3058 disc: None,
3059 duration_ms: None,
3060 state: ItemState::Ready,
3061 played: false,
3062 }
3063 }
3064
3065 pub(super) fn playlist_ids(player: &Player) -> Vec<QueueItemId> {
3066 let (items, _) = player.shared_state.snapshot_playlist();
3067 items.iter().map(|i| i.id).collect()
3068 }
3069
3070 fn playlist_titles(player: &Player) -> Vec<String> {
3071 let (items, _) = player.shared_state.snapshot_playlist();
3072 items.iter().map(|i| i.title.clone()).collect()
3073 }
3074
3075 fn pending_item(title: &str) -> PlaylistItem {
3076 PlaylistItem {
3077 playlist_entry_id: None,
3078 state: ItemState::Pending,
3079 ..make_item(title)
3080 }
3081 }
3082
3083 fn test_session(id: QueueItemId, engine: Box<dyn AudioEngineHandle>) -> Session {
3085 Session {
3086 track: TrackInfo {
3087 id,
3088 path: PathBuf::from("/music/t.flac"),
3089 codec: String::new(),
3090 sample_rate: 44_100,
3091 bit_depth: None,
3092 bitrate_kbps: None,
3093 channels: 2,
3094 duration_ms: 1_000,
3095 },
3096 run: Run::Playing,
3097 lookahead: Default::default(),
3098 output: Output::Local(Local {
3099 engine,
3100 decode_handle: buffer::DecodeHandle::new_for_test(Default::default()),
3101 stream: None,
3102 _rate_watch: None,
3103 dsp: None,
3104 }),
3105 }
3106 }
3107
3108 fn pretend_playing(player: &mut Player, id: QueueItemId) {
3112 assert!(
3113 player.shared_state.get_item(id).is_some(),
3114 "item is in the queue"
3115 );
3116 player.stop_engine();
3117 player.transport = Transport::Loaded(test_session(
3118 id,
3119 Box::new(NullEngine {
3120 starts: Default::default(),
3121 running: Default::default(),
3122 lead_in: Default::default(),
3123 }),
3124 ));
3125 player.shared_state.set_cursor(Some(id));
3126 player.publish();
3127 }
3128
3129 fn playing_id(player: &Player) -> Option<QueueItemId> {
3130 player.shared_state.track_info().map(|t| t.id)
3131 }
3132
3133 fn seed(player: &mut Player, n: usize) -> Vec<QueueItemId> {
3135 let items: Vec<_> = (0..n).map(|i| make_item(&format!("t{i}"))).collect();
3136 let ids = items.iter().map(|i| i.id).collect();
3137 player.process_command(PlayerCommand::AddToPlaylist(items));
3138 ids
3139 }
3140
3141 fn listen(player: &mut Player, from_ms: u64, to_ms: u64) {
3145 let mut at = from_ms;
3146 if let Some(f) = player.in_flight.as_mut() {
3147 f.advance(at); }
3149 while at < to_ms {
3150 at = (at + 50).min(to_ms);
3151 if let Some(f) = player.in_flight.as_mut() {
3152 f.advance(at);
3153 }
3154 }
3155 }
3156
3157 fn start(player: &mut Player, track_id: i64) -> QueueItemId {
3158 let id = QueueItemId::new();
3159 player.on_track_changed(id, 0);
3160 player
3162 .in_flight
3163 .as_mut()
3164 .unwrap()
3165 .track_id_for_test(track_id);
3166 id
3167 }
3168
3169 #[test]
3170 fn a_gapless_transition_closes_the_outgoing_track_and_opens_the_next() {
3171 let mut player = Player::new();
3172 start(&mut player, 11);
3173 listen(&mut player, 0, 200_000);
3174
3175 let b = QueueItemId::new();
3176 player.on_track_changed(b, 0);
3177 let f = player
3178 .in_flight
3179 .as_ref()
3180 .expect("the next track is counting");
3181 assert_eq!(f.item, b);
3182 assert_eq!(f.listened_ms(), 0, "and starts from nothing");
3183 }
3184
3185 #[test]
3186 fn a_track_skipped_seconds_in_is_still_history() {
3187 let mut player = Player::new();
3188 start(&mut player, 7);
3189 listen(&mut player, 0, 2_000);
3190
3191 let event = player
3192 .finish_play()
3193 .expect("putting something on is a thing you did, however briefly");
3194 assert!(matches!(
3195 event,
3196 history::PlayEvent::Finished {
3197 track_id: 7,
3198 listened_ms: 2_000
3199 }
3200 ));
3201 }
3202
3203 #[test]
3204 fn a_track_is_closed_out_once() {
3205 let mut player = Player::new();
3206 start(&mut player, 7);
3207 listen(&mut player, 0, 200_000);
3208
3209 assert!(player.finish_play().is_some());
3210 assert!(player.finish_play().is_none());
3211 }
3212
3213 #[test]
3214 fn seeking_around_a_track_does_not_enter_it_twice() {
3215 let mut player = Player::new();
3216 let id = start(&mut player, 7);
3217 listen(&mut player, 0, 120_000);
3218
3219 player.on_track_changed(id, 30_000);
3221 assert_eq!(
3222 player.in_flight.as_ref().unwrap().listened_ms(),
3223 120_000,
3224 "the seek kept the count rather than restarting it"
3225 );
3226 listen(&mut player, 30_000, 40_000);
3227
3228 let Some(history::PlayEvent::Finished { listened_ms, .. }) = player.finish_play() else {
3229 panic!("still one play");
3230 };
3231 assert_eq!(listened_ms, 130_000);
3232 assert!(player.finish_play().is_none());
3233 }
3234
3235 #[test]
3236 fn a_track_that_is_not_in_the_library_is_not_recorded() {
3237 let mut player = Player::new();
3238 let id = QueueItemId::new();
3239 player.on_track_changed(id, 0);
3240 listen(&mut player, 0, 200_000);
3241 assert!(player.finish_play().is_none());
3242 }
3243
3244 #[test]
3245 fn stopping_closes_out_what_was_heard() {
3246 let mut player = Player::new();
3247 start(&mut player, 7);
3248 listen(&mut player, 0, 150_000);
3249
3250 player.stop_playback_and_clear_state();
3251 assert!(player.in_flight.is_none(), "the stop consumed it");
3252 }
3253
3254 #[test]
3255 fn resume_with_nothing_loaded_plays_the_cursor() {
3256 let dir = tempfile::tempdir().unwrap();
3257 let path = dir.path().join("t.wav");
3258 crate::test_utils::generate_wav(&path, 8_000, 1, 10.0, 16);
3259
3260 let mut player = Player::new();
3261 player.backend = Box::new(StuckBackend {
3262 rate: 8_000.0,
3263 asked: Default::default(),
3264 starts: Default::default(),
3265 });
3266 let item = PlaylistItem {
3267 db_id: Some(5),
3268 path,
3269 ..make_item("t")
3270 };
3271 let id = item.id;
3272 player.process_command(PlayerCommand::AddToPlaylist(vec![item]));
3273 player.shared_state.set_cursor(Some(id));
3274 assert!(player.session().is_none());
3275
3276 player.process_command(PlayerCommand::Resume);
3277 assert!(player.session().is_some(), "the cursor's track started");
3278 assert_eq!(player.shared_state.playback_state(), PlaybackState::Playing);
3279 player.process_command(PlayerCommand::Stop);
3280 }
3281
3282 #[test]
3283 fn the_audio_session_is_released_only_after_output_has_been_idle_a_while() {
3284 let mut player = Player::new();
3285 let now = std::time::Instant::now();
3286 assert!(
3287 !player.session_release_due(false, true, now),
3288 "the grace starts"
3289 );
3290 assert!(!player.session_release_due(false, true, now + SESSION_GRACE / 2));
3291 assert!(
3292 player.session_release_due(false, true, now + SESSION_GRACE),
3293 "released once it runs out"
3294 );
3295 assert_eq!(player.session_release_at, None);
3296
3297 assert!(!player.session_release_due(false, true, now));
3298 assert!(
3299 !player.session_release_due(true, true, now + SESSION_GRACE / 2),
3300 "playing again inside the grace keeps the session"
3301 );
3302 assert_eq!(player.session_release_at, None);
3303 assert!(
3304 !player.session_release_due(false, false, now),
3305 "nothing held"
3306 );
3307 assert_eq!(player.session_release_at, None);
3308 }
3309
3310 #[derive(Clone, Default)]
3314 struct Interruptible {
3315 running: Arc<std::sync::atomic::AtomicBool>,
3316 muted: Arc<std::sync::atomic::AtomicBool>,
3317 }
3318 impl AudioEngineHandle for Interruptible {
3319 fn start(&self) -> Result<(), BackendError> {
3320 self.running.store(true, Ordering::Relaxed);
3321 Ok(())
3322 }
3323 fn stop(&self) -> Result<(), BackendError> {
3324 self.running.store(false, Ordering::Relaxed);
3325 Ok(())
3326 }
3327 fn is_running(&self) -> bool {
3328 self.running.load(Ordering::Relaxed)
3329 }
3330 fn fade_out(&self) {
3331 self.muted.store(true, Ordering::Relaxed);
3332 }
3333 fn fade_in(&self) -> Result<(), BackendError> {
3334 self.muted.store(false, Ordering::Relaxed);
3335 self.start()
3336 }
3337 fn is_silent(&self) -> bool {
3338 false
3339 }
3340 fn is_faded_out(&self) -> bool {
3341 self.muted.load(Ordering::Relaxed)
3342 }
3343 }
3344
3345 #[test]
3346 fn pausing_output_the_system_stopped_leaves_nothing_waiting_and_resumes_audibly() {
3347 let engine = Interruptible::default();
3348 engine.running.store(true, Ordering::Relaxed);
3349 let mut player = Player::new();
3350 player.transport =
3351 Transport::Loaded(test_session(QueueItemId::new(), Box::new(engine.clone())));
3352
3353 engine.running.store(false, Ordering::Relaxed);
3355 player.pause_with(Fade::Short);
3356 player.update_playback_state();
3357 assert!(!engine.muted.load(Ordering::Relaxed), "no fade begun");
3358 assert_eq!(player.next_event(), None, "nothing to wake for");
3359 assert!(!player.wants_output(), "so the audio session can go");
3360
3361 player.resume();
3362 assert!(engine.running.load(Ordering::Relaxed));
3363 assert!(!engine.muted.load(Ordering::Relaxed), "heard on resuming");
3364
3365 player.pause_with(Fade::Short);
3367 assert!(engine.muted.load(Ordering::Relaxed), "fading");
3368 engine.running.store(false, Ordering::Relaxed);
3369 player.update_playback_state();
3370 assert_eq!(player.next_event(), None);
3371 player.resume();
3372 assert!(!engine.muted.load(Ordering::Relaxed), "faded back in");
3373 }
3374
3375 #[test]
3376 fn waiting_to_play_a_track_keeps_the_audio_session() {
3377 let mut player = Player::new();
3378 let item = make_item("t");
3379 player.transport = Transport::Waiting(Waiting {
3380 id: item.id,
3381 position_ms: 0,
3382 start: Run::Playing,
3383 });
3384 assert!(player.wants_output(), "play was asked for");
3385 let playing = player.wants_output();
3386 let now = std::time::Instant::now();
3387 for later in [now, now + SESSION_GRACE * 2] {
3388 assert!(!player.session_release_due(playing, true, later));
3389 }
3390 assert_eq!(player.session_release_at, None);
3391 }
3392
3393 #[test]
3394 fn an_item_unlinked_from_its_row_plays_from_its_own_file() {
3395 let dir = tempfile::tempdir().unwrap();
3396 let path = dir.path().join("t.wav");
3397 crate::test_utils::generate_wav(&path, 8_000, 1, 1.0, 16);
3398
3399 let mut player = Player::new();
3400 player.backend = Box::new(StuckBackend {
3401 rate: 8_000.0,
3402 asked: Default::default(),
3403 starts: Default::default(),
3404 });
3405 let on_disk = PlaylistItem {
3406 path,
3407 db_id: Some(7),
3408 uid: Some("0190a000-0000-7000-8000-000000000001".into()),
3409 ..make_item("t")
3410 };
3411 let arriving = PlaylistItem {
3412 path: dir.path().join("not yet.flac"),
3413 db_id: Some(8),
3414 uid: Some("0190a000-0000-7000-8000-000000000002".into()),
3415 state: ItemState::Pending,
3416 ..make_item("u")
3417 };
3418 let (kept, stranded) = (on_disk.id, arriving.id);
3419 player.process_command(PlayerCommand::AddToPlaylist(vec![on_disk, arriving]));
3420 player
3422 .shared_state
3423 .relink(&[(kept, None), (stranded, None)]);
3424 assert_eq!(player.shared_state.item_db_id(kept), None);
3425 assert!(
3426 matches!(
3427 player.shared_state.item_state(stranded),
3428 Some(ItemState::Failed(_))
3429 ),
3430 "nothing would ever fetch it: failed, not waited on"
3431 );
3432
3433 player.process_command(PlayerCommand::Play(kept));
3434 assert_eq!(player.shared_state.playback_state(), PlaybackState::Playing);
3435
3436 player.shared_state.relink(&[(stranded, Some(9))]);
3438 assert_eq!(
3439 player.shared_state.item_state(stranded),
3440 Some(ItemState::Pending)
3441 );
3442 }
3443
3444 #[test]
3445 fn a_player_with_nothing_coming_has_nothing_to_wake_for() {
3446 let dir = tempfile::tempdir().unwrap();
3447 let path = dir.path().join("t.wav");
3448 crate::test_utils::generate_wav(&path, 8_000, 1, 10.0, 16);
3449
3450 let mut player = Player::new();
3451 player.backend = Box::new(StuckBackend {
3452 rate: 8_000.0,
3453 asked: Default::default(),
3454 starts: Default::default(),
3455 });
3456 let item = PlaylistItem {
3457 path,
3458 ..make_item("t")
3459 };
3460 let id = item.id;
3461 player.process_command(PlayerCommand::AddToPlaylist(vec![item]));
3462 assert_eq!(player.next_wake(), None, "stopped");
3463
3464 player.process_command(PlayerCommand::Play(id));
3465 assert_eq!(player.shared_state.playback_state(), PlaybackState::Playing);
3466 let check = player.next_wake().expect("a look at the output starting");
3468 assert!(check <= std::time::Instant::now() + OUTPUT_START_CHECK);
3469 player
3470 .timeline
3471 .samples_played_counter()
3472 .store(800, std::sync::atomic::Ordering::Relaxed);
3473 player.output_check = Some((std::time::Instant::now(), 0));
3474 player.update_playback_state();
3475 assert_eq!(
3476 player.next_wake(),
3477 None,
3478 "playing, with no track queued after it"
3479 );
3480
3481 if let Transport::Loaded(session) = &mut player.transport {
3482 session.engine().unwrap().stop().unwrap();
3483 session.run = Run::Paused;
3484 }
3485 assert_eq!(player.next_wake(), None, "paused");
3486 player.process_command(PlayerCommand::Stop);
3487 }
3488
3489 #[test]
3490 fn a_track_cued_paused_never_starts_the_output() {
3491 let dir = tempfile::tempdir().unwrap();
3492 let path = dir.path().join("t.wav");
3493 crate::test_utils::generate_wav(&path, 8_000, 1, 10.0, 16);
3494
3495 let starts = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3496 let mut player = Player::new();
3497 player.backend = Box::new(StuckBackend {
3498 rate: 8_000.0,
3499 asked: Default::default(),
3500 starts: starts.clone(),
3501 });
3502 let item = PlaylistItem {
3503 path,
3504 ..make_item("t")
3505 };
3506 let id = item.id;
3507 player.process_command(PlayerCommand::AddToPlaylist(vec![item]));
3508
3509 player.process_command(PlayerCommand::Cue {
3510 id,
3511 position_ms: 2_000,
3512 play: false,
3513 });
3514 assert!(player.session().is_some(), "loaded");
3515 assert_eq!(player.shared_state.playback_state(), PlaybackState::Paused);
3516 loop {
3520 match player
3521 .commands
3522 .rx
3523 .recv_timeout(std::time::Duration::from_secs(5))
3524 {
3525 Ok(PlayerCommand::TrackQueued) => break,
3526 Ok(_) => {}
3527 Err(e) => panic!("the decoder never queued the track: {e}"),
3528 }
3529 }
3530 let at = player.shared_state.position_ms();
3531 assert!((1_750..=2_000).contains(&at), "cued at {at}ms");
3532 assert_eq!(starts.load(Ordering::Relaxed), 0, "not a sample let out");
3533
3534 player.process_command(PlayerCommand::Seek(4_000));
3536 assert_eq!(player.shared_state.playback_state(), PlaybackState::Paused);
3537 assert_eq!(starts.load(Ordering::Relaxed), 0);
3538
3539 player.process_command(PlayerCommand::Resume);
3540 assert_eq!(player.shared_state.playback_state(), PlaybackState::Playing);
3541 assert_eq!(starts.load(Ordering::Relaxed), 1);
3542 player.process_command(PlayerCommand::Stop);
3543 }
3544
3545 fn downloading_wav(dir: &Path) -> (Player, QueueItemId, Arc<std::sync::atomic::AtomicUsize>) {
3548 let path = dir.join("t.wav");
3549 crate::test_utils::generate_wav(&path, 8_000, 1, 10.0, 16);
3550 let starts = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3551 let mut player = Player::new();
3552 player.backend = Box::new(StuckBackend {
3553 rate: 8_000.0,
3554 asked: Default::default(),
3555 starts: starts.clone(),
3556 });
3557 let item = PlaylistItem {
3558 path,
3559 state: ItemState::Pending,
3560 ..make_item("t")
3561 };
3562 let id = item.id;
3563 player.process_command(PlayerCommand::AddToPlaylist(vec![item]));
3564 (player, id, starts)
3565 }
3566
3567 fn land(player: &mut Player, id: QueueItemId) {
3570 player.shared_state.update_item_state(id, ItemState::Ready);
3571 player.process_command(PlayerCommand::TrackReady(id));
3572 }
3573
3574 #[test]
3575 fn track_ready_never_resurrects_an_item_that_failed() {
3576 let mut player = Player::new();
3577 let item = PlaylistItem {
3578 state: ItemState::Failed("gone".into()),
3579 ..make_item("t")
3580 };
3581 let id = item.id;
3582 player.process_command(PlayerCommand::AddToPlaylist(vec![item]));
3583
3584 player.process_command(PlayerCommand::TrackReady(id));
3585
3586 assert!(matches!(
3587 player.shared_state.get_item(id).map(|i| i.state),
3588 Some(ItemState::Failed(_))
3589 ));
3590 }
3591
3592 fn await_queued(player: &Player) {
3593 loop {
3594 match player
3595 .commands
3596 .rx
3597 .recv_timeout(std::time::Duration::from_secs(5))
3598 {
3599 Ok(PlayerCommand::TrackQueued) => return,
3600 Ok(_) => {}
3601 Err(e) => panic!("the decoder never queued the track: {e}"),
3602 }
3603 }
3604 }
3605
3606 #[test]
3607 fn a_cue_on_a_downloading_track_opens_at_its_position_once_it_lands() {
3608 let dir = tempfile::tempdir().unwrap();
3609 let (mut player, id, starts) = downloading_wav(dir.path());
3610
3611 player.process_command(PlayerCommand::Cue {
3612 id,
3613 position_ms: 6_000,
3614 play: true,
3615 });
3616 assert!(player.session().is_none(), "nothing opened early");
3617 assert_eq!(player.shared_state.playback_state(), PlaybackState::Stopped);
3618 assert_eq!(starts.load(Ordering::Relaxed), 0);
3619
3620 land(&mut player, id);
3621 assert_eq!(player.shared_state.playback_state(), PlaybackState::Playing);
3622 assert_eq!(player.playback_starts, 1, "opened once, at the position");
3623 assert_eq!(starts.load(Ordering::Relaxed), 1);
3624 await_queued(&player);
3625 let at = player.shared_state.position_ms();
3626 assert!((5_750..=6_000).contains(&at), "opened at {at}ms");
3627 player.process_command(PlayerCommand::Stop);
3628 }
3629
3630 #[test]
3631 fn a_paused_cue_on_a_downloading_track_lands_paused() {
3632 let dir = tempfile::tempdir().unwrap();
3633 let (mut player, id, starts) = downloading_wav(dir.path());
3634
3635 player.process_command(PlayerCommand::Cue {
3636 id,
3637 position_ms: 3_000,
3638 play: false,
3639 });
3640 land(&mut player, id);
3641 assert_eq!(player.shared_state.playback_state(), PlaybackState::Paused);
3642 assert_eq!(starts.load(Ordering::Relaxed), 0, "not a sample let out");
3643 await_queued(&player);
3644 let at = player.shared_state.position_ms();
3645 assert!((2_750..=3_000).contains(&at), "cued at {at}ms");
3646 player.process_command(PlayerCommand::Stop);
3647 }
3648
3649 #[test]
3650 fn playing_something_else_forgets_a_waiting_cue() {
3651 let dir = tempfile::tempdir().unwrap();
3652 let (mut player, id, _) = downloading_wav(dir.path());
3653 let other = seed(&mut player, 1)[0];
3654
3655 player.process_command(PlayerCommand::Cue {
3656 id,
3657 position_ms: 6_000,
3658 play: true,
3659 });
3660 player.process_command(PlayerCommand::Play(other));
3661 player.process_command(PlayerCommand::Play(id));
3662 assert!(player.waiting().is_some_and(|w| w.position_ms == 0));
3663 player.process_command(PlayerCommand::Stop);
3664 }
3665
3666 #[test]
3667 fn a_paused_track_whose_download_lands_stays_paused_where_it_was() {
3668 let dir = tempfile::tempdir().unwrap();
3669 let (mut player, id, starts) = downloading_wav(dir.path());
3670 player.shared_state.update_item_state(id, ItemState::Ready);
3672 player.process_command(PlayerCommand::Cue {
3673 id,
3674 position_ms: 3_000,
3675 play: false,
3676 });
3677 await_queued(&player);
3678
3679 player.process_command(PlayerCommand::TrackReady(id));
3680 assert_eq!(player.shared_state.playback_state(), PlaybackState::Paused);
3681 assert_eq!(player.playback_starts, 1, "not reopened");
3682 assert_eq!(starts.load(Ordering::Relaxed), 0, "not a sample let out");
3683 player.process_command(PlayerCommand::Stop);
3684 }
3685
3686 #[test]
3687 fn pausing_a_track_on_its_way_opens_it_paused() {
3688 let dir = tempfile::tempdir().unwrap();
3689 let (mut player, id, starts) = downloading_wav(dir.path());
3690
3691 player.process_command(PlayerCommand::Play(id));
3692 assert!(player.shared_state.is_waiting());
3693 assert!(player.shared_state.wants_to_play(), "a toggle pauses it");
3694 assert!(!player.shared_state.is_idle(), "adding tracks leaves it be");
3695 player.process_command(PlayerCommand::Pause);
3696 assert_eq!(player.shared_state.playback_state(), PlaybackState::Paused);
3697 assert!(!player.shared_state.wants_to_play());
3698
3699 player.shared_state.update_item_state(id, ItemState::Ready);
3700 player.process_command(PlayerCommand::TrackReady(id));
3701 assert!(player.session().is_some(), "loaded");
3702 assert_eq!(player.shared_state.playback_state(), PlaybackState::Paused);
3703 assert_eq!(starts.load(Ordering::Relaxed), 0, "not a sample let out");
3704 player.process_command(PlayerCommand::Stop);
3705 }
3706
3707 #[test]
3708 fn a_cue_paused_and_resumed_on_its_way_keeps_its_position() {
3709 let dir = tempfile::tempdir().unwrap();
3710 let (mut player, id, starts) = downloading_wav(dir.path());
3711
3712 player.process_command(PlayerCommand::Cue {
3713 id,
3714 position_ms: 6_000,
3715 play: true,
3716 });
3717 player.process_command(PlayerCommand::Pause);
3718 player.process_command(PlayerCommand::Resume);
3719 assert!(player.session().is_none(), "still on its way");
3720 assert_eq!(player.shared_state.playback_state(), PlaybackState::Stopped);
3721
3722 player.shared_state.update_item_state(id, ItemState::Ready);
3723 player.process_command(PlayerCommand::TrackReady(id));
3724 assert_eq!(player.shared_state.playback_state(), PlaybackState::Playing);
3725 assert_eq!(starts.load(Ordering::Relaxed), 1);
3726 await_queued(&player);
3727 let at = player.shared_state.position_ms();
3728 assert!((5_750..=6_000).contains(&at), "opened at {at}ms");
3729 player.process_command(PlayerCommand::Stop);
3730 }
3731
3732 #[test]
3733 fn a_restored_cursor_does_not_start_when_its_download_lands() {
3734 let dir = tempfile::tempdir().unwrap();
3735 let (mut player, id, starts) = downloading_wav(dir.path());
3736 player.shared_state.set_cursor(Some(id));
3737
3738 player.shared_state.update_item_state(id, ItemState::Ready);
3739 player.process_command(PlayerCommand::TrackReady(id));
3740 assert!(player.session().is_none());
3741 assert_eq!(starts.load(Ordering::Relaxed), 0);
3742 }
3743
3744 #[test]
3745 fn replacing_the_queue_keeps_a_transfer_both_queues_want() {
3746 let pending = |title: &str| PlaylistItem {
3752 db_id: Some(7),
3753 state: ItemState::Pending,
3754 ..make_item(title)
3755 };
3756 let mut player = Player::new();
3757 let old = pending("old");
3758 let old_id = old.id;
3759 player.process_command(PlayerCommand::AddToPlaylist(vec![old]));
3760 let store = player.shared_state.downloads().clone();
3761 store.claim(7, Some(old_id));
3762
3763 let before = player.shared_state.pending_version();
3764 player.process_command(PlayerCommand::ReplacePlaylist {
3765 items: vec![pending("again")],
3766 start: 0,
3767 position_ms: 0,
3768 play: false,
3769 mode: QueueMode::Keep,
3770 });
3771 assert_eq!(
3772 player.shared_state.pending_version(),
3773 before + 1,
3774 "one change, so no reader can see the playlist between two"
3775 );
3776
3777 store.resync(&player.shared_state.pending_downloads());
3779 assert!(!store.abandoned(7));
3780 }
3781
3782 #[test]
3783 fn a_hand_off_opens_paused_at_its_position_in_one_command() {
3784 let dir = tempfile::tempdir().unwrap();
3785 let path = dir.path().join("t.wav");
3786 crate::test_utils::generate_wav(&path, 8_000, 1, 10.0, 16);
3787 let starts = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3788 let mut player = Player::new();
3789 player.backend = Box::new(StuckBackend {
3790 rate: 8_000.0,
3791 asked: Default::default(),
3792 starts: starts.clone(),
3793 });
3794 seed(&mut player, 2);
3795 let item = PlaylistItem {
3796 path,
3797 ..make_item("t")
3798 };
3799 let id = item.id;
3800
3801 player.process_command(PlayerCommand::ReplacePlaylist {
3802 items: vec![make_item("before"), item],
3803 start: 1,
3804 position_ms: 4_000,
3805 play: false,
3806 mode: QueueMode::Keep,
3807 });
3808 assert_eq!(
3809 playlist_titles(&player),
3810 vec!["before", "t"],
3811 "replaced, not added to"
3812 );
3813 assert_eq!(player.shared_state.cursor(), Some(id));
3814 assert_eq!(player.shared_state.playback_state(), PlaybackState::Paused);
3815 assert_eq!(player.playback_starts, 1, "opened once, at the position");
3816 assert_eq!(starts.load(Ordering::Relaxed), 0, "not a sample let out");
3817 await_queued(&player);
3818 let at = player.shared_state.position_ms();
3819 assert!((3_750..=4_000).contains(&at), "opened at {at}ms");
3820
3821 player.process_command(PlayerCommand::Undo);
3822 assert_eq!(playlist_titles(&player), vec!["t0", "t1"], "one undo step");
3823 player.process_command(PlayerCommand::Stop);
3824 }
3825
3826 #[test]
3827 fn a_decode_end_from_a_replaced_session_is_ignored() {
3828 let dir = tempfile::tempdir().unwrap();
3829 let mut player = Player::new();
3830 player.backend = Box::new(StuckBackend {
3831 rate: 8_000.0,
3832 asked: Default::default(),
3833 starts: Default::default(),
3834 });
3835 let items: Vec<_> = ["a", "b", "c"]
3836 .iter()
3837 .map(|name| {
3838 let path = dir.path().join(format!("{name}.wav"));
3839 crate::test_utils::generate_wav(&path, 8_000, 1, 10.0, 16);
3840 PlaylistItem {
3841 path,
3842 ..make_item(name)
3843 }
3844 })
3845 .collect();
3846 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
3847 player.process_command(PlayerCommand::AddToPlaylist(items));
3848
3849 player.process_command(PlayerCommand::Play(ids[0]));
3850 let stale = player.session;
3851 player.process_command(PlayerCommand::Play(ids[1]));
3852 player.process_command(PlayerCommand::DecodeFinished(stale));
3853
3854 assert_eq!(player.shared_state.cursor(), Some(ids[1]));
3855 assert_eq!(player.playback_starts, 2);
3856 player.process_command(PlayerCommand::Stop);
3857 }
3858
3859 fn queued_wavs(dir: &Path, names: &[&str]) -> (Player, Vec<QueueItemId>) {
3862 let (player, ids) = wavs_playing(dir, names, 1.0);
3863 for _ in &ids {
3864 await_queued(&player);
3865 }
3866 (player, ids)
3867 }
3868
3869 fn wavs_playing(dir: &Path, names: &[&str], seconds: f32) -> (Player, Vec<QueueItemId>) {
3872 wavs_in(dir, names, seconds, Repeat::Off)
3873 }
3874
3875 fn wavs_in(
3878 dir: &Path,
3879 names: &[&str],
3880 seconds: f32,
3881 repeat: Repeat,
3882 ) -> (Player, Vec<QueueItemId>) {
3883 let mut player = Player::new();
3884 player.backend = Box::new(StuckBackend {
3885 rate: 8_000.0,
3886 asked: Default::default(),
3887 starts: Default::default(),
3888 });
3889 let items: Vec<_> = names
3890 .iter()
3891 .zip(1..)
3892 .map(|(name, track)| {
3893 let path = dir.join(format!("{name}.wav"));
3894 crate::test_utils::generate_wav(&path, 8_000, 1, seconds, 16);
3895 PlaylistItem {
3896 path,
3897 db_id: Some(track),
3898 ..make_item(name)
3899 }
3900 })
3901 .collect();
3902 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
3903 player.process_command(PlayerCommand::AddToPlaylist(items));
3904 player.process_command(PlayerCommand::SetRepeat(repeat));
3905 player.process_command(PlayerCommand::Play(ids[0]));
3906 (player, ids)
3907 }
3908
3909 fn queued_at_least(player: &Player, n: usize) -> Vec<QueueItemId> {
3911 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
3912 loop {
3913 let queued = player.timeline.queued_after_playhead();
3914 if queued.len() >= n {
3915 return queued;
3916 }
3917 assert!(
3918 std::time::Instant::now() < deadline,
3919 "the decoder queued {queued:?}, never {n}"
3920 );
3921 std::thread::yield_now();
3922 }
3923 }
3924
3925 #[test]
3928 fn repeating_the_queue_runs_the_last_track_into_the_first() {
3929 let dir = tempfile::tempdir().unwrap();
3930 let (mut player, ids) = wavs_in(dir.path(), &["a", "b"], 1.0, Repeat::Queue);
3931
3932 let queued = queued_at_least(&player, 3);
3933 assert_eq!(
3934 queued[..3],
3935 [ids[1], ids[0], ids[1]],
3936 "gapless, round again"
3937 );
3938 let steps = player.session().unwrap().lookahead.lock().clone();
3939 assert!(!steps[0].wrapped);
3940 let wrap = steps.iter().find(|s| s.after == ids[1]).unwrap();
3941 assert_eq!(wrap.next, Some(ids[0]));
3942 assert!(wrap.wrapped);
3943 assert_eq!(player.playback_starts, 1);
3944 player.process_command(PlayerCommand::Stop);
3945 }
3946
3947 #[test]
3948 fn turning_repeat_off_takes_back_a_wrap_already_queued() {
3949 let dir = tempfile::tempdir().unwrap();
3950 let (mut player, ids) = wavs_in(dir.path(), &["a"], 1.0, Repeat::Queue);
3951 assert_eq!(queued_at_least(&player, 1)[0], ids[0]);
3952
3953 player.process_command(PlayerCommand::SetRepeat(Repeat::Off));
3954 assert_eq!(player.playback_starts, 2, "restarted at the playhead");
3955 wait_for_lookahead(&player);
3956 assert!(player.timeline.queued_after_playhead().is_empty());
3957 player.process_command(PlayerCommand::Stop);
3958 }
3959
3960 #[test]
3961 fn repeating_one_track_queues_it_again_and_each_pass_is_a_play() {
3962 let dir = tempfile::tempdir().unwrap();
3963 let (mut player, ids) = wavs_in(dir.path(), &["a", "b"], 1.0, Repeat::One);
3964 let (recorder, events) = history::PlayRecorder::capture();
3965 player.history = Some(recorder);
3966 player.in_flight = None;
3969 player.on_track_changed(ids[0], 0);
3970
3971 assert_eq!(queued_at_least(&player, 2)[..2], [ids[0], ids[0]]);
3972 player
3974 .timeline
3975 .samples_played
3976 .store(8_400, Ordering::Relaxed);
3977 player.update_playback_state();
3978
3979 let flight = player.in_flight.as_ref().unwrap();
3980 assert_eq!((flight.item, flight.boundary), (ids[0], 1));
3981 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
3982 let events: Vec<_> = events.try_iter().collect();
3983 let started = events
3984 .iter()
3985 .filter(|e| matches!(e, PlayEvent::Started { track_id: 1, .. }))
3986 .count();
3987 assert_eq!(started, 2, "two plays: {events:?}");
3988 assert!(
3989 events.contains(&PlayEvent::Finished {
3990 track_id: 1,
3991 listened_ms: 1_000
3992 }),
3993 "the first pass banked whole: {events:?}"
3994 );
3995 player.process_command(PlayerCommand::Stop);
3996 }
3997
3998 #[test]
3999 fn next_moves_on_from_a_track_repeating() {
4000 let dir = tempfile::tempdir().unwrap();
4001 let (mut player, ids) = wavs_in(dir.path(), &["a", "b"], 1.0, Repeat::One);
4002
4003 player.process_command(PlayerCommand::NextTrack);
4004 assert_eq!(playing_id(&player), Some(ids[1]));
4005 player.process_command(PlayerCommand::NextTrack);
4006 assert_eq!(
4007 playing_id(&player),
4008 Some(ids[0]),
4009 "round from the last, as repeating does"
4010 );
4011 player.process_command(PlayerCommand::Stop);
4012 }
4013
4014 #[test]
4015 fn a_repeat_of_nothing_heard_stops_rather_than_going_round() {
4016 let dir = tempfile::tempdir().unwrap();
4017 let (mut player, _) = wavs_in(dir.path(), &["a"], 1.0, Repeat::One);
4018
4019 player.process_command(PlayerCommand::DecodeFinished(player.session));
4022 assert!(matches!(player.transport, Transport::Idle));
4023 }
4024
4025 fn session_run(player: &Player) -> Option<Run> {
4028 player.session().map(|s| s.run)
4029 }
4030
4031 #[test]
4032 fn a_sleep_timer_fades_out_at_its_time_and_keeps_the_queue() {
4033 let dir = tempfile::tempdir().unwrap();
4034 let (mut player, ids) = wavs_playing(dir.path(), &["a", "b"], 1.0);
4035
4036 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::After {
4037 minutes: 30,
4038 })));
4039 let Some(Sleep::At { unix_ms }) = player.shared_state.sleep() else {
4040 panic!("published as a time");
4041 };
4042 let now = std::time::SystemTime::now()
4043 .duration_since(std::time::UNIX_EPOCH)
4044 .unwrap()
4045 .as_millis() as u64;
4046 assert!(unix_ms.abs_diff(now + 30 * 60_000) < 5_000);
4047 let at = player.sleep.and_then(|s| s.at).unwrap();
4048 assert!(player.next_wake().is_some_and(|w| w <= at), "woken for it");
4049
4050 player.sleep.as_mut().unwrap().at = Some(std::time::Instant::now());
4052 player.update_playback_state();
4053 assert_eq!(session_run(&player), Some(Run::Paused));
4054 assert!(
4055 !player.session().unwrap().engine().unwrap().is_running(),
4056 "stopped at silence: the fade did the fading"
4057 );
4058 assert!(
4059 player.sleep_fade.is_none(),
4060 "full level for what plays next"
4061 );
4062 assert_eq!(player.shared_state.sleep(), None, "spent");
4063 assert_eq!(playlist_ids(&player), ids, "the queue as it was");
4064 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
4065 player.process_command(PlayerCommand::Stop);
4066 }
4067
4068 fn fading_gain(player: &Player) -> Option<f32> {
4069 match player.sleep_fade {
4070 Some(sleep::SleepFade::Falling { gain, .. }) => Some(gain),
4071 _ => None,
4072 }
4073 }
4074
4075 fn db(gain: f32) -> f32 {
4076 20.0 * gain.log10()
4077 }
4078
4079 #[test]
4082 fn a_sleep_timer_fades_over_the_run_up_to_its_deadline() {
4083 let dir = tempfile::tempdir().unwrap();
4084 let (mut player, _) = wavs_playing(dir.path(), &["a"], 1.0);
4085 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::After {
4086 minutes: 15,
4087 })));
4088 let now = std::time::Instant::now();
4089 let secs = std::time::Duration::from_secs;
4090
4091 player.sleep.as_mut().unwrap().at = Some(now + secs(91));
4092 player.update_playback_state();
4093 assert_eq!(fading_gain(&player), None, "not yet: 91 s out");
4094 assert!(!player.shared_state.sleep_fading());
4095 let wake = player.next_wake().unwrap();
4096 assert!(
4097 wake <= now + secs(2),
4098 "woken for the fade's start, a second away"
4099 );
4100
4101 player.sleep.as_mut().unwrap().at = Some(now + secs(45));
4102 player.update_playback_state();
4103 let half = fading_gain(&player).expect("half way through the fade");
4104 assert!((db(half) + 30.0).abs() < 1.0, "{} dB", db(half));
4105 assert!(player.shared_state.sleep_fading(), "published as fading");
4106 assert!(player.next_wake().unwrap() <= std::time::Instant::now() + secs(1));
4107
4108 player.sleep.as_mut().unwrap().at = Some(now + std::time::Duration::from_millis(900));
4109 player.update_playback_state();
4110 let late = fading_gain(&player).unwrap();
4111 assert!(
4112 db(late) < -59.0,
4113 "all but silent at the end: {} dB",
4114 db(late)
4115 );
4116 player.process_command(PlayerCommand::Stop);
4117 }
4118
4119 #[test]
4121 fn cancelling_a_sleep_timer_mid_fade_brings_the_level_back() {
4122 let dir = tempfile::tempdir().unwrap();
4123 let (mut player, _) = wavs_playing(dir.path(), &["a"], 1.0);
4124 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::After {
4125 minutes: 15,
4126 })));
4127 player.sleep.as_mut().unwrap().at =
4128 Some(std::time::Instant::now() + std::time::Duration::from_secs(45));
4129 player.update_playback_state();
4130 assert!(fading_gain(&player).is_some());
4131
4132 player.process_command(PlayerCommand::SetSleepTimer(None));
4133 let Some(sleep::SleepFade::Restoring { from, .. }) = player.sleep_fade else {
4134 panic!("coming back up: {:?}", player.sleep_fade);
4135 };
4136 assert!((db(from) + 30.0).abs() < 1.0, "from where it had got to");
4137 assert!(!player.shared_state.sleep_fading());
4138 if let Some(sleep::SleepFade::Restoring { since, .. }) = player.sleep_fade.as_mut() {
4140 *since -= std::time::Duration::from_secs(1);
4141 }
4142 player.update_playback_state();
4143 assert!(player.sleep_fade.is_none(), "at full level, and left alone");
4144 assert_eq!(session_run(&player), Some(Run::Playing));
4145 player.process_command(PlayerCommand::Stop);
4146 }
4147
4148 #[test]
4151 fn pausing_during_the_fade_cancels_the_timer() {
4152 let dir = tempfile::tempdir().unwrap();
4153 let (mut player, _) = wavs_playing(dir.path(), &["a"], 1.0);
4154 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::After {
4155 minutes: 15,
4156 })));
4157 player.sleep.as_mut().unwrap().at =
4158 Some(std::time::Instant::now() + std::time::Duration::from_secs(45));
4159 player.update_playback_state();
4160 assert!(fading_gain(&player).is_some());
4161
4162 player.process_command(PlayerCommand::Pause);
4163 assert_eq!(player.shared_state.sleep(), None, "cancelled");
4164 assert!(player.sleep_fade.is_none());
4165 assert!(player.sleep_snap_on_resume);
4166 player.process_command(PlayerCommand::Resume);
4167 assert!(!player.sleep_snap_on_resume, "full level taken on resuming");
4168 player.process_command(PlayerCommand::Stop);
4169 }
4170
4171 #[test]
4175 fn the_end_of_a_track_or_record_fades_into_the_boundary() {
4176 let dir = tempfile::tempdir().unwrap();
4177 let (mut player, _) = queued_wavs(dir.path(), &["a", "b"]);
4178 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::EndOfTrack)));
4179 player.update_playback_state();
4180 assert!(
4181 fading_gain(&player).is_some(),
4182 "a one-second track fades whole"
4183 );
4184
4185 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::EndOfRecord)));
4188 if let Some(sleep::SleepFade::Restoring { since, .. }) = player.sleep_fade.as_mut() {
4189 *since -= std::time::Duration::from_secs(1);
4190 }
4191 player.update_playback_state();
4192 assert_eq!(fading_gain(&player), None, "the record goes on");
4193 player.process_command(PlayerCommand::Stop);
4194 }
4195
4196 #[test]
4197 fn a_sleep_timer_going_off_while_paused_only_ends() {
4198 let dir = tempfile::tempdir().unwrap();
4199 let (mut player, _) = wavs_playing(dir.path(), &["a"], 1.0);
4200 player.process_command(PlayerCommand::Pause);
4201 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::After {
4202 minutes: 1,
4203 })));
4204 player.sleep.as_mut().unwrap().at = Some(std::time::Instant::now());
4205 player.update_playback_state();
4206 assert_eq!(player.shared_state.sleep(), None);
4207 player.process_command(PlayerCommand::Resume);
4208 assert_eq!(
4209 session_run(&player),
4210 Some(Run::Playing),
4211 "nothing left to go off"
4212 );
4213 player.process_command(PlayerCommand::Stop);
4214 }
4215
4216 #[test]
4217 fn cancelling_a_sleep_timer_leaves_playback_alone() {
4218 let dir = tempfile::tempdir().unwrap();
4219 let (mut player, _) = wavs_playing(dir.path(), &["a"], 1.0);
4220 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::After {
4221 minutes: 1,
4222 })));
4223 player.process_command(PlayerCommand::SetSleepTimer(None));
4224 assert_eq!(player.shared_state.sleep(), None);
4225 assert!(player.sleep.is_none());
4226 player.update_playback_state();
4227 assert_eq!(session_run(&player), Some(Run::Playing));
4228 player.process_command(PlayerCommand::Stop);
4229 }
4230
4231 #[test]
4234 fn a_sleep_timer_for_the_end_of_the_track_stops_at_the_boundary() {
4235 let dir = tempfile::tempdir().unwrap();
4236 let (mut player, ids) = queued_wavs(dir.path(), &["a", "b"]);
4237 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::EndOfTrack)));
4238 assert_eq!(player.shared_state.sleep(), Some(Sleep::EndOfTrack));
4239
4240 player
4242 .timeline
4243 .samples_played
4244 .store(4_000, Ordering::Relaxed);
4245 player.update_playback_state();
4246 assert_eq!(session_run(&player), Some(Run::Playing));
4247 assert_eq!(player.shared_state.sleep(), Some(Sleep::EndOfTrack));
4248
4249 player
4251 .timeline
4252 .samples_played
4253 .store(8_400, Ordering::Relaxed);
4254 player.update_playback_state();
4255 assert_eq!(player.shared_state.cursor(), Some(ids[1]));
4256 assert_eq!(session_run(&player), Some(Run::Paused));
4257 assert!(
4258 !player.session().unwrap().engine().unwrap().is_running(),
4259 "stopped at once, nothing of the next track faded through"
4260 );
4261 assert_eq!(player.shared_state.sleep(), None);
4262 assert_eq!(playlist_ids(&player), ids);
4263 player.process_command(PlayerCommand::Stop);
4264 }
4265
4266 #[test]
4269 fn a_sleep_timer_for_the_end_of_the_track_stops_a_track_repeating() {
4270 let dir = tempfile::tempdir().unwrap();
4271 let (mut player, ids) = wavs_in(dir.path(), &["a", "b"], 1.0, Repeat::One);
4272 assert_eq!(queued_at_least(&player, 1)[0], ids[0]);
4273 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::EndOfTrack)));
4274 player
4275 .timeline
4276 .samples_played
4277 .store(8_400, Ordering::Relaxed);
4278 player.update_playback_state();
4279 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
4280 assert_eq!(session_run(&player), Some(Run::Paused));
4281 assert_eq!(player.shared_state.sleep(), None);
4282 player.process_command(PlayerCommand::Stop);
4283 }
4284
4285 #[test]
4288 fn a_sleep_timer_for_the_end_of_the_track_opens_the_next_paused() {
4289 let dir = tempfile::tempdir().unwrap();
4290 let (mut player, ids) = wavs_playing(dir.path(), &["a", "b"], 1.0);
4291 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::EndOfTrack)));
4292 player.process_command(PlayerCommand::DecodeFinished(player.session));
4293 assert_eq!(player.shared_state.cursor(), Some(ids[1]));
4294 assert_eq!(player.intent(), Some(Run::Paused));
4295 assert_eq!(player.shared_state.sleep(), None);
4296 player.process_command(PlayerCommand::Stop);
4297 }
4298
4299 #[test]
4300 fn a_sleep_timer_for_the_end_of_the_record_plays_the_record_out() {
4301 let dir = tempfile::tempdir().unwrap();
4302 let mut player = Player::new();
4303 player.backend = Box::new(StuckBackend {
4304 rate: 8_000.0,
4305 asked: Default::default(),
4306 starts: Default::default(),
4307 });
4308 let items: Vec<_> = [("a", "Low"), ("b", "Low"), ("c", "Heroes")]
4309 .iter()
4310 .map(|(name, album)| {
4311 let path = dir.path().join(format!("{name}.wav"));
4312 crate::test_utils::generate_wav(&path, 8_000, 1, 1.0, 16);
4313 PlaylistItem {
4314 path,
4315 album: album.to_string(),
4316 album_artist: "David Bowie".into(),
4317 ..make_item(name)
4318 }
4319 })
4320 .collect();
4321 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
4322 player.process_command(PlayerCommand::AddToPlaylist(items));
4323 player.process_command(PlayerCommand::Play(ids[0]));
4324 player.process_command(PlayerCommand::SetSleepTimer(Some(SleepTimer::EndOfRecord)));
4325
4326 player.process_command(PlayerCommand::DecodeFinished(player.session));
4327 assert_eq!(player.shared_state.cursor(), Some(ids[1]));
4328 assert_eq!(player.intent(), Some(Run::Playing), "the same record");
4329 assert_eq!(player.shared_state.sleep(), Some(Sleep::EndOfRecord));
4330
4331 player.process_command(PlayerCommand::DecodeFinished(player.session));
4332 assert_eq!(player.shared_state.cursor(), Some(ids[2]));
4333 assert_eq!(player.intent(), Some(Run::Paused), "the next record waits");
4334 assert_eq!(player.shared_state.sleep(), None);
4335 player.process_command(PlayerCommand::Stop);
4336 }
4337
4338 #[test]
4339 fn shuffle_is_not_an_undo_step() {
4340 let mut player = Player::new();
4341 let ids = seed(&mut player, 10);
4342 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[4]));
4343 player.process_command(PlayerCommand::SetShuffle(true));
4344 assert_eq!(playlist_ids(&player).len(), 9, "the queue is as it was");
4345
4346 player.process_command(PlayerCommand::Undo);
4347 assert_eq!(playlist_ids(&player), ids, "the removal undone");
4348 assert!(player.shared_state.play_mode().shuffle, "shuffle left on");
4349 }
4350
4351 #[test]
4352 fn a_queue_replaced_while_shuffled_starts_where_asked_and_shuffles_the_rest() {
4353 let mut player = Player::new();
4354 seed(&mut player, 3);
4355 player.process_command(PlayerCommand::SetShuffle(true));
4356
4357 let items: Vec<_> = (0..20).map(|i| make_item(&format!("n{i}"))).collect();
4358 let given: Vec<_> = items.iter().map(|i| i.id).collect();
4359 player.process_command(PlayerCommand::ReplacePlaylist {
4360 items,
4361 start: 5,
4362 position_ms: 0,
4363 play: false,
4364 mode: QueueMode::Keep,
4365 });
4366 assert_eq!(playlist_ids(&player), given, "in the order given");
4367 assert_eq!(player.shared_state.cursor(), Some(given[5]));
4368
4369 let mut heard = vec![given[5]];
4370 while let Some(id) = player.shared_state.advance_cursor_loadable() {
4371 heard.push(id);
4372 }
4373 assert_ne!(heard, given);
4374 heard.sort_by_key(|id| given.iter().position(|i| i == id));
4375 assert_eq!(heard, given, "each once");
4376 }
4377
4378 #[test]
4379 fn the_pass_of_a_track_is_the_step_that_opened_it() {
4380 let step = |boundary, pass| state::Lookahead {
4381 after: QueueItemId::new(),
4382 next: None,
4383 chosen: None,
4384 wrapped: false,
4385 boundary,
4386 after_pass: 0,
4387 pass,
4388 };
4389 let steps = [step(0, 0), step(1, 0), step(1, 1), step(2, 1)];
4392 assert_eq!(step_pass(&steps, 1), Some(1));
4393 assert_eq!(step_pass(&steps, 3), None);
4394 }
4395
4396 fn played_out(player: &mut Player) -> Vec<QueueItemId> {
4399 let ids = seed(player, 4);
4400 player.process_command(PlayerCommand::SetShuffle(true));
4401 let state = &player.shared_state;
4402 state.advance_cursor_loadable();
4403 loop {
4404 state.mark_played(state.cursor().unwrap());
4405 if state.advance_cursor_loadable().is_none() {
4406 break;
4407 }
4408 }
4409 ids
4410 }
4411
4412 fn played_count(player: &Player) -> usize {
4413 player.shared_state.derive_visible_queue().finished_count
4414 }
4415
4416 #[test]
4417 fn a_session_restored_at_the_end_of_a_pass_does_not_start_another() {
4418 let mut player = Player::new();
4419 let ids = played_out(&mut player);
4420 let last = player.shared_state.cursor().unwrap();
4421
4422 player.process_command(PlayerCommand::Cue {
4423 id: last,
4424 position_ms: 0,
4425 play: false,
4426 });
4427 assert_eq!(played_count(&player), ids.len() - 1, "the marks kept");
4428 assert_eq!(
4429 player.shared_state.advance_cursor_loadable(),
4430 None,
4431 "repeat off: the queue ends"
4432 );
4433 }
4434
4435 #[test]
4436 fn a_track_repeating_keeps_the_marks() {
4437 let mut player = Player::new();
4438 let ids = played_out(&mut player);
4439 player.process_command(PlayerCommand::SetRepeat(Repeat::One));
4440 let last = player.shared_state.cursor().unwrap();
4441 player.cue(last, 0, Run::Paused);
4443 assert_eq!(played_count(&player), ids.len() - 1);
4444 player.process_command(PlayerCommand::Stop);
4445 }
4446
4447 #[test]
4448 fn picking_a_row_of_a_played_out_queue_starts_a_new_pass() {
4449 let mut player = Player::new();
4450 let ids = played_out(&mut player);
4451 player.process_command(PlayerCommand::Play(ids[1]));
4452 assert_eq!(played_count(&player), 0);
4453 assert_eq!(player.shared_state.upcoming().len(), ids.len() - 1);
4454 player.process_command(PlayerCommand::Stop);
4455 }
4456
4457 fn replace(player: &mut Player, n: usize, mode: QueueMode) -> Vec<QueueItemId> {
4458 let items: Vec<_> = (0..n).map(|i| make_item(&format!("r{i}"))).collect();
4459 let ids = items.iter().map(|i| i.id).collect();
4460 player.process_command(PlayerCommand::ReplacePlaylist {
4461 items,
4462 start: 0,
4463 position_ms: 0,
4464 play: false,
4465 mode,
4466 });
4467 ids
4468 }
4469
4470 fn shuffled_and_repeating() -> Player {
4471 let mut player = Player::new();
4472 seed(&mut player, 3);
4473 player.process_command(PlayerCommand::SetShuffle(true));
4474 player.process_command(PlayerCommand::SetRepeat(Repeat::Queue));
4475 player
4476 }
4477
4478 #[test]
4479 fn playing_from_a_play_button_turns_shuffle_and_repeat_off() {
4480 let mut player = shuffled_and_repeating();
4481 let ids = replace(&mut player, 8, QueueMode::InOrder);
4482 assert_eq!(player.shared_state.play_mode(), PlayMode::default());
4483 assert!(!player.shared_state.is_shuffled());
4484 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
4485 }
4486
4487 #[test]
4488 fn shuffling_from_a_play_button_turns_shuffle_on_and_repeat_off() {
4489 let mut player = Player::new();
4490 player.process_command(PlayerCommand::SetRepeat(Repeat::One));
4491 let ids = replace(&mut player, 12, QueueMode::Shuffled);
4492 assert_eq!(
4493 player.shared_state.play_mode(),
4494 PlayMode {
4495 shuffle: true,
4496 repeat: Repeat::Off
4497 }
4498 );
4499 assert_eq!(playlist_ids(&player), ids, "the queue in the order given");
4500
4501 let mut heard = vec![player.shared_state.cursor().unwrap()];
4502 while let Some(id) = player.shared_state.advance_cursor_loadable() {
4503 heard.push(id);
4504 }
4505 assert_ne!(heard, ids, "started and played in a shuffled order");
4506 heard.sort_by_key(|id| ids.iter().position(|i| i == id));
4507 assert_eq!(heard, ids);
4508 }
4509
4510 #[test]
4511 fn a_queue_restored_or_handed_over_keeps_the_modes() {
4512 let mut player = shuffled_and_repeating();
4513 let mode = player.shared_state.play_mode();
4514 replace(&mut player, 5, QueueMode::Keep);
4515 assert_eq!(player.shared_state.play_mode(), mode);
4516 assert!(player.shared_state.is_shuffled());
4517 }
4518
4519 #[test]
4520 fn adding_to_the_queue_keeps_the_modes() {
4521 let mut player = shuffled_and_repeating();
4522 let mode = player.shared_state.play_mode();
4523 seed(&mut player, 4);
4524 let ids = playlist_ids(&player);
4525 player.process_command(PlayerCommand::InsertInPlaylist {
4526 items: vec![make_item("next")],
4527 after: ids[0],
4528 });
4529 assert_eq!(player.shared_state.play_mode(), mode);
4530 }
4531
4532 #[test]
4533 fn removing_the_playing_track_while_shuffled_moves_on_in_the_play_order() {
4534 let mut player = Player::new();
4535 seed(&mut player, 10);
4536 player.process_command(PlayerCommand::SetShuffle(true));
4537 let playing = player.shared_state.advance_cursor_loadable().unwrap();
4538 pretend_playing(&mut player, playing);
4539 let next = player.shared_state.lookahead_after(playing).unwrap().next;
4540
4541 player.process_command(PlayerCommand::RemoveFromPlaylist(playing));
4542 assert_eq!(player.shared_state.cursor(), next);
4543 player.process_command(PlayerCommand::Stop);
4544 }
4545
4546 #[test]
4547 fn shuffled_the_decoder_queues_the_play_order() {
4548 let dir = tempfile::tempdir().unwrap();
4549 let names = ["a", "b", "c", "d", "e", "f"];
4550 let (mut player, ids) = wavs_playing(dir.path(), &names, 1.0);
4551 queued_at_least(&player, names.len() - 1);
4552
4553 player.process_command(PlayerCommand::SetShuffle(true));
4554 let order = player.shared_state.upcoming();
4555 assert_eq!(order.len(), names.len() - 1);
4556 assert_eq!(queued_at_least(&player, order.len()), order);
4557 assert_eq!(playlist_ids(&player), ids, "the queue never moved");
4558 player.process_command(PlayerCommand::Stop);
4559 }
4560
4561 #[test]
4562 fn removing_the_last_track_playing_carries_on_from_the_top_when_repeating() {
4563 let mut player = Player::new();
4564 let ids = seed(&mut player, 3);
4565 player.process_command(PlayerCommand::SetRepeat(Repeat::Queue));
4566 player.shared_state.set_cursor(Some(ids[2]));
4567
4568 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[2]));
4569 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
4570 }
4571
4572 #[test]
4573 fn removing_a_track_the_decoder_queued_takes_it_back() {
4574 let dir = tempfile::tempdir().unwrap();
4575 let (mut player, ids) = queued_wavs(dir.path(), &["a", "b", "c"]);
4576 assert_eq!(player.timeline.queued_after_playhead(), ids[1..]);
4577
4578 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[1]));
4579 assert_eq!(player.playback_starts, 2, "restarted at the playhead");
4580 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
4581 await_queued(&player);
4582 await_queued(&player);
4583 assert_eq!(player.timeline.queued_after_playhead(), vec![ids[2]]);
4584 player.process_command(PlayerCommand::Stop);
4585 }
4586
4587 #[test]
4588 fn a_track_inserted_to_play_next_is_not_skipped() {
4589 let dir = tempfile::tempdir().unwrap();
4590 let (mut player, ids) = queued_wavs(dir.path(), &["a", "b"]);
4591 let path = dir.path().join("next.wav");
4592 crate::test_utils::generate_wav(&path, 8_000, 1, 1.0, 16);
4593 let next = PlaylistItem {
4594 path,
4595 ..make_item("next")
4596 };
4597 let next_id = next.id;
4598
4599 player.process_command(PlayerCommand::InsertInPlaylist {
4600 items: vec![next],
4601 after: ids[0],
4602 });
4603 assert_eq!(player.playback_starts, 2);
4604 for _ in 0..3 {
4605 await_queued(&player);
4606 }
4607 assert_eq!(
4608 player.timeline.queued_after_playhead(),
4609 vec![next_id, ids[1]]
4610 );
4611 player.process_command(PlayerCommand::Stop);
4612 }
4613
4614 #[test]
4615 fn a_track_the_decoder_could_not_open_does_not_make_every_edit_restart() {
4616 let dir = tempfile::tempdir().unwrap();
4617 let mut player = Player::new();
4618 player.backend = Box::new(StuckBackend {
4619 rate: 8_000.0,
4620 asked: Default::default(),
4621 starts: Default::default(),
4622 });
4623 let wav = |name: &str| {
4624 let path = dir.path().join(format!("{name}.wav"));
4625 crate::test_utils::generate_wav(&path, 8_000, 1, 30.0, 16);
4626 PlaylistItem {
4627 path,
4628 ..make_item(name)
4629 }
4630 };
4631 let items = vec![wav("a"), make_item("missing"), wav("c")];
4633 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
4634 player.process_command(PlayerCommand::AddToPlaylist(items));
4635 player.process_command(PlayerCommand::Play(ids[0]));
4636 await_queued(&player);
4637 await_queued(&player);
4638
4639 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("later")]));
4640 assert_eq!(
4641 player.playback_starts, 1,
4642 "nothing the decoder decided changed"
4643 );
4644 assert_eq!(player.timeline.queued_after_playhead(), vec![ids[2]]);
4645 player.process_command(PlayerCommand::Stop);
4646 }
4647
4648 #[test]
4649 fn a_track_added_after_the_decoder_reached_the_end_follows_gaplessly() {
4650 let dir = tempfile::tempdir().unwrap();
4651 let (mut player, ids) = queued_wavs(dir.path(), &["a"]);
4652 wait_for_lookahead(&player);
4653 let path = dir.path().join("b.wav");
4654 crate::test_utils::generate_wav(&path, 8_000, 1, 1.0, 16);
4655 let b = PlaylistItem {
4656 path,
4657 ..make_item("b")
4658 };
4659 let b_id = b.id;
4660
4661 player.process_command(PlayerCommand::AddToPlaylist(vec![b]));
4662 assert_eq!(player.playback_starts, 2, "restarted to queue it");
4663 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
4664 await_queued(&player);
4665 await_queued(&player);
4666 assert_eq!(player.timeline.queued_after_playhead(), vec![b_id]);
4667 player.process_command(PlayerCommand::Stop);
4668 }
4669
4670 #[test]
4671 fn playing_next_a_track_still_downloading_takes_back_the_lookahead() {
4672 let dir = tempfile::tempdir().unwrap();
4673 let (mut player, ids) = queued_wavs(dir.path(), &["a", "b"]);
4674 let remote = pending_item("remote");
4675 let remote_id = remote.id;
4676
4677 player.process_command(PlayerCommand::InsertInPlaylist {
4678 items: vec![remote],
4679 after: ids[0],
4680 });
4681 assert_eq!(player.playback_starts, 2, "b is taken back out of the ring");
4682 await_queued(&player);
4683 wait_for_lookahead(&player);
4684 let session = player.session().unwrap();
4685 let step = session.lookahead.lock().last().cloned().unwrap();
4686 assert_eq!(step.next, Some(remote_id), "the decoder waits for it");
4687 assert!(player.timeline.queued_after_playhead().is_empty());
4688 player.process_command(PlayerCommand::Stop);
4689 }
4690
4691 #[test]
4692 fn gapless_playback_waits_for_a_track_still_downloading() {
4693 let dir = tempfile::tempdir().unwrap();
4694 let mut player = Player::new();
4695 player.backend = Box::new(StuckBackend {
4696 rate: 8_000.0,
4697 asked: Default::default(),
4698 starts: Default::default(),
4699 });
4700 let wav = |name: &str| {
4701 let path = dir.path().join(format!("{name}.wav"));
4702 crate::test_utils::generate_wav(&path, 8_000, 1, 1.0, 16);
4703 PlaylistItem {
4704 path,
4705 ..make_item(name)
4706 }
4707 };
4708 let items = vec![wav("a"), pending_item("arriving"), wav("c")];
4709 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
4710 player.process_command(PlayerCommand::AddToPlaylist(items));
4711 player.process_command(PlayerCommand::Play(ids[0]));
4712 await_queued(&player);
4713 wait_for_lookahead(&player);
4714
4715 assert!(
4716 player.timeline.queued_after_playhead().is_empty(),
4717 "c is not queued over the track before it"
4718 );
4719 player.process_command(PlayerCommand::DecodeFinished(player.session));
4721 assert_eq!(player.shared_state.cursor(), Some(ids[1]));
4722 assert!(player.waiting().is_some());
4723 player.process_command(PlayerCommand::Stop);
4724 }
4725
4726 fn wait_for_lookahead(player: &Player) {
4728 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
4729 while player
4730 .session()
4731 .is_some_and(|s| s.lookahead.lock().is_empty())
4732 {
4733 assert!(
4734 std::time::Instant::now() < deadline,
4735 "the decoder never looked ahead"
4736 );
4737 std::thread::yield_now();
4738 }
4739 }
4740
4741 #[test]
4742 fn an_edit_after_what_the_decoder_queued_leaves_playback_alone() {
4743 let dir = tempfile::tempdir().unwrap();
4744 let (mut player, ids) = wavs_playing(dir.path(), &["a", "b"], 30.0);
4745 await_queued(&player);
4746 await_queued(&player);
4747 let path = dir.path().join("later.wav");
4748 crate::test_utils::generate_wav(&path, 8_000, 1, 1.0, 16);
4749
4750 player.process_command(PlayerCommand::AddToPlaylist(vec![PlaylistItem {
4751 path,
4752 ..make_item("later")
4753 }]));
4754 assert_eq!(player.playback_starts, 1);
4755 assert_eq!(player.timeline.queued_after_playhead(), vec![ids[1]]);
4756 player.process_command(PlayerCommand::Stop);
4757 }
4758
4759 #[test]
4760 fn undoing_the_add_of_a_track_on_its_way_forgets_it() {
4761 let dir = tempfile::tempdir().unwrap();
4762 let (mut player, _, starts) = downloading_wav(dir.path());
4763 let path = dir.path().join("added.wav");
4764 crate::test_utils::generate_wav(&path, 8_000, 1, 1.0, 16);
4765 let added = PlaylistItem {
4766 path,
4767 state: ItemState::Pending,
4768 ..make_item("added")
4769 };
4770 let added_id = added.id;
4771 player.process_command(PlayerCommand::AddToPlaylist(vec![added]));
4772 player.process_command(PlayerCommand::Play(added_id));
4773
4774 player.process_command(PlayerCommand::Undo);
4775 assert!(player.waiting().is_none());
4776 assert_eq!(player.shared_state.playback_state(), PlaybackState::Stopped);
4777
4778 player.process_command(PlayerCommand::Redo);
4780 player
4781 .shared_state
4782 .update_item_state(added_id, ItemState::Ready);
4783 player.process_command(PlayerCommand::TrackReady(added_id));
4784 assert!(player.session().is_none());
4785 assert_eq!(starts.load(Ordering::Relaxed), 0);
4786 }
4787
4788 pub(super) struct Rng(pub(super) u64);
4791
4792 impl Rng {
4793 pub(super) fn next(&mut self) -> u64 {
4794 self.0 ^= self.0 << 13;
4795 self.0 ^= self.0 >> 7;
4796 self.0 ^= self.0 << 17;
4797 self.0
4798 }
4799
4800 pub(super) fn below(&mut self, n: usize) -> usize {
4801 (self.next() % n.max(1) as u64) as usize
4802 }
4803
4804 pub(super) fn coin(&mut self) -> bool {
4805 self.next() & 1 == 1
4806 }
4807 }
4808
4809 pub(super) fn asks_to_play(cmd: &PlayerCommand) -> bool {
4812 cmd.asks_to_play()
4813 }
4814
4815 pub(super) fn check_invariants(
4818 player: &Player,
4819 wanted_before: bool,
4820 asked: bool,
4821 ) -> Result<(), String> {
4822 let state = &player.shared_state;
4823 let ids = playlist_ids(player);
4824 let listed = |id: QueueItemId| ids.contains(&id);
4825 let mut seen = std::collections::HashSet::new();
4826 if !ids.iter().all(|id| seen.insert(*id)) {
4827 return Err("an item is in the playlist twice".into());
4828 }
4829 let cursor = state.cursor();
4830 if cursor.is_some_and(|c| !listed(c)) {
4831 return Err("the cursor is on an item not in the playlist".into());
4832 }
4833 let info = state.track_info();
4834 match (&player.transport, &info) {
4835 (Transport::Loaded(_), None) => return Err("loaded, but no track published".into()),
4836 (Transport::Loaded(_), Some(info)) => {
4837 if !listed(info.id) {
4838 return Err("playing an item not in the playlist".into());
4839 }
4840 if cursor != Some(info.id) {
4841 return Err("the cursor is not on what is playing".into());
4842 }
4843 if !matches!(
4844 state.playback_state(),
4845 PlaybackState::Playing | PlaybackState::Paused
4846 ) {
4847 return Err("loaded, but published as stopped".into());
4848 }
4849 }
4850 (_, Some(_)) => return Err("a track is published with nothing loaded".into()),
4851 (Transport::Waiting(w), None) => {
4852 if cursor != Some(w.id) || !listed(w.id) {
4853 return Err("waiting on an item that is not the cursor's".into());
4854 }
4855 let expected = match w.start {
4856 Run::Playing => PlaybackState::Stopped,
4857 Run::Paused => PlaybackState::Paused,
4858 };
4859 if state.playback_state() != expected {
4860 return Err("a wait published in the wrong state".into());
4861 }
4862 }
4863 (Transport::Idle, None) => {
4864 if state.playback_state() != PlaybackState::Stopped {
4865 return Err("idle, but not published as stopped".into());
4866 }
4867 }
4868 }
4869 if state.is_waiting() != player.waiting().is_some() {
4870 return Err("the published wait disagrees with the player".into());
4871 }
4872 if player
4873 .timeline
4874 .queued_after_playhead()
4875 .into_iter()
4876 .any(|id| !listed(id))
4877 {
4878 return Err("the decoder has queued an item no longer in the playlist".into());
4879 }
4880 if !asked && !wanted_before && state.wants_to_play() {
4881 return Err("started without being asked".into());
4882 }
4883 if state.play_mode() != player.mode {
4884 return Err("the published mode disagrees with the player".into());
4885 }
4886 if state.is_shuffled() != player.mode.shuffle {
4887 return Err("a play order kept, or not, against the mode".into());
4888 }
4889 if player.mode.repeat == Repeat::Off
4890 && let Some(session) = player.session()
4891 && let Some(playhead) = player.timeline.playhead()
4892 && session.track.id == playhead.id
4893 && session
4894 .lookahead
4895 .lock()
4896 .iter()
4897 .filter(|step| step.boundary > playhead.boundary)
4898 .any(|step| step.wrapped || step.next == Some(step.after))
4899 {
4900 return Err("repeat is off, but the decoder has queued a wrap".into());
4901 }
4902 Ok(())
4903 }
4904
4905 #[test]
4906 fn random_use_keeps_the_player_honest() {
4907 let dir = tempfile::tempdir().unwrap();
4908 let path = dir.path().join("t.wav");
4909 crate::test_utils::generate_wav(&path, 8_000, 1, 0.5, 16);
4910
4911 for seed in 1..=40u64 {
4912 let mut rng = Rng(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1);
4913 let mut player = Player::new();
4914 player.backend = Box::new(StuckBackend {
4915 rate: 8_000.0,
4916 asked: Default::default(),
4917 starts: Default::default(),
4918 });
4919 let fresh = |rng: &mut Rng| {
4920 let state = match rng.below(3) {
4921 0 => ItemState::Pending,
4922 _ => ItemState::Ready,
4923 };
4924 PlaylistItem {
4925 path: path.clone(),
4926 state,
4927 ..make_item("t")
4928 }
4929 };
4930 let start: Vec<_> = (0..5).map(|_| fresh(&mut rng)).collect();
4931 player.process_command(PlayerCommand::AddToPlaylist(start));
4932
4933 let mut history = Vec::new();
4934 for step in 0..150 {
4935 let ids = playlist_ids(&player);
4936 let pick = |rng: &mut Rng| ids.get(rng.below(ids.len())).copied();
4937 let cmd = match rng.below(23) {
4938 0 => pick(&mut rng).map(PlayerCommand::Play),
4939 1 => pick(&mut rng).map(|id| PlayerCommand::Cue {
4940 id,
4941 position_ms: if rng.coin() { 0 } else { 200 },
4942 play: rng.coin(),
4943 }),
4944 2 => Some(PlayerCommand::Pause),
4945 3 => Some(PlayerCommand::Resume),
4946 4 => Some(PlayerCommand::NextTrack),
4947 5 => Some(PlayerCommand::PrevTrack),
4948 6 => Some(PlayerCommand::Seek(100)),
4949 7 => pick(&mut rng).map(PlayerCommand::RemoveFromPlaylist),
4950 8 => Some(PlayerCommand::RemoveFromPlaylistBatch(
4951 (0..2).filter_map(|_| pick(&mut rng)).collect(),
4952 )),
4953 9 => pick(&mut rng).zip(pick(&mut rng)).map(|(id, target)| {
4954 PlayerCommand::MoveInPlaylist {
4955 id,
4956 target,
4957 after: rng.coin(),
4958 }
4959 }),
4960 10 => pick(&mut rng).map(|after| PlayerCommand::InsertInPlaylist {
4961 items: vec![fresh(&mut rng)],
4962 after,
4963 }),
4964 11 => Some(PlayerCommand::AddToPlaylist(vec![fresh(&mut rng)])),
4965 12 => Some(PlayerCommand::Undo),
4966 13 => Some(PlayerCommand::Redo),
4967 14 | 15 => {
4968 let (items, _) = player.shared_state.snapshot_playlist();
4969 let pending: Vec<_> = items
4970 .iter()
4971 .filter(|i| matches!(i.state, ItemState::Pending))
4972 .map(|i| i.id)
4973 .collect();
4974 pending.get(rng.below(pending.len())).map(|&id| {
4975 if rng.below(4) == 0 {
4976 player
4977 .shared_state
4978 .update_item_state(id, ItemState::Failed("gone".into()));
4979 PlayerCommand::TrackFailed(id)
4980 } else {
4981 player.shared_state.update_item_state(id, ItemState::Ready);
4982 PlayerCommand::TrackReady(id)
4983 }
4984 })
4985 }
4986 16 => Some(PlayerCommand::DecodeFinished(player.session)),
4987 17 => Some(PlayerCommand::DecodeFinished(
4988 player.session.wrapping_sub(1),
4989 )),
4990 18 => Some(PlayerCommand::ClearPlaylist),
4991 20 => Some(PlayerCommand::SetShuffle(rng.coin())),
4992 21 => Some(PlayerCommand::SetRepeat(
4993 [Repeat::Off, Repeat::Queue, Repeat::One][rng.below(3)],
4994 )),
4995 _ => Some(PlayerCommand::ReplacePlaylist {
4996 items: (0..3).map(|_| fresh(&mut rng)).collect(),
4997 start: rng.below(4),
4998 position_ms: if rng.coin() { 0 } else { 200 },
4999 play: rng.coin(),
5000 mode: [QueueMode::Keep, QueueMode::InOrder, QueueMode::Shuffled]
5001 [rng.below(3)],
5002 }),
5003 };
5004 let Some(cmd) = cmd else { continue };
5005 let label = format!("{cmd:?}");
5006 let asked = asks_to_play(&cmd);
5007 let wanted_before = player.shared_state.wants_to_play();
5008 player.process_command(cmd);
5009 while let Ok(sent) = player.commands.rx.try_recv() {
5013 player.process_command(sent);
5014 }
5015 player.update_playback_state();
5016 history.push(label);
5017 if let Err(broken) = check_invariants(&player, wanted_before, asked) {
5018 let tail = history[history.len().saturating_sub(8)..].join("\n ");
5019 panic!("seed {seed}, step {step}: {broken}\nlast commands:\n {tail}");
5020 }
5021 }
5022 player.process_command(PlayerCommand::Stop);
5023 }
5024 }
5025
5026 struct QuickEngine {
5027 silent: Arc<std::sync::atomic::AtomicBool>,
5028 quick_outs: Arc<std::sync::atomic::AtomicUsize>,
5029 outs: Arc<std::sync::atomic::AtomicUsize>,
5030 period: std::time::Duration,
5031 }
5032 impl AudioEngineHandle for QuickEngine {
5033 fn start(&self) -> Result<(), BackendError> {
5034 Ok(())
5035 }
5036 fn stop(&self) -> Result<(), BackendError> {
5037 Ok(())
5038 }
5039 fn is_running(&self) -> bool {
5040 true
5041 }
5042 fn fade_out(&self) {
5043 self.outs.fetch_add(1, Ordering::Relaxed);
5044 }
5045 fn fade_out_quickly(&self) {
5046 self.quick_outs.fetch_add(1, Ordering::Relaxed);
5047 }
5048 fn fade_in(&self) -> Result<(), BackendError> {
5049 Ok(())
5050 }
5051 fn is_silent(&self) -> bool {
5052 self.silent.load(Ordering::Relaxed)
5053 }
5054 fn period(&self) -> std::time::Duration {
5055 self.period
5056 }
5057 }
5058
5059 fn quick_player(
5062 frames: u32,
5063 rate: u32,
5064 ) -> (
5065 Player,
5066 Arc<std::sync::atomic::AtomicBool>,
5067 Arc<std::sync::atomic::AtomicUsize>,
5068 Arc<std::sync::atomic::AtomicUsize>,
5069 ) {
5070 let silent = Arc::new(std::sync::atomic::AtomicBool::new(false));
5071 let quick_outs = Arc::new(std::sync::atomic::AtomicUsize::new(0));
5072 let outs = Arc::new(std::sync::atomic::AtomicUsize::new(0));
5073 let mut player = Player::new();
5074 player.transport = Transport::Loaded(test_session(
5075 QueueItemId::new(),
5076 Box::new(QuickEngine {
5077 silent: silent.clone(),
5078 quick_outs: quick_outs.clone(),
5079 outs: outs.clone(),
5080 period: std::time::Duration::from_secs_f64(frames as f64 / rate as f64),
5081 }),
5082 ));
5083 (player, silent, quick_outs, outs)
5084 }
5085
5086 #[test]
5090 fn a_dsp_change_restarts_after_a_quick_fade_and_coalesces() {
5091 let (mut player, silent, quick_outs, outs) = quick_player(512, 44100);
5092
5093 player.restart_for_dsp();
5094 assert_eq!(quick_outs.load(Ordering::Relaxed), 1);
5095 assert_eq!(outs.load(Ordering::Relaxed), 0, "not the pause's fade");
5096
5097 player.restart_for_dsp();
5099 assert_eq!(quick_outs.load(Ordering::Relaxed), 1);
5100 player.update_playback_state();
5101 assert!(player.dsp_restart.is_some(), "not restarted while audible");
5102
5103 silent.store(true, Ordering::Relaxed);
5104 player.update_playback_state();
5105 assert!(player.dsp_restart.is_none(), "restarted once silent");
5106 assert!(
5107 !player.quick_start,
5108 "the quick start is the restart's alone"
5109 );
5110 }
5111
5112 #[test]
5117 fn a_dsp_change_wakes_a_buffer_period_after_its_fade() {
5118 use crate::audio::fade::QUICK_FADE;
5119 for (frames, rate) in [(512, 44100), (1024, 48000)] {
5120 let period = std::time::Duration::from_secs_f64(frames as f64 / rate as f64);
5121 let (mut player, silent, _, _) = quick_player(frames, rate);
5122 player.restart_for_dsp();
5123 let since = player.dsp_restart.expect("waiting on the fade");
5124
5125 let first = player.next_event().expect("a wake when the fade ends");
5126 assert_eq!(first, since + QUICK_FADE + period + BOUNDARY_SLACK);
5127 assert_eq!(dsp_wake(since, period, since), first);
5128
5129 player.update_playback_state();
5132 assert!(player.dsp_restart.is_some());
5133 let second = dsp_wake(since, period, first);
5134 assert_eq!(second, first + period);
5135 assert!(
5136 second < since + QUICK_FADE_LIMIT,
5137 "{frames} frames: inside the limit"
5138 );
5139 assert_eq!(dsp_wake(since, period, second), since + QUICK_FADE_LIMIT);
5140
5141 silent.store(true, Ordering::Relaxed);
5142 player.update_playback_state();
5143 assert!(
5144 player.dsp_restart.is_none(),
5145 "{frames} frames: restarted at silence"
5146 );
5147 }
5148 }
5149
5150 #[test]
5151 fn a_pause_reports_where_the_fade_went_silent() {
5152 use std::sync::atomic::AtomicBool;
5153
5154 struct FadingEngine {
5155 running: Arc<AtomicBool>,
5156 silent: Arc<AtomicBool>,
5157 }
5158 impl AudioEngineHandle for FadingEngine {
5159 fn start(&self) -> Result<(), BackendError> {
5160 self.running.store(true, Ordering::Relaxed);
5161 Ok(())
5162 }
5163 fn stop(&self) -> Result<(), BackendError> {
5164 self.running.store(false, Ordering::Relaxed);
5165 Ok(())
5166 }
5167 fn is_running(&self) -> bool {
5168 self.running.load(Ordering::Relaxed)
5169 }
5170 fn fade_out(&self) {}
5171 fn fade_in(&self) -> Result<(), BackendError> {
5172 Ok(())
5173 }
5174 fn is_silent(&self) -> bool {
5175 self.silent.load(Ordering::Relaxed)
5176 }
5177 }
5178
5179 let running = Arc::new(AtomicBool::new(true));
5180 let silent = Arc::new(AtomicBool::new(false));
5181 let mut player = Player::new();
5182 player.transport = Transport::Loaded(test_session(
5183 QueueItemId::new(),
5184 Box::new(FadingEngine {
5185 running: running.clone(),
5186 silent: silent.clone(),
5187 }),
5188 ));
5189 player
5190 .shared_state
5191 .set_playback_state(PlaybackState::Playing);
5192 player.shared_state.set_position_ms(5_000);
5193
5194 let (reply, answer) = crossbeam_channel::bounded(1);
5195 player.process_command(PlayerCommand::PauseAndReport(reply));
5196
5197 if crate::config::Config::cached().playback.fade_on_pause {
5198 assert!(answer.try_recv().is_err(), "not while the fade is audible");
5199 player.shared_state.set_position_ms(5_150);
5201 player.update_playback_state();
5202 assert!(answer.try_recv().is_err());
5203 silent.store(true, Ordering::Relaxed);
5204 player.update_playback_state();
5205 assert!(!running.load(Ordering::Relaxed));
5206 assert_eq!(answer.try_recv().unwrap(), 5_150);
5207 } else {
5208 assert_eq!(answer.try_recv().unwrap(), 5_000);
5209 }
5210 }
5211
5212 #[test]
5213 fn the_server_hears_each_turn_playback_takes() {
5214 use PlaybackReportState::{Paused, Playing, Stopped};
5215
5216 let dir = tempfile::tempdir().unwrap();
5217 let path = dir.path().join("t.wav");
5218 crate::test_utils::generate_wav(&path, 8_000, 1, 10.0, 16);
5219
5220 let mut player = Player::new();
5221 player.backend = Box::new(StuckBackend {
5222 rate: 8_000.0,
5223 asked: Default::default(),
5224 starts: Default::default(),
5225 });
5226 let (recorder, events) = PlayRecorder::capture();
5227 player.history = Some(recorder);
5228
5229 let item = PlaylistItem {
5230 db_id: Some(5),
5231 path,
5232 ..make_item("t")
5233 };
5234 let id = item.id;
5235 player.process_command(PlayerCommand::AddToPlaylist(vec![item]));
5236 player.process_command(PlayerCommand::Play(id));
5237 player.process_command(PlayerCommand::Pause);
5238 player.process_command(PlayerCommand::Seek(4_000));
5239 await_queued(&player);
5244 player.process_command(PlayerCommand::Resume);
5245 player.process_command(PlayerCommand::Stop);
5246
5247 let events: Vec<_> = events.try_iter().collect();
5248 let near_seek = |position_ms: u64| (3_750..=4_000).contains(&position_ms);
5249 let reports: Vec<_> = events
5250 .iter()
5251 .filter_map(|e| match e {
5252 PlayEvent::Playback(r) => Some((r.track_id, r.state, r.position_ms)),
5253 _ => None,
5254 })
5255 .collect();
5256 assert_eq!(
5257 events.first(),
5258 Some(&PlayEvent::Started {
5259 track_id: 5,
5260 position_ms: 0
5261 })
5262 );
5263 assert_eq!(reports.len(), 4, "{events:?}");
5264 assert_eq!(reports[0], (5, Paused, 0));
5265 for (report, state) in reports[1..].iter().zip([Paused, Playing, Stopped]) {
5266 assert_eq!((report.0, report.1), (5, state), "{events:?}");
5267 assert!(near_seek(report.2), "{events:?}");
5268 }
5269 assert_eq!(
5270 events.last(),
5271 Some(&PlayEvent::Finished {
5272 track_id: 5,
5273 listened_ms: 0
5274 })
5275 );
5276 }
5277
5278 #[test]
5279 fn removing_the_playing_track_resumes_at_its_successor() {
5280 let mut player = Player::new();
5281 let ids = seed(&mut player, 5);
5282 pretend_playing(&mut player, ids[2]);
5283
5284 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[2]));
5285
5286 assert_eq!(
5287 player.shared_state.cursor(),
5288 Some(ids[3]),
5289 "playback must continue at the next track, not restart the queue"
5290 );
5291 assert_eq!(player.playback_starts, 1);
5292 }
5293
5294 #[test]
5295 fn removing_the_paused_track_moves_on_paused() {
5296 let mut player = Player::new();
5297 let ids = seed(&mut player, 3);
5298 pretend_playing(&mut player, ids[1]);
5299 if let Transport::Loaded(session) = &mut player.transport {
5300 session.run = Run::Paused;
5301 }
5302
5303 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[1]));
5304 assert_eq!(player.shared_state.cursor(), Some(ids[2]));
5305 assert!(!player.shared_state.wants_to_play(), "still paused");
5306 }
5307
5308 #[test]
5309 fn removing_the_cursor_with_nothing_loaded_starts_nothing() {
5310 let mut player = Player::new();
5311 let ids = seed(&mut player, 3);
5312 player.shared_state.set_cursor(Some(ids[1]));
5313
5314 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[1]));
5315 assert_eq!(player.shared_state.cursor(), Some(ids[2]));
5316 assert_eq!(player.playback_starts, 0);
5317 }
5318
5319 #[test]
5320 fn removing_the_first_playing_track_resumes_at_the_new_first() {
5321 let mut player = Player::new();
5322 let ids = seed(&mut player, 3);
5323 pretend_playing(&mut player, ids[0]);
5324
5325 player.process_command(PlayerCommand::RemoveFromPlaylist(ids[0]));
5326
5327 assert_eq!(player.shared_state.cursor(), Some(ids[1]));
5328 }
5329
5330 #[test]
5331 fn next_track_parks_on_a_track_that_has_not_downloaded_yet() {
5332 let mut player = Player::new();
5333 let playing = make_item("playing");
5334 let waiting = pending_item("waiting");
5335 let later = make_item("later");
5336 let (playing_id, waiting_id) = (playing.id, waiting.id);
5337 player.process_command(PlayerCommand::AddToPlaylist(vec![playing, waiting, later]));
5338 pretend_playing(&mut player, playing_id);
5339
5340 player.process_command(PlayerCommand::DecodeFinished(player.session));
5341
5342 assert_eq!(
5343 player.shared_state.cursor(),
5344 Some(waiting_id),
5345 "the cursor parks on the track being fetched"
5346 );
5347 assert_eq!(
5348 player.playback_starts, 0,
5349 "nothing to play until its bytes land"
5350 );
5351
5352 player
5355 .shared_state
5356 .update_item_state(waiting_id, ItemState::Ready);
5357 player.process_command(PlayerCommand::TrackReady(waiting_id));
5358
5359 assert_eq!(player.playback_starts, 1);
5360 assert_eq!(player.shared_state.cursor(), Some(waiting_id));
5361 }
5362
5363 #[test]
5367 fn an_album_starts_at_its_first_track_however_much_is_cached() {
5368 let mut player = Player::new();
5369 let remote = pending_item("remote");
5370 let cached = [make_item("cached 2"), make_item("cached 3")];
5371 let first = remote.id;
5372 player.process_command(PlayerCommand::ReplacePlaylist {
5373 items: [vec![remote], cached.to_vec()].concat(),
5374 start: 0,
5375 position_ms: 0,
5376 play: true,
5377 mode: QueueMode::Keep,
5378 });
5379
5380 assert_eq!(player.shared_state.cursor(), Some(first));
5381 assert_eq!(player.playback_starts, 0, "nothing decodes before it lands");
5382
5383 player
5384 .shared_state
5385 .update_item_state(first, ItemState::Ready);
5386 player.process_command(PlayerCommand::TrackReady(first));
5387 assert_eq!(player.playback_starts, 1);
5388 assert_eq!(
5389 player.shared_state.cursor(),
5390 Some(first),
5391 "the first track decodes first"
5392 );
5393 }
5394
5395 #[test]
5398 fn a_stream_that_ends_unheard_mid_download_waits_for_its_track() {
5399 let mut player = Player::new();
5400 let streaming = pending_item("streaming");
5401 let next = make_item("next");
5402 let id = streaming.id;
5403 player.process_command(PlayerCommand::AddToPlaylist(vec![streaming, next]));
5404 pretend_playing(&mut player, id);
5405
5406 player.process_command(PlayerCommand::DecodeFinished(player.session));
5407 assert_eq!(player.shared_state.cursor(), Some(id), "not passed over");
5408 assert_eq!(player.playback_starts, 0);
5409
5410 player.shared_state.update_item_state(id, ItemState::Ready);
5411 player.process_command(PlayerCommand::TrackReady(id));
5412 assert_eq!(player.playback_starts, 1);
5413 assert_eq!(player.shared_state.cursor(), Some(id));
5414 }
5415
5416 #[test]
5417 fn a_download_that_cannot_land_moves_the_cursor_on() {
5418 let mut player = Player::new();
5419 let waiting = pending_item("waiting");
5420 let later = make_item("later");
5421 let (waiting_id, later_id) = (waiting.id, later.id);
5422 player.process_command(PlayerCommand::AddToPlaylist(vec![waiting, later]));
5423
5424 player.process_command(PlayerCommand::Play(waiting_id));
5425 assert_eq!(player.playback_starts, 0, "nothing to play yet");
5426
5427 player
5429 .shared_state
5430 .update_item_state(waiting_id, ItemState::Failed("remote unavailable".into()));
5431 player.process_command(PlayerCommand::TrackFailed(waiting_id));
5432
5433 assert_eq!(
5434 player.shared_state.cursor(),
5435 Some(later_id),
5436 "the queue moves past a track that can never load"
5437 );
5438 assert_eq!(player.playback_starts, 1);
5439 }
5440
5441 #[test]
5442 fn a_queue_that_can_never_load_stops_rather_than_waiting() {
5443 let mut player = Player::new();
5444 let first = pending_item("first");
5445 let second = pending_item("second");
5446 let (first_id, second_id) = (first.id, second.id);
5447 player.process_command(PlayerCommand::AddToPlaylist(vec![first, second]));
5448
5449 player.process_command(PlayerCommand::Play(first_id));
5450 for id in [first_id, second_id] {
5451 player
5452 .shared_state
5453 .update_item_state(id, ItemState::Failed("remote unavailable".into()));
5454 player.process_command(PlayerCommand::TrackFailed(id));
5455 }
5456
5457 assert_eq!(player.playback_starts, 0);
5458 assert_eq!(
5459 player.shared_state.playback_state(),
5460 PlaybackState::Stopped,
5461 "a stop the UI can see, not an indefinite wait for TrackReady"
5462 );
5463 }
5464
5465 #[test]
5466 fn a_failure_elsewhere_in_the_queue_leaves_the_cursor_alone() {
5467 let mut player = Player::new();
5468 let waiting = pending_item("waiting");
5469 let other = pending_item("other");
5470 let (waiting_id, other_id) = (waiting.id, other.id);
5471 player.process_command(PlayerCommand::AddToPlaylist(vec![waiting, other]));
5472 player.process_command(PlayerCommand::Play(waiting_id));
5473
5474 player
5475 .shared_state
5476 .update_item_state(other_id, ItemState::Failed("remote unavailable".into()));
5477 player.process_command(PlayerCommand::TrackFailed(other_id));
5478
5479 assert_eq!(
5480 player.shared_state.cursor(),
5481 Some(waiting_id),
5482 "a track still downloading keeps the cursor"
5483 );
5484 }
5485
5486 #[test]
5487 fn batch_delete_containing_the_cursor_restarts_the_engine_once() {
5488 let mut player = Player::new();
5489 let ids = seed(&mut player, 5);
5490 pretend_playing(&mut player, ids[2]);
5491
5492 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![
5493 ids[1], ids[2], ids[3],
5494 ]));
5495
5496 assert_eq!(playlist_titles(&player), vec!["t0", "t4"]);
5497 assert_eq!(player.shared_state.cursor(), Some(ids[4]));
5498 assert_eq!(
5499 player.playback_starts, 1,
5500 "one resume for the whole selection, not one per deleted track"
5501 );
5502 }
5503
5504 #[test]
5505 fn batch_delete_below_the_cursor_leaves_playback_alone() {
5506 let mut player = Player::new();
5507 let ids = seed(&mut player, 4);
5508 player.shared_state.set_cursor(Some(ids[0]));
5509
5510 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![ids[2], ids[3]]));
5511
5512 assert_eq!(player.shared_state.cursor(), Some(ids[0]));
5513 assert_eq!(player.playback_starts, 0);
5514 }
5515
5516 #[test]
5517 fn undo_of_a_batch_delete_restores_the_original_order() {
5518 let mut player = Player::new();
5522 let items = vec![
5523 make_item("A"),
5524 make_item("B"),
5525 make_item("C"),
5526 make_item("D"),
5527 ];
5528 let (b_id, c_id) = (items[1].id, items[2].id);
5529 player.process_command(PlayerCommand::AddToPlaylist(items));
5530
5531 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![c_id, b_id]));
5532 assert_eq!(playlist_titles(&player), vec!["A", "D"]);
5533
5534 player.process_command(PlayerCommand::Undo);
5535 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
5536 }
5537
5538 #[test]
5541 fn undo_add_removes_items() {
5542 let mut player = Player::new();
5543 let items = vec![make_item("A"), make_item("B")];
5544 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
5545
5546 player.process_command(PlayerCommand::AddToPlaylist(items));
5547 assert_eq!(playlist_ids(&player), ids);
5548 assert!(player.undo_stack().can_undo());
5549
5550 player.process_command(PlayerCommand::Undo);
5551 assert!(playlist_ids(&player).is_empty());
5552 assert!(player.undo_stack().can_redo());
5553 }
5554
5555 #[test]
5556 fn redo_add_restores_items() {
5557 let mut player = Player::new();
5558 let items = vec![make_item("A"), make_item("B")];
5559
5560 player.process_command(PlayerCommand::AddToPlaylist(items));
5561 player.process_command(PlayerCommand::Undo);
5562 assert!(playlist_ids(&player).is_empty());
5563
5564 player.process_command(PlayerCommand::Redo);
5565 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
5566 }
5567
5568 #[test]
5571 fn undo_remove_restores_item_at_position() {
5572 let mut player = Player::new();
5573 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5574 let b_id = items[1].id;
5575
5576 player.process_command(PlayerCommand::AddToPlaylist(items));
5577 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
5578 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
5579
5580 player.process_command(PlayerCommand::Undo);
5581 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5582 }
5583
5584 #[test]
5585 fn undo_remove_first_item() {
5586 let mut player = Player::new();
5587 let items = vec![make_item("A"), make_item("B")];
5588 let a_id = items[0].id;
5589
5590 player.process_command(PlayerCommand::AddToPlaylist(items));
5591 player.process_command(PlayerCommand::RemoveFromPlaylist(a_id));
5592 assert_eq!(playlist_titles(&player), vec!["B"]);
5593
5594 player.process_command(PlayerCommand::Undo);
5595 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
5596 }
5597
5598 #[test]
5599 fn undo_batch_remove_restores_all() {
5600 let mut player = Player::new();
5601 let items = vec![
5602 make_item("A"),
5603 make_item("B"),
5604 make_item("C"),
5605 make_item("D"),
5606 ];
5607 let b_id = items[1].id;
5608 let c_id = items[2].id;
5609
5610 player.process_command(PlayerCommand::AddToPlaylist(items));
5611 let version_before = player.shared_state.playlist_version();
5612 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![b_id, c_id]));
5613 assert_eq!(playlist_titles(&player), vec!["A", "D"]);
5614 assert_eq!(
5617 player.shared_state.playlist_version(),
5618 version_before + 1,
5619 "batch removal must bump the playlist version exactly once"
5620 );
5621
5622 player.process_command(PlayerCommand::Undo);
5624 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
5625 }
5626
5627 #[test]
5628 fn redo_batch_remove() {
5629 let mut player = Player::new();
5630 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5631 let a_id = items[0].id;
5632 let b_id = items[1].id;
5633
5634 player.process_command(PlayerCommand::AddToPlaylist(items));
5635 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![a_id, b_id]));
5636 player.process_command(PlayerCommand::Undo);
5637 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5638
5639 player.process_command(PlayerCommand::Redo);
5640 assert_eq!(playlist_titles(&player), vec!["C"]);
5641 }
5642
5643 #[test]
5644 fn redo_remove() {
5645 let mut player = Player::new();
5646 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5647 let b_id = items[1].id;
5648
5649 player.process_command(PlayerCommand::AddToPlaylist(items));
5650 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
5651 player.process_command(PlayerCommand::Undo);
5652 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5653
5654 player.process_command(PlayerCommand::Redo);
5655 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
5656 }
5657
5658 #[test]
5661 fn undo_insert_removes_inserted_items() {
5662 let mut player = Player::new();
5663 let items = vec![make_item("A"), make_item("C")];
5664 let a_id = items[0].id;
5665
5666 player.process_command(PlayerCommand::AddToPlaylist(items));
5667
5668 let inserted = vec![make_item("B")];
5669 player.process_command(PlayerCommand::InsertInPlaylist {
5670 items: inserted,
5671 after: a_id,
5672 });
5673 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5674
5675 player.process_command(PlayerCommand::Undo);
5676 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
5677 }
5678
5679 #[test]
5682 fn undo_move_restores_position() {
5683 let mut player = Player::new();
5684 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5685 let a_id = items[0].id;
5686 let c_id = items[2].id;
5687
5688 player.process_command(PlayerCommand::AddToPlaylist(items));
5689
5690 player.process_command(PlayerCommand::MoveInPlaylist {
5692 id: a_id,
5693 target: c_id,
5694 after: true,
5695 });
5696 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
5697
5698 player.process_command(PlayerCommand::Undo);
5699 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5700 }
5701
5702 #[test]
5703 fn redo_move() {
5704 let mut player = Player::new();
5705 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5706 let a_id = items[0].id;
5707 let c_id = items[2].id;
5708
5709 player.process_command(PlayerCommand::AddToPlaylist(items));
5710 player.process_command(PlayerCommand::MoveInPlaylist {
5711 id: a_id,
5712 target: c_id,
5713 after: true,
5714 });
5715 player.process_command(PlayerCommand::Undo);
5716 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5717
5718 player.process_command(PlayerCommand::Redo);
5719 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
5720 }
5721
5722 #[test]
5725 fn undo_batch_move() {
5726 let mut player = Player::new();
5727 let items = vec![
5728 make_item("A"),
5729 make_item("B"),
5730 make_item("C"),
5731 make_item("D"),
5732 ];
5733 let a_id = items[0].id;
5734 let b_id = items[1].id;
5735 let d_id = items[3].id;
5736
5737 player.process_command(PlayerCommand::AddToPlaylist(items));
5738
5739 player.process_command(PlayerCommand::MoveItemsInPlaylist {
5741 ids: vec![a_id, b_id],
5742 target: d_id,
5743 after: true,
5744 });
5745 assert_eq!(playlist_titles(&player), vec!["C", "D", "A", "B"]);
5746
5747 player.process_command(PlayerCommand::Undo);
5748 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
5749 }
5750
5751 #[test]
5754 fn undo_clear_restores_playlist() {
5755 let mut player = Player::new();
5756 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5757
5758 player.process_command(PlayerCommand::AddToPlaylist(items));
5759 player.process_command(PlayerCommand::ClearPlaylist);
5760 assert!(playlist_ids(&player).is_empty());
5761
5762 player.process_command(PlayerCommand::Undo);
5763 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5764 }
5765
5766 #[test]
5771 fn undoing_a_replace_does_not_leave_the_engine_on_an_orphaned_track() {
5772 let mut player = Player::new();
5773 let original = seed(&mut player, 3);
5774 player.shared_state.set_cursor(Some(original[0]));
5775 pretend_playing(&mut player, original[0]);
5776
5777 let replacement = vec![make_item("something else")];
5778 let orphan = replacement[0].id;
5779 player.process_command(PlayerCommand::ReplacePlaylist {
5780 items: replacement,
5781 start: 0,
5782 position_ms: 0,
5783 play: true,
5784 mode: QueueMode::Keep,
5785 });
5786 pretend_playing(&mut player, orphan);
5788
5789 player.process_command(PlayerCommand::Undo);
5790
5791 assert_eq!(playlist_ids(&player), original, "the queue comes back");
5792 assert!(
5793 player.shared_state.get_item(orphan).is_none(),
5794 "and the replacement is gone from it"
5795 );
5796 assert!(
5797 playing_id(&player).is_none_or(|id| player.shared_state.get_item(id).is_some()),
5798 "so nothing may still be playing out of it"
5799 );
5800 }
5801
5802 #[test]
5804 fn undoing_an_add_does_not_leave_the_engine_on_a_removed_track() {
5805 let mut player = Player::new();
5806 seed(&mut player, 2);
5807 let added = seed(&mut player, 1);
5808 pretend_playing(&mut player, added[0]);
5809
5810 player.process_command(PlayerCommand::Undo);
5811
5812 assert!(player.shared_state.get_item(added[0]).is_none());
5813 assert!(
5814 playing_id(&player).is_none_or(|id| player.shared_state.get_item(id).is_some()),
5815 "the engine cannot be left on the item the undo removed"
5816 );
5817 }
5818
5819 #[test]
5821 fn undoing_a_move_leaves_playback_alone() {
5822 let mut player = Player::new();
5823 let ids = seed(&mut player, 3);
5824 player.shared_state.set_cursor(Some(ids[0]));
5825 pretend_playing(&mut player, ids[0]);
5826 let starts = player.playback_starts;
5827
5828 player.process_command(PlayerCommand::MoveInPlaylist {
5829 id: ids[2],
5830 target: ids[0],
5831 after: false,
5832 });
5833 player.process_command(PlayerCommand::Undo);
5834
5835 assert_eq!(playlist_ids(&player), ids);
5836 assert_eq!(playing_id(&player), Some(ids[0]), "still on the same track");
5837 assert_eq!(player.playback_starts, starts, "and not restarted");
5838 }
5839
5840 #[test]
5841 fn redo_clear() {
5842 let mut player = Player::new();
5843 let items = vec![make_item("A"), make_item("B")];
5844
5845 player.process_command(PlayerCommand::AddToPlaylist(items));
5846 player.process_command(PlayerCommand::ClearPlaylist);
5847 player.process_command(PlayerCommand::Undo);
5848 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
5849
5850 player.process_command(PlayerCommand::Redo);
5851 assert!(playlist_ids(&player).is_empty());
5852 }
5853
5854 #[test]
5857 fn multiple_undos_in_sequence() {
5858 let mut player = Player::new();
5859
5860 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("A")]));
5861 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
5862 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("C")]));
5863 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5864
5865 player.process_command(PlayerCommand::Undo);
5866 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
5867
5868 player.process_command(PlayerCommand::Undo);
5869 assert_eq!(playlist_titles(&player), vec!["A"]);
5870
5871 player.process_command(PlayerCommand::Undo);
5872 assert!(playlist_ids(&player).is_empty());
5873 }
5874
5875 #[test]
5876 fn undo_redo_undo_cycle() {
5877 let mut player = Player::new();
5878 let items = vec![make_item("A"), make_item("B")];
5879
5880 player.process_command(PlayerCommand::AddToPlaylist(items));
5881 player.process_command(PlayerCommand::Undo);
5882 assert!(playlist_ids(&player).is_empty());
5883
5884 player.process_command(PlayerCommand::Redo);
5885 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
5886
5887 player.process_command(PlayerCommand::Undo);
5888 assert!(playlist_ids(&player).is_empty());
5889 }
5890
5891 #[test]
5892 fn new_action_clears_redo_stack() {
5893 let mut player = Player::new();
5894 let items = vec![make_item("A")];
5895
5896 player.process_command(PlayerCommand::AddToPlaylist(items));
5897 player.process_command(PlayerCommand::Undo);
5898 assert!(player.undo_stack().can_redo());
5899
5900 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
5902 assert!(!player.undo_stack().can_redo());
5903 }
5904
5905 #[test]
5906 fn undo_on_empty_stack_is_noop() {
5907 let mut player = Player::new();
5908 player.process_command(PlayerCommand::Undo);
5909 assert!(playlist_ids(&player).is_empty());
5910 }
5911
5912 #[test]
5913 fn redo_on_empty_stack_is_noop() {
5914 let mut player = Player::new();
5915 player.process_command(PlayerCommand::Redo);
5916 assert!(playlist_ids(&player).is_empty());
5917 }
5918
5919 #[test]
5922 fn playback_commands_not_undoable() {
5923 let mut player = Player::new();
5924 player.process_command(PlayerCommand::Pause);
5925 player.process_command(PlayerCommand::Resume);
5926 player.process_command(PlayerCommand::NextTrack);
5927 player.process_command(PlayerCommand::PrevTrack);
5928 assert!(!player.undo_stack().can_undo());
5929 }
5930
5931 #[test]
5932 fn update_paths_not_undoable() {
5933 let mut player = Player::new();
5934 let items = vec![make_item("A")];
5935 let id = items[0].id;
5936 player.process_command(PlayerCommand::AddToPlaylist(items));
5937
5938 let undo_count = player.undo_stack().undo_len();
5939 player.process_command(PlayerCommand::UpdatePaths(vec![(
5940 id,
5941 PathBuf::from("/new/path.flac"),
5942 )]));
5943 assert_eq!(player.undo_stack().undo_len(), undo_count);
5944 }
5945
5946 #[test]
5949 fn add_remove_undo_undo_produces_original() {
5950 let mut player = Player::new();
5951 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5952 let b_id = items[1].id;
5953 let original_titles = vec!["A", "B", "C"];
5954
5955 player.process_command(PlayerCommand::AddToPlaylist(items));
5956 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
5957 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
5958
5959 player.process_command(PlayerCommand::Undo);
5961 assert_eq!(playlist_titles(&player), original_titles);
5962
5963 player.process_command(PlayerCommand::Undo);
5965 assert!(playlist_ids(&player).is_empty());
5966 }
5967
5968 #[test]
5969 fn interleaved_adds_and_moves_undo() {
5970 let mut player = Player::new();
5971 let items = vec![make_item("A"), make_item("B"), make_item("C")];
5972 let a_id = items[0].id;
5973 let c_id = items[2].id;
5974
5975 player.process_command(PlayerCommand::AddToPlaylist(items));
5976
5977 player.process_command(PlayerCommand::MoveInPlaylist {
5979 id: a_id,
5980 target: c_id,
5981 after: true,
5982 });
5983 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
5984
5985 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("D")]));
5987 assert_eq!(playlist_titles(&player), vec!["B", "C", "A", "D"]);
5988
5989 player.process_command(PlayerCommand::Undo);
5991 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
5992
5993 player.process_command(PlayerCommand::Undo);
5995 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
5996 }
5997
5998 #[test]
6003 fn stop_engine_drops_engine_synchronously() {
6004 use std::sync::atomic::{AtomicBool, Ordering};
6005
6006 struct MockEngine {
6007 dropped: Arc<AtomicBool>,
6008 }
6009 impl AudioEngineHandle for MockEngine {
6010 fn start(&self) -> Result<(), BackendError> {
6011 Ok(())
6012 }
6013 fn stop(&self) -> Result<(), BackendError> {
6014 Ok(())
6015 }
6016 fn is_running(&self) -> bool {
6017 false
6018 }
6019 fn fade_out(&self) {}
6020 fn fade_in(&self) -> Result<(), BackendError> {
6021 Ok(())
6022 }
6023 fn is_silent(&self) -> bool {
6024 false
6025 }
6026 }
6027 impl Drop for MockEngine {
6028 fn drop(&mut self) {
6029 self.dropped.store(true, Ordering::SeqCst);
6030 }
6031 }
6032
6033 let dropped = Arc::new(AtomicBool::new(false));
6034
6035 let mut player = Player::new();
6036 player.transport = Transport::Loaded(test_session(
6037 QueueItemId::new(),
6038 Box::new(MockEngine {
6039 dropped: dropped.clone(),
6040 }),
6041 ));
6042
6043 player.stop_engine();
6044
6045 assert!(
6049 dropped.load(Ordering::SeqCst),
6050 "AudioEngine must be dropped synchronously in stop_engine (GitHub #89)"
6051 );
6052 }
6053
6054 #[test]
6055 fn abandoning_a_stream_wakes_a_reader_parked_at_the_write_head() {
6056 let live = LiveStream {
6057 feed: crate::remote::downloads::ByteFeed::new(),
6058 abandoned: Default::default(),
6059 };
6060 let feed = live.feed.clone();
6061 let started = std::time::Instant::now();
6062 let reader = thread::spawn(move || {
6063 feed.wait_past(
6064 0,
6065 std::time::Instant::now() + std::time::Duration::from_secs(30),
6066 )
6067 });
6068 thread::sleep(std::time::Duration::from_millis(50));
6069 live.abandon();
6070 reader.join().unwrap();
6071
6072 assert!(live.abandoned.load(std::sync::atomic::Ordering::Acquire));
6073 assert!(started.elapsed() < std::time::Duration::from_secs(5));
6074 }
6075
6076 pub(super) struct StuckBackend {
6081 pub(super) rate: f64,
6082 pub(super) asked: Arc<std::sync::Mutex<Option<(f64, u32)>>>,
6083 pub(super) starts: Arc<std::sync::atomic::AtomicUsize>,
6085 }
6086
6087 struct NullEngine {
6088 starts: Arc<std::sync::atomic::AtomicUsize>,
6089 running: std::sync::atomic::AtomicBool,
6090 lead_in: Arc<AtomicU64>,
6091 }
6092 impl AudioEngineHandle for NullEngine {
6093 fn start(&self) -> Result<(), BackendError> {
6094 self.starts.fetch_add(1, Ordering::Relaxed);
6095 self.running.store(true, Ordering::Relaxed);
6096 Ok(())
6097 }
6098 fn stop(&self) -> Result<(), BackendError> {
6099 self.running.store(false, Ordering::Relaxed);
6100 Ok(())
6101 }
6102 fn is_running(&self) -> bool {
6103 self.running.load(Ordering::Relaxed)
6104 }
6105 fn fade_out(&self) {}
6106 fn fade_in(&self) -> Result<(), BackendError> {
6107 Ok(())
6108 }
6109 fn is_silent(&self) -> bool {
6110 false
6111 }
6112 fn lead_in(&self, frames: u64) {
6113 self.lead_in.store(frames, Ordering::Relaxed);
6114 }
6115 }
6116
6117 impl AudioBackend for StuckBackend {
6118 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
6119 Ok(vec![self.default_device()?])
6120 }
6121 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
6122 Ok(backend::DeviceInfo {
6123 name: "Stuck DAC".into(),
6124 sample_rates: vec![self.rate],
6125 platform_id: 0,
6126 kind: Default::default(),
6127 })
6128 }
6129 fn supported_sample_rates(
6130 &self,
6131 _device: &backend::DeviceInfo,
6132 ) -> Result<Vec<f64>, BackendError> {
6133 Ok(vec![self.rate])
6134 }
6135 fn get_device_sample_rate(
6136 &self,
6137 _device: &backend::DeviceInfo,
6138 ) -> Result<f64, BackendError> {
6139 Ok(self.rate)
6140 }
6141 fn set_device_sample_rate(
6142 &self,
6143 _device: &backend::DeviceInfo,
6144 rate: f64,
6145 ) -> Result<f64, BackendError> {
6146 Err(BackendError::UnsupportedSampleRate(rate))
6147 }
6148 fn create_engine(
6149 &self,
6150 _device: &backend::DeviceInfo,
6151 sample_rate: f64,
6152 channels: u32,
6153 _consumer: rtrb::Consumer<f32>,
6154 _samples_played: Arc<AtomicU64>,
6155 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
6156 *self.asked.lock().unwrap() = Some((sample_rate, channels));
6157 Ok(Box::new(NullEngine {
6158 starts: self.starts.clone(),
6159 running: Default::default(),
6160 lead_in: Default::default(),
6161 }))
6162 }
6163 }
6164
6165 struct FallbackBackend {
6168 asked_depth: Arc<std::sync::Mutex<Option<Option<u16>>>>,
6169 mixed: std::sync::atomic::AtomicBool,
6170 }
6171
6172 impl AudioBackend for FallbackBackend {
6173 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
6174 Ok(vec![self.default_device()?])
6175 }
6176 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
6177 Ok(backend::DeviceInfo {
6178 name: "Android".into(),
6179 sample_rates: vec![],
6180 platform_id: 0,
6181 kind: Default::default(),
6182 })
6183 }
6184 fn supported_sample_rates(
6185 &self,
6186 _device: &backend::DeviceInfo,
6187 ) -> Result<Vec<f64>, BackendError> {
6188 Ok(vec![])
6189 }
6190 fn get_device_sample_rate(
6191 &self,
6192 _device: &backend::DeviceInfo,
6193 ) -> Result<f64, BackendError> {
6194 Ok(if self.mixed.load(std::sync::atomic::Ordering::Relaxed) {
6195 48000.0
6196 } else {
6197 44100.0
6198 })
6199 }
6200 fn set_device_sample_rate(
6201 &self,
6202 _device: &backend::DeviceInfo,
6203 rate: f64,
6204 ) -> Result<f64, BackendError> {
6205 Ok(rate)
6206 }
6207 fn prepare_output(
6208 &self,
6209 _device: &backend::DeviceInfo,
6210 _current: f64,
6211 rate: f64,
6212 _channels: u16,
6213 bit_depth: Option<u16>,
6214 ) -> Result<f64, BackendError> {
6215 *self.asked_depth.lock().unwrap() = Some(bit_depth);
6216 Ok(rate)
6217 }
6218 fn output_mixed(&self) -> Option<bool> {
6219 Some(self.mixed.load(std::sync::atomic::Ordering::Relaxed))
6220 }
6221 fn narrows_depth(&self) -> bool {
6222 true
6223 }
6224 fn create_engine(
6225 &self,
6226 _device: &backend::DeviceInfo,
6227 _sample_rate: f64,
6228 _channels: u32,
6229 _consumer: rtrb::Consumer<f32>,
6230 _samples_played: Arc<AtomicU64>,
6231 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
6232 self.mixed.store(true, std::sync::atomic::Ordering::Relaxed);
6233 Ok(Box::new(NullEngine {
6234 starts: Default::default(),
6235 running: Default::default(),
6236 lead_in: Default::default(),
6237 }))
6238 }
6239 }
6240
6241 #[test]
6242 fn a_session_assumes_its_first_tracks_depth_until_a_narrowing_output_opens() {
6243 let info = |bit_depth| buffer::StreamInfo {
6244 codec: "FLAC".into(),
6245 sample_rate: 44100,
6246 channels: 2,
6247 bit_depth,
6248 bitrate_kbps: None,
6249 duration_ms: 1000,
6250 };
6251 let mut player = Player::new();
6252 player.backend = Box::new(FallbackBackend {
6253 asked_depth: Default::default(),
6254 mixed: Default::default(),
6255 });
6256 assert_eq!(player.assumed_depth(&info(Some(16))), 16);
6257 assert_eq!(player.assumed_depth(&info(None)), 24);
6258
6259 player.backend = Box::new(StuckBackend {
6260 rate: 44100.0,
6261 asked: Default::default(),
6262 starts: Default::default(),
6263 });
6264 assert_eq!(player.assumed_depth(&info(Some(16))), 0, "nothing narrows");
6265 }
6266
6267 #[test]
6268 fn a_stream_that_opens_through_the_mixer_is_reported_mixed_at_its_rate() {
6269 let mut player = Player::new();
6270 let asked_depth = Arc::new(std::sync::Mutex::new(None));
6271 player.backend = Box::new(FallbackBackend {
6272 asked_depth: asked_depth.clone(),
6273 mixed: Default::default(),
6274 });
6275 let state = player.shared_state.clone();
6276 let info = buffer::StreamInfo {
6277 codec: "FLAC".into(),
6278 sample_rate: 44100,
6279 channels: 2,
6280 bit_depth: Some(16),
6281 bitrate_kbps: None,
6282 duration_ms: 1000,
6283 };
6284 let (_producer, consumer) = rtrb::RingBuffer::new(16);
6285 player.create_engine_for(&info, consumer).unwrap();
6286 assert_eq!(*asked_depth.lock().unwrap(), Some(Some(16)));
6287 assert_eq!(
6288 state.output_mixed(),
6289 Some(true),
6290 "said of the stream as it opened"
6291 );
6292 assert_eq!(state.output_sample_rate(), Some(48000));
6293 }
6294
6295 struct CaptureBackend {
6298 consumers: Arc<std::sync::Mutex<Vec<rtrb::Consumer<f32>>>>,
6299 }
6300
6301 impl AudioBackend for CaptureBackend {
6302 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
6303 Ok(vec![self.default_device()?])
6304 }
6305 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
6306 Ok(backend::DeviceInfo {
6307 name: "Capture DAC".into(),
6308 sample_rates: vec![44100.0],
6309 platform_id: 0,
6310 kind: Default::default(),
6311 })
6312 }
6313 fn supported_sample_rates(
6314 &self,
6315 _device: &backend::DeviceInfo,
6316 ) -> Result<Vec<f64>, BackendError> {
6317 Ok(vec![44100.0])
6318 }
6319 fn get_device_sample_rate(
6320 &self,
6321 _device: &backend::DeviceInfo,
6322 ) -> Result<f64, BackendError> {
6323 Ok(44100.0)
6324 }
6325 fn set_device_sample_rate(
6326 &self,
6327 _device: &backend::DeviceInfo,
6328 rate: f64,
6329 ) -> Result<f64, BackendError> {
6330 Ok(rate)
6331 }
6332 fn create_engine(
6333 &self,
6334 _device: &backend::DeviceInfo,
6335 _sample_rate: f64,
6336 _channels: u32,
6337 consumer: rtrb::Consumer<f32>,
6338 _samples_played: Arc<AtomicU64>,
6339 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
6340 self.consumers.lock().unwrap().push(consumer);
6341 Ok(Box::new(NullEngine {
6342 starts: Default::default(),
6343 running: Default::default(),
6344 lead_in: Default::default(),
6345 }))
6346 }
6347 }
6348
6349 fn peak_reaching_the_device(
6351 consumers: &std::sync::Mutex<Vec<rtrb::Consumer<f32>>>,
6352 want: usize,
6353 ) -> f32 {
6354 let mut consumer = consumers.lock().unwrap().pop().expect("an engine was made");
6355 let mut peak = 0.0f32;
6356 let mut got = 0;
6357 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
6358 while got < want && std::time::Instant::now() < deadline {
6359 let n = consumer.slots();
6360 if n == 0 {
6361 std::thread::sleep(std::time::Duration::from_millis(2));
6362 continue;
6363 }
6364 let chunk = consumer.read_chunk(n).unwrap();
6365 let (a, b) = chunk.as_slices();
6366 peak = a.iter().chain(b).fold(peak, |p, s| p.max(s.abs()));
6367 got += n;
6368 chunk.commit_all();
6369 }
6370 assert!(got >= want, "only {got} of {want} samples arrived");
6371 peak
6372 }
6373
6374 #[test]
6378 fn a_profile_processes_files_and_streams_alike() {
6379 let dir = tempfile::tempdir().unwrap();
6380 let tone = dir.path().join("tone.wav");
6381 crate::test_utils::generate_wav_tone(&tone, 44100, 440.0, 0.5);
6382 let want = 22050;
6383
6384 let consumers = Arc::new(std::sync::Mutex::new(Vec::new()));
6385 let mut player = Player::new();
6386 player.backend = Box::new(CaptureBackend {
6387 consumers: consumers.clone(),
6388 });
6389 let mut item = make_item("tone");
6390 item.path = tone.clone();
6391 let id = item.id;
6392 player.shared_state.add_items(vec![item]);
6393 player.shared_state.set_cursor(Some(id));
6394
6395 let stream = || {
6396 let feed = crate::remote::downloads::ByteFeed::new();
6397 feed.set(std::fs::metadata(&tone).unwrap().len());
6398 Source::Stream(StreamSource {
6399 path: tone.clone(),
6400 bytes_written: feed,
6401 total: 0,
6402 mode: streaming::ProbeMode::Full,
6403 })
6404 };
6405 let peaks = |player: &mut Player| {
6406 let mut out = Vec::new();
6407 for source in [Source::File(tone.clone()), stream()] {
6408 player
6409 .try_open_session(id, source, None, 0, Run::Playing)
6410 .unwrap();
6411 player.publish();
6412 out.push((
6413 peak_reaching_the_device(&consumers, want),
6414 player.shared_state.dsp().map(|d| d.profile),
6415 ));
6416 player.stop_engine();
6417 }
6418 out
6419 };
6420
6421 let untouched = peaks(&mut player);
6422
6423 player.dsp_override = Some(Arc::new(
6424 crate::audio::dsp::Setup::new(vec![], vec![]).with_preamp(-6.0206),
6425 ));
6426 let processed = peaks(&mut player);
6427
6428 for (kind, ((before, none), (after, half))) in ["file", "stream"]
6429 .iter()
6430 .zip(untouched.iter().zip(&processed))
6431 {
6432 assert_eq!(
6433 none, &None,
6434 "{kind}: nothing is published without a profile"
6435 );
6436 assert_eq!(half.as_deref(), Some("test"), "{kind}: the badge names it");
6437 assert!(*before > 0.1, "{kind}: the tone reached the device");
6438 assert!(
6439 (after / before - 0.5).abs() < 0.01,
6440 "{kind}: {before} → {after}, not halved"
6441 );
6442 }
6443 }
6444
6445 #[test]
6451 fn a_replaced_queue_never_sounds_the_track_it_replaced() {
6452 let dir = tempfile::tempdir().unwrap();
6453 let a = dir.path().join("a.wav");
6454 crate::test_utils::generate_wav_tone(&a, 44100, 440.0, 5.0);
6455 let consumers = Arc::new(std::sync::Mutex::new(Vec::new()));
6456 let mut player = Player::new();
6457 player.backend = Box::new(CaptureBackend {
6458 consumers: consumers.clone(),
6459 });
6460 let engines = || consumers.lock().unwrap().len();
6461
6462 let item_a = PlaylistItem {
6463 path: a,
6464 ..make_item("paused")
6465 };
6466 let a_id = item_a.id;
6467 player.process_command(PlayerCommand::AddToPlaylist(vec![item_a]));
6468 player.process_command(PlayerCommand::Play(a_id));
6469 player.process_command(PlayerCommand::Pause);
6470 assert_eq!(engines(), 1);
6471
6472 let landed = dir.path().join("long.opus");
6473 let item_b = PlaylistItem {
6474 db_id: Some(77),
6475 state: ItemState::Pending,
6476 duration_ms: Some(32_523_781),
6477 path: landed.clone(),
6478 ..make_item("long")
6479 };
6480 let b_id = item_b.id;
6481 player.process_command(PlayerCommand::ReplacePlaylist {
6482 items: vec![item_b],
6483 start: 0,
6484 position_ms: 0,
6485 play: true,
6486 mode: QueueMode::Keep,
6487 });
6488 player.publish();
6489 let state = player.shared_state.clone();
6490 assert_eq!(state.cursor(), Some(b_id), "the new track, at once");
6491 assert!(
6492 state.is_waiting(),
6493 "waiting for it, which a client shows as loading"
6494 );
6495 assert_eq!(state.playback_state(), PlaybackState::Stopped);
6496 assert_eq!(
6497 state.track_info(),
6498 None,
6499 "the paused track is no longer published"
6500 );
6501 assert_eq!(engines(), 1, "nothing opened while it is on its way");
6502
6503 let store = state.downloads().clone();
6505 store.claim(77, Some(b_id));
6506 std::fs::write(&landed, include_bytes!("testdata/thirty-seconds.opus")).unwrap();
6507 let _settled = crate::remote::downloads::settle(&state, 77, &Ok(landed));
6509 player.process_command(PlayerCommand::TrackReady(b_id));
6510 player.publish();
6511
6512 assert_eq!(
6513 engines(),
6514 2,
6515 "one engine for the new track, none for the old"
6516 );
6517 assert_eq!(state.track_info().map(|t| t.id), Some(b_id));
6518 assert_eq!(state.playback_state(), PlaybackState::Playing);
6519 player.process_command(PlayerCommand::Stop);
6520 }
6521
6522 #[test]
6527 fn a_stream_opened_without_a_length_takes_the_librarys_duration() {
6528 let dir = tempfile::tempdir().unwrap();
6529 let bytes = include_bytes!("testdata/thirty-seconds.opus");
6530 let half = bytes.len() / 2;
6531 let part = dir.path().join("long.opus.part");
6532 std::fs::write(&part, &bytes[..half]).unwrap();
6533 let feed = crate::remote::downloads::ByteFeed::new();
6534 feed.set(half as u64);
6535
6536 let mut player = Player::new();
6537 player.backend = Box::new(CaptureBackend {
6538 consumers: Default::default(),
6539 });
6540 let item = PlaylistItem {
6541 db_id: Some(9),
6542 state: ItemState::Pending,
6543 duration_ms: Some(32_523_781),
6544 path: dir.path().join("long.opus"),
6545 ..make_item("long")
6546 };
6547 let id = item.id;
6548 player.shared_state.add_items(vec![item]);
6549 player.shared_state.set_cursor(Some(id));
6550
6551 let lengthless = buffer::StreamInfo {
6552 codec: "Opus".into(),
6553 sample_rate: 48_000,
6554 channels: 1,
6555 bit_depth: None,
6556 bitrate_kbps: None,
6557 duration_ms: 0,
6558 };
6559 player
6560 .try_open_session(
6561 id,
6562 Source::Stream(StreamSource {
6563 path: part,
6564 bytes_written: feed,
6565 total: bytes.len() as u64,
6566 mode: streaming::ProbeMode::Lengthless,
6567 }),
6568 Some(lengthless),
6569 0,
6570 Run::Playing,
6571 )
6572 .unwrap();
6573 player.publish();
6574
6575 assert_eq!(player.shared_state.duration_ms(), 32_523_781);
6576 assert_eq!(
6577 player.shared_state.track_info().map(|t| t.duration_ms),
6578 Some(32_523_781)
6579 );
6580 player.stop_engine();
6581 }
6582
6583 fn engine_format_for(source_rate: u32, channels: u16, device_rate: f64) -> (f64, u32) {
6584 let asked = Arc::new(std::sync::Mutex::new(None));
6585 let mut player = Player::new();
6586 player.backend = Box::new(StuckBackend {
6587 rate: device_rate,
6588 asked: asked.clone(),
6589 starts: Default::default(),
6590 });
6591
6592 let info = buffer::StreamInfo {
6593 codec: "MP3".into(),
6594 sample_rate: source_rate,
6595 channels,
6596 bit_depth: Some(16),
6597 bitrate_kbps: None,
6598 duration_ms: 1000,
6599 };
6600 let (_producer, consumer) = rtrb::RingBuffer::new(16);
6601 player
6602 .create_engine_for(&info, consumer)
6603 .expect("engine creation should succeed");
6604 let asked = *asked.lock().unwrap();
6605 asked.expect("engine was never created")
6606 }
6607
6608 #[test]
6609 fn engine_uses_source_rate_when_device_refuses_switch() {
6610 assert_eq!(engine_format_for(22050, 2, 48000.0), (22050.0, 2));
6613 assert_eq!(engine_format_for(32000, 2, 44100.0), (32000.0, 2));
6614 }
6615
6616 #[test]
6617 fn engine_uses_source_channel_count() {
6618 assert_eq!(engine_format_for(44100, 1, 44100.0), (44100.0, 1));
6619 }
6620
6621 fn settled_rate_for(source_rate: u32, device_rate: f64) -> Option<u32> {
6623 let mut player = Player::new();
6624 player.backend = Box::new(StuckBackend {
6625 rate: device_rate,
6626 asked: Arc::new(std::sync::Mutex::new(None)),
6627 starts: Default::default(),
6628 });
6629 let state = player.shared_state.clone();
6630
6631 let info = buffer::StreamInfo {
6632 codec: "MP3".into(),
6633 sample_rate: source_rate,
6634 channels: 2,
6635 bit_depth: Some(16),
6636 bitrate_kbps: None,
6637 duration_ms: 1000,
6638 };
6639 let (_producer, consumer) = rtrb::RingBuffer::new(16);
6640 player
6641 .create_engine_for(&info, consumer)
6642 .expect("engine creation should succeed");
6643 state.output_sample_rate()
6644 }
6645
6646 #[test]
6647 fn settled_device_rate_reaches_the_shared_state() {
6648 assert_eq!(settled_rate_for(22050, 48000.0), Some(48000));
6652 assert_eq!(settled_rate_for(44100, 44100.0), Some(44100));
6654 }
6655
6656 struct SlowBackend {
6658 observed: Arc<std::sync::Mutex<Vec<Option<u32>>>>,
6659 state: Arc<SharedPlayerState>,
6660 lead_in: Arc<AtomicU64>,
6661 }
6662
6663 impl AudioBackend for SlowBackend {
6664 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
6665 Ok(vec![self.default_device()?])
6666 }
6667 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
6668 Ok(backend::DeviceInfo {
6669 name: "Slow DAC".into(),
6670 sample_rates: vec![44100.0, 48000.0],
6671 platform_id: 0,
6672 kind: Default::default(),
6673 })
6674 }
6675 fn supported_sample_rates(
6676 &self,
6677 _device: &backend::DeviceInfo,
6678 ) -> Result<Vec<f64>, BackendError> {
6679 Ok(vec![44100.0, 48000.0])
6680 }
6681 fn get_device_sample_rate(
6682 &self,
6683 _device: &backend::DeviceInfo,
6684 ) -> Result<f64, BackendError> {
6685 Ok(48000.0)
6686 }
6687 fn set_device_sample_rate(
6688 &self,
6689 _device: &backend::DeviceInfo,
6690 rate: f64,
6691 ) -> Result<f64, BackendError> {
6692 self.observed
6694 .lock()
6695 .unwrap()
6696 .push(self.state.output_sample_rate());
6697 Ok(rate)
6698 }
6699 fn create_engine(
6700 &self,
6701 _device: &backend::DeviceInfo,
6702 _sample_rate: f64,
6703 _channels: u32,
6704 _consumer: rtrb::Consumer<f32>,
6705 _samples_played: Arc<AtomicU64>,
6706 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
6707 Ok(Box::new(NullEngine {
6708 starts: Default::default(),
6709 running: Default::default(),
6710 lead_in: self.lead_in.clone(),
6711 }))
6712 }
6713 }
6714
6715 #[test]
6716 fn the_previous_rate_is_not_published_while_the_device_reclocks() {
6717 let mut player = Player::new();
6723 let state = player.shared_state.clone();
6724 state.set_output_sample_rate(48000);
6725
6726 let observed = Arc::new(std::sync::Mutex::new(Vec::new()));
6727 player.backend = Box::new(SlowBackend {
6728 observed: observed.clone(),
6729 state: state.clone(),
6730 lead_in: Default::default(),
6731 });
6732
6733 let info = buffer::StreamInfo {
6734 codec: "FLAC".into(),
6735 sample_rate: 44100,
6736 channels: 2,
6737 bit_depth: Some(16),
6738 bitrate_kbps: None,
6739 duration_ms: 1000,
6740 };
6741 let (_producer, consumer) = rtrb::RingBuffer::new(16);
6742 player
6743 .create_engine_for(&info, consumer)
6744 .expect("engine creation should succeed");
6745
6746 assert_eq!(
6747 *observed.lock().unwrap(),
6748 vec![None],
6749 "mid-switch the output rate must read as unknown, not as the last track's"
6750 );
6751 assert_eq!(state.output_sample_rate(), Some(44100));
6752 }
6753
6754 fn lead_in_for(source_rate: u32) -> u64 {
6757 let mut player = Player::new();
6758 let lead_in = Arc::new(AtomicU64::new(0));
6759 player.backend = Box::new(SlowBackend {
6760 observed: Default::default(),
6761 state: player.shared_state.clone(),
6762 lead_in: lead_in.clone(),
6763 });
6764 let info = buffer::StreamInfo {
6765 codec: "FLAC".into(),
6766 sample_rate: source_rate,
6767 channels: 2,
6768 bit_depth: Some(16),
6769 bitrate_kbps: None,
6770 duration_ms: 1000,
6771 };
6772 let (_producer, consumer) = rtrb::RingBuffer::new(16);
6773 player
6774 .create_engine_for(&info, consumer)
6775 .expect("engine creation should succeed");
6776 lead_in.load(Ordering::Relaxed)
6777 }
6778
6779 #[test]
6780 fn an_engine_made_inside_the_silence_keeps_the_rest_of_it() {
6781 let mut player = Player::new();
6784 let lead_in = Arc::new(AtomicU64::new(0));
6785 player.backend = Box::new(SlowBackend {
6786 observed: Default::default(),
6787 state: player.shared_state.clone(),
6788 lead_in: lead_in.clone(),
6789 });
6790 let info = |sample_rate| buffer::StreamInfo {
6791 codec: "FLAC".into(),
6792 sample_rate,
6793 channels: 2,
6794 bit_depth: Some(16),
6795 bitrate_kbps: None,
6796 duration_ms: 1000,
6797 };
6798 let (_p, consumer) = rtrb::RingBuffer::new(16);
6799 player.create_engine_for(&info(44100), consumer).unwrap();
6800 let first = lead_in.swap(0, Ordering::Relaxed);
6801 assert!(first > 0);
6802
6803 let (_p, consumer) = rtrb::RingBuffer::new(16);
6804 player.create_engine_for(&info(48000), consumer).unwrap();
6805 let carried = lead_in.load(Ordering::Relaxed);
6806 assert!(
6808 carried > 0 && carried < first * 48000 / 44100,
6809 "carried {carried} of {first}"
6810 );
6811 }
6812
6813 #[test]
6814 fn silence_leads_in_only_after_the_device_changed_rate() {
6815 assert!(
6816 lead_in_for(44100) > 0,
6817 "the device is relocking, so the start of the track would be lost"
6818 );
6819 assert_eq!(lead_in_for(48000), 0, "no switch, nothing to wait for");
6820 }
6821
6822 #[cfg(target_os = "macos")]
6824 struct IosRates {
6825 lead_in: Arc<AtomicU64>,
6826 }
6827
6828 #[cfg(target_os = "macos")]
6829 impl AudioBackend for IosRates {
6830 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
6831 crate::audio::ios_backend::IosAudioBackend.list_devices()
6832 }
6833 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
6834 crate::audio::ios_backend::IosAudioBackend.default_device()
6835 }
6836 fn supported_sample_rates(
6837 &self,
6838 device: &backend::DeviceInfo,
6839 ) -> Result<Vec<f64>, BackendError> {
6840 crate::audio::ios_backend::IosAudioBackend.supported_sample_rates(device)
6841 }
6842 fn get_device_sample_rate(
6843 &self,
6844 device: &backend::DeviceInfo,
6845 ) -> Result<f64, BackendError> {
6846 crate::audio::ios_backend::IosAudioBackend.get_device_sample_rate(device)
6847 }
6848 fn set_device_sample_rate(
6849 &self,
6850 device: &backend::DeviceInfo,
6851 rate: f64,
6852 ) -> Result<f64, BackendError> {
6853 crate::audio::ios_backend::IosAudioBackend.set_device_sample_rate(device, rate)
6854 }
6855 fn create_engine(
6856 &self,
6857 _device: &backend::DeviceInfo,
6858 _sample_rate: f64,
6859 _channels: u32,
6860 _consumer: rtrb::Consumer<f32>,
6861 _samples_played: Arc<AtomicU64>,
6862 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
6863 Ok(Box::new(NullEngine {
6864 starts: Default::default(),
6865 running: Default::default(),
6866 lead_in: self.lead_in.clone(),
6867 }))
6868 }
6869 }
6870
6871 #[cfg(target_os = "macos")]
6873 struct SwitchingDac;
6874
6875 #[cfg(target_os = "macos")]
6876 impl crate::audio::ios_backend::AudioSession for SwitchingDac {
6877 fn activate(&self, sample_rate: f64) -> Option<f64> {
6878 Some(self.follow(sample_rate))
6879 }
6880 fn follow(&self, sample_rate: f64) -> f64 {
6881 if sample_rate == 44100.0 {
6882 44100.0
6883 } else {
6884 48000.0
6885 }
6886 }
6887 fn release(&self) {}
6888 }
6889
6890 #[test]
6891 #[cfg(target_os = "macos")]
6892 fn a_session_let_go_is_no_rate_switch_to_wait_out() {
6893 use crate::audio::ios_backend;
6894 let _session = ios_backend::TEST_SESSION.lock();
6895 ios_backend::set_session(Arc::new(SwitchingDac));
6896 let lead_in_at = |sample_rate| {
6897 let mut player = Player::new();
6898 let lead_in = Arc::new(AtomicU64::new(0));
6899 player.backend = Box::new(IosRates {
6900 lead_in: lead_in.clone(),
6901 });
6902 let info = buffer::StreamInfo {
6903 codec: "FLAC".into(),
6904 sample_rate,
6905 channels: 2,
6906 bit_depth: Some(16),
6907 bitrate_kbps: None,
6908 duration_ms: 1000,
6909 };
6910 let (_producer, consumer) = rtrb::RingBuffer::new(16);
6911 player.create_engine_for(&info, consumer).unwrap();
6912 lead_in.load(Ordering::Relaxed)
6913 };
6914
6915 assert!(ios_backend::activate_session(48000.0));
6917 assert!(lead_in_at(44100) > 0);
6918
6919 assert!(ios_backend::activate_session(48000.0));
6923 ios_backend::release_session();
6924 assert_eq!(lead_in_at(44100), 0);
6925 }
6926
6927 struct WatchedBackend {
6929 inner: StuckBackend,
6930 #[allow(clippy::type_complexity)]
6931 captured: Arc<std::sync::Mutex<Option<Box<dyn Fn(f64) + Send + Sync>>>>,
6932 }
6933
6934 struct NullWatch;
6935 impl backend::SampleRateWatch for NullWatch {}
6936
6937 impl AudioBackend for WatchedBackend {
6938 fn list_devices(&self) -> Result<Vec<backend::DeviceInfo>, BackendError> {
6939 self.inner.list_devices()
6940 }
6941 fn default_device(&self) -> Result<backend::DeviceInfo, BackendError> {
6942 self.inner.default_device()
6943 }
6944 fn supported_sample_rates(
6945 &self,
6946 device: &backend::DeviceInfo,
6947 ) -> Result<Vec<f64>, BackendError> {
6948 self.inner.supported_sample_rates(device)
6949 }
6950 fn get_device_sample_rate(
6951 &self,
6952 device: &backend::DeviceInfo,
6953 ) -> Result<f64, BackendError> {
6954 self.inner.get_device_sample_rate(device)
6955 }
6956 fn set_device_sample_rate(
6957 &self,
6958 device: &backend::DeviceInfo,
6959 rate: f64,
6960 ) -> Result<f64, BackendError> {
6961 self.inner.set_device_sample_rate(device, rate)
6962 }
6963 fn watch_device_sample_rate(
6964 &self,
6965 _device: &backend::DeviceInfo,
6966 on_change: Box<dyn Fn(f64) + Send + Sync>,
6967 ) -> Option<Box<dyn backend::SampleRateWatch>> {
6968 *self.captured.lock().unwrap() = Some(on_change);
6969 Some(Box::new(NullWatch))
6970 }
6971 fn create_engine(
6972 &self,
6973 device: &backend::DeviceInfo,
6974 sample_rate: f64,
6975 channels: u32,
6976 consumer: rtrb::Consumer<f32>,
6977 samples_played: Arc<AtomicU64>,
6978 ) -> Result<Box<dyn AudioEngineHandle>, BackendError> {
6979 self.inner
6980 .create_engine(device, sample_rate, channels, consumer, samples_played)
6981 }
6982 }
6983
6984 #[test]
6985 fn external_rate_change_reaches_the_shared_state() {
6986 let captured = Arc::new(std::sync::Mutex::new(None));
6990 let mut player = Player::new();
6991 player.backend = Box::new(WatchedBackend {
6992 inner: StuckBackend {
6993 rate: 44100.0,
6994 asked: Arc::new(std::sync::Mutex::new(None)),
6995 starts: Default::default(),
6996 },
6997 captured: captured.clone(),
6998 });
6999 let state = player.shared_state.clone();
7000
7001 let info = buffer::StreamInfo {
7002 codec: "FLAC".into(),
7003 sample_rate: 44100,
7004 channels: 2,
7005 bit_depth: Some(16),
7006 bitrate_kbps: None,
7007 duration_ms: 1000,
7008 };
7009 let (_producer, consumer) = rtrb::RingBuffer::new(16);
7010 player
7011 .create_engine_for(&info, consumer)
7012 .expect("engine creation should succeed");
7013 assert_eq!(state.output_sample_rate(), Some(44100));
7014
7015 let on_change = captured.lock().unwrap().take().expect("watch registered");
7016 on_change(48000.0);
7017 assert_eq!(state.output_sample_rate(), Some(48000));
7018 }
7019}