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