1pub mod commands;
2pub mod state;
3pub mod undo;
4
5use std::path::Path;
6use std::sync::Arc;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::thread;
9
10use thiserror::Error;
11
12use crate::audio::{
13 analyzer::VizAnalyzer,
14 backend::{self, AudioBackend, AudioEngineHandle, BackendError},
15 buffer, streaming,
16 viz::{VizBuffer, VizSnapshot},
17};
18use buffer::PlaybackTimeline;
19use commands::{CommandChannel, PlayerCommand};
20use state::{LoadState, PlaybackSource, PlaybackState, QueueItemId, SharedPlayerState, TrackInfo};
21use undo::{UndoEntry, UndoStack};
22
23const RING_BUFFER_SIZE: usize = 192_000 * 2;
25
26#[derive(Debug, Error)]
27pub enum PlayerError {
28 #[error("backend error: {0}")]
29 Backend(#[from] BackendError),
30 #[error("decode error: {0}")]
31 Decode(#[from] buffer::DecodeError),
32}
33
34pub struct Player {
36 shared_state: Arc<SharedPlayerState>,
37 commands: CommandChannel,
38 active_playback: Option<ActivePlayback>,
39 timeline: Arc<PlaybackTimeline>,
40 viz_buffer: Arc<VizBuffer>,
41 viz_snapshot: Arc<VizSnapshot>,
42 _viz_analyzer: VizAnalyzer,
44 undo_stack: UndoStack,
45 batch_buffer: Option<Vec<UndoEntry>>,
48 output_device_name: Option<String>,
50 backend: Box<dyn AudioBackend>,
52}
53
54struct ActivePlayback {
56 engine: Box<dyn AudioEngineHandle>,
57 decode_handle: buffer::DecodeHandle,
58}
59
60impl Default for Player {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl Player {
67 pub fn new() -> Self {
68 let viz_buffer = VizBuffer::new();
69 let viz_snapshot = VizSnapshot::new();
70 let cfg = crate::config::Config::load_or_default();
71 let viz_analyzer = VizAnalyzer::spawn_with_snapshot(
72 Arc::clone(&viz_buffer),
73 &cfg.visualizer,
74 Arc::clone(&viz_snapshot),
75 );
76
77 Self {
78 shared_state: SharedPlayerState::new(),
79 commands: CommandChannel::new(),
80 active_playback: None,
81 timeline: PlaybackTimeline::new(),
82 viz_buffer,
83 viz_snapshot,
84 _viz_analyzer: viz_analyzer,
85 undo_stack: UndoStack::new(),
86 batch_buffer: None,
87 output_device_name: cfg.playback.output_device.clone(),
88 backend: crate::audio::platform_backend(),
89 }
90 }
91
92 pub fn shared_state(&self) -> Arc<SharedPlayerState> {
94 self.shared_state.clone()
95 }
96
97 pub fn timeline(&self) -> Arc<PlaybackTimeline> {
99 self.timeline.clone()
100 }
101
102 pub fn viz_buffer(&self) -> Arc<VizBuffer> {
104 self.viz_buffer.clone()
105 }
106
107 pub fn viz_snapshot(&self) -> Arc<VizSnapshot> {
110 self.viz_snapshot.clone()
111 }
112
113 pub fn undo_stack(&self) -> &UndoStack {
115 &self.undo_stack
116 }
117
118 fn resolve_device(&self) -> Result<backend::DeviceInfo, PlayerError> {
121 if let Some(ref name) = self.output_device_name {
122 match self.backend.list_devices() {
123 Ok(devices) => {
124 if let Some(dev) = devices.into_iter().find(|d| d.name == *name) {
125 return Ok(dev);
126 }
127 log::warn!(
128 "configured output device '{}' not found, falling back to default",
129 name,
130 );
131 }
132 Err(e) => {
133 log::warn!("failed to list devices while resolving '{}': {}", name, e);
134 }
135 }
136 }
137 Ok(self.backend.default_device()?)
138 }
139
140 pub fn set_output_device(&mut self, name: String) {
143 log::info!("switching output device to: {}", name);
144 self.output_device_name = Some(name.clone());
145
146 if let Err(e) = crate::config::Config::update_base(|cfg| {
148 cfg.playback.output_device = Some(name);
149 }) {
150 log::error!("failed to save output device config: {}", e);
151 }
152
153 self.restart_on_current_track();
154 }
155
156 pub fn clear_output_device(&mut self) {
158 log::info!("reverting to system default output device");
159 self.output_device_name = None;
160
161 if let Err(e) = crate::config::Config::update_base(|cfg| {
162 cfg.playback.output_device = None;
163 }) {
164 log::error!("failed to save output device config: {}", e);
165 }
166
167 self.restart_on_current_track();
168 }
169
170 fn restart_on_current_track(&mut self) {
173 if let Some(info) = self.shared_state.track_info() {
174 let was_paused = self.shared_state.playback_state() == PlaybackState::Paused;
175 let position_ms = self.shared_state.position_ms();
176 if let Err(e) = self.start_playback(info.id, &info.path, position_ms) {
177 log::error!("failed to restart playback on device switch: {}", e);
178 return;
179 }
180 if was_paused {
181 self.pause();
182 }
183 }
184 }
185
186 pub fn output_device_name(&self) -> Option<&str> {
188 self.output_device_name.as_deref()
189 }
190
191 pub fn command_sender(&self) -> crossbeam_channel::Sender<PlayerCommand> {
193 self.commands.tx.clone()
194 }
195
196 pub fn play(&mut self, id: QueueItemId) {
199 self.shared_state.set_cursor(Some(id));
200
201 match self.shared_state.item_playback_source(id) {
202 Some(PlaybackSource::Ready(path)) => {
203 if let Err(e) = self.start_playback(id, &path, 0) {
204 log::error!("play failed: {}", e);
205 }
206 }
207 Some(PlaybackSource::Streaming {
208 path,
209 bytes_written,
210 total,
211 }) => {
212 if let Err(e) = self.start_streaming_playback(id, &path, bytes_written, total) {
213 log::error!("streaming play failed, waiting for full download: {}", e);
214 self.stop_engine();
216 self.shared_state.set_playback_state(PlaybackState::Stopped);
217 }
218 }
219 None => {
220 self.stop_engine();
222 self.shared_state.set_playback_state(PlaybackState::Stopped);
223 log::info!("play: item {:?} not ready, waiting for TrackReady", id);
224 }
225 }
226 }
227
228 fn start_playback(
230 &mut self,
231 id: QueueItemId,
232 path: &Path,
233 seek_ms: u64,
234 ) -> Result<(), PlayerError> {
235 self.stop_engine();
236
237 let info = buffer::probe_file(path)?;
238
239 self.shared_state.set_track_info(Some(TrackInfo {
243 id,
244 path: path.to_path_buf(),
245 codec: info.codec.clone(),
246 sample_rate: info.sample_rate,
247 bit_depth: info.bit_depth,
248 channels: info.channels,
249 duration_ms: info.duration_ms,
250 }));
251 self.shared_state.set_position_ms(seek_ms);
252 log::info!(
253 "playing: {} ({:?}) — {} {}Hz/{}bit/{}ch, {}ms{}",
254 path.display(),
255 id,
256 info.codec,
257 info.sample_rate,
258 info.bit_depth,
259 info.channels,
260 info.duration_ms,
261 if seek_ms > 0 {
262 format!(" @{}ms", seek_ms)
263 } else {
264 String::new()
265 }
266 );
267
268 let device = self.resolve_device()?;
269 let device_rate = self.backend.get_device_sample_rate(&device)?;
270 let source_rate = info.sample_rate as f64;
271
272 if (device_rate - source_rate).abs() > 0.1 {
273 log::info!(
274 "switching device sample rate: {}Hz → {}Hz",
275 device_rate,
276 source_rate
277 );
278 if let Err(e) = self.backend.set_device_sample_rate(&device, source_rate) {
279 log::warn!(
280 "failed to set sample rate (continuing at device rate): {}",
281 e
282 );
283 }
284 }
285
286 let (producer, consumer) = rtrb::RingBuffer::new(RING_BUFFER_SIZE);
287
288 let _generation = self.shared_state.bump_generation();
289
290 self.timeline.reset();
292
293 let advance_state = self.shared_state.clone();
297 let decode_cursor = std::sync::Mutex::new(Some(id));
298 let next_track = move || {
299 let current = decode_cursor.lock().ok()?.take()?;
300 let next = advance_state.peek_next_ready_after(current);
301 if let Some((next_id, _)) = &next
302 && let Ok(mut guard) = decode_cursor.lock()
303 {
304 *guard = Some(*next_id);
305 }
306 next
307 };
308
309 let cfg = crate::config::Config::load_or_default();
311 let rg_mode = cfg.playback.replaygain;
312 let pre_amp_db = cfg.playback.pre_amp_db;
313
314 let (_stream_info, decode_handle) = buffer::start_decode_file(
315 id,
316 path,
317 producer,
318 seek_ms,
319 next_track,
320 self.timeline.clone(),
321 Some(self.viz_buffer.clone()),
322 rg_mode,
323 pre_amp_db,
324 )?;
325
326 let actual_rate = self
328 .backend
329 .get_device_sample_rate(&device)
330 .unwrap_or(source_rate);
331 let engine = self.backend.create_engine(
332 &device,
333 actual_rate,
334 info.channels as u32,
335 consumer,
336 self.timeline.samples_played_counter(),
337 )?;
338 engine.start()?;
339
340 self.shared_state.set_playback_state(PlaybackState::Playing);
341
342 self.active_playback = Some(ActivePlayback {
343 engine,
344 decode_handle,
345 });
346
347 Ok(())
348 }
349
350 fn start_streaming_playback(
357 &mut self,
358 id: QueueItemId,
359 path: &Path,
360 bytes_written: Arc<AtomicU64>,
361 total: u64,
362 ) -> Result<(), PlayerError> {
363 self.stop_engine();
364
365 let stream_buf = streaming::StreamBuffer::new(if total > 0 { Some(total) } else { None });
367
368 let pump_path = path.to_path_buf();
372 let pump_buf = stream_buf.clone();
373 let pump_written = bytes_written.clone();
374 thread::Builder::new()
375 .name("koan-stream-pump".into())
376 .spawn(move || {
377 use std::fs::File;
378 use std::io::Read;
379 let mut file = match File::open(&pump_path) {
380 Ok(f) => f,
381 Err(e) => {
382 log::error!("stream pump: failed to open {}: {}", pump_path.display(), e);
383 pump_buf.finish();
384 return;
385 }
386 };
387 let mut buf = vec![0u8; 65536];
388 let mut offset: u64 = 0;
389 loop {
390 let available = pump_written.load(Ordering::Acquire);
391 if offset >= available {
392 if total > 0 && available >= total {
393 break; }
395 thread::sleep(std::time::Duration::from_millis(10));
396 continue;
397 }
398 let to_read = ((available - offset) as usize).min(buf.len());
399 match file.read(&mut buf[..to_read]) {
400 Ok(0) => {
401 if total > 0 && offset >= total {
404 break;
405 }
406 let latest = pump_written.load(Ordering::Acquire);
407 if total > 0 && latest >= total && offset >= latest {
408 break;
409 }
410 thread::sleep(std::time::Duration::from_millis(1));
412 continue;
413 }
414 Ok(n) => {
415 pump_buf.push(&buf[..n]);
416 offset += n as u64;
417 }
418 Err(e) => {
419 log::warn!("stream pump read error: {}", e);
420 break;
421 }
422 }
423 }
424 pump_buf.finish();
425 })
426 .map_err(|e| PlayerError::Decode(buffer::DecodeError::Io(e)))?;
427
428 let probe_reader = stream_buf.reader();
430 let probe_hint = {
431 let mut h = symphonia::core::probe::Hint::new();
432 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
433 h.with_extension(ext);
434 }
435 h
436 };
437 let probe_mss =
438 symphonia::core::io::MediaSourceStream::new(Box::new(probe_reader), Default::default());
439 let info = buffer::probe_source(probe_mss, &probe_hint)?;
440
441 self.shared_state.set_track_info(Some(TrackInfo {
442 id,
443 path: path.to_path_buf(),
444 codec: info.codec.clone(),
445 sample_rate: info.sample_rate,
446 bit_depth: info.bit_depth,
447 channels: info.channels,
448 duration_ms: info.duration_ms,
449 }));
450 self.shared_state.set_position_ms(0);
451 log::info!(
452 "streaming: {} ({:?}) — {} {}Hz/{}bit/{}ch, {}ms",
453 path.display(),
454 id,
455 info.codec,
456 info.sample_rate,
457 info.bit_depth,
458 info.channels,
459 info.duration_ms,
460 );
461
462 let device = self.resolve_device()?;
463 let device_rate = self.backend.get_device_sample_rate(&device)?;
464 let source_rate = info.sample_rate as f64;
465
466 if (device_rate - source_rate).abs() > 0.1 {
467 log::info!(
468 "switching device sample rate: {}Hz → {}Hz",
469 device_rate,
470 source_rate
471 );
472 if let Err(e) = self.backend.set_device_sample_rate(&device, source_rate) {
473 log::warn!(
474 "failed to set sample rate (continuing at device rate): {}",
475 e
476 );
477 }
478 }
479
480 let (producer, consumer) = rtrb::RingBuffer::new(RING_BUFFER_SIZE);
481
482 let _generation = self.shared_state.bump_generation();
483 self.timeline.reset();
484
485 let advance_state = self.shared_state.clone();
487 let decode_cursor = std::sync::Mutex::new(Some(id));
488 let next_track = move || {
489 let current = decode_cursor.lock().ok()?.take()?;
490 let next = advance_state.peek_next_ready_after(current);
491 if let Some((next_id, _)) = &next
492 && let Ok(mut guard) = decode_cursor.lock()
493 {
494 *guard = Some(*next_id);
495 }
496 next
497 };
498
499 let decode_reader = stream_buf.reader();
504 let path_buf = path.to_path_buf();
505 let ext = path
506 .extension()
507 .and_then(|e| e.to_str())
508 .unwrap_or("")
509 .to_string();
510 let mut decode_hint = symphonia::core::probe::Hint::new();
511 if !ext.is_empty() {
512 decode_hint.with_extension(&ext);
513 }
514 let first = buffer::SourceEntry {
515 id,
516 path: path_buf,
517 hint: decode_hint,
518 make_mss: Box::new(move || {
519 Ok(symphonia::core::io::MediaSourceStream::new(
520 Box::new(decode_reader),
521 Default::default(),
522 ))
523 }),
524 };
525
526 let cfg = crate::config::Config::load_or_default();
528 let rg_mode = cfg.playback.replaygain;
529 let pre_amp_db = cfg.playback.pre_amp_db;
530
531 let (_stream_info, decode_handle) = buffer::start_decode(
532 first,
533 producer,
534 0,
535 move || {
536 let (next_id, next_path) = next_track()?;
537 Some(buffer::SourceEntry::from_file(next_id, next_path))
538 },
539 self.timeline.clone(),
540 Some(self.viz_buffer.clone()),
541 rg_mode,
542 pre_amp_db,
543 )?;
544
545 let actual_rate = self
546 .backend
547 .get_device_sample_rate(&device)
548 .unwrap_or(source_rate);
549 let engine = self.backend.create_engine(
550 &device,
551 actual_rate,
552 info.channels as u32,
553 consumer,
554 self.timeline.samples_played_counter(),
555 )?;
556 engine.start()?;
557
558 self.shared_state.set_playback_state(PlaybackState::Playing);
559
560 self.active_playback = Some(ActivePlayback {
561 engine,
562 decode_handle,
563 });
564
565 Ok(())
566 }
567
568 pub fn seek(&mut self, position_ms: u64) {
571 let info = match self.shared_state.track_info() {
572 Some(info) => info,
573 None => return,
574 };
575 let id = info.id;
576 let path = info.path.clone();
577 let duration = info.duration_ms;
578
579 let mut clamped = if duration > 0 {
581 position_ms.min(duration.saturating_sub(500))
582 } else {
583 position_ms
584 };
585
586 if let Some(dl_frac) = self.shared_state.current_download_fraction() {
589 let max_ms = (dl_frac * duration as f64) as u64;
590 if max_ms > 5_000 {
591 clamped = clamped.min(max_ms - 5_000);
592 }
593 }
594
595 let was_paused = self.shared_state.playback_state() == PlaybackState::Paused;
596
597 if let Err(e) = self.start_playback(id, &path, clamped) {
598 log::error!("seek failed: {}", e);
599 return;
600 }
601
602 if was_paused {
603 self.pause();
604 }
605 }
606
607 pub fn next_track(&mut self) {
609 match self.shared_state.advance_cursor() {
611 Some((id, path)) => {
612 if let Err(e) = self.start_playback(id, &path, 0) {
613 log::error!("next track failed: {}", e);
614 }
615 }
616 None => {
617 log::info!("no more tracks in playlist");
618 self.stop_playback_and_clear_state();
619 }
620 }
621 }
622
623 pub fn prev_track(&mut self) {
625 match self.shared_state.retreat_cursor() {
626 Some((id, path)) => {
627 if matches!(path.try_exists(), Ok(true)) {
628 if let Err(e) = self.start_playback(id, &path, 0) {
629 log::error!("prev track failed: {}", e);
630 }
631 } else {
632 log::warn!("prev track path doesn't exist: {}", path.display());
633 }
634 }
635 None => {
636 if let Some(info) = self.shared_state.track_info()
638 && let Err(e) = self.start_playback(info.id, &info.path, 0)
639 {
640 log::error!("restart failed: {}", e);
641 }
642 }
643 }
644 }
645
646 pub fn pause(&mut self) {
648 if let Some(ref playback) = self.active_playback {
649 let _ = playback.engine.stop();
650 self.shared_state.set_playback_state(PlaybackState::Paused);
651 }
652 }
653
654 pub fn resume(&mut self) {
656 if let Some(ref playback) = self.active_playback {
657 let _ = playback.engine.start();
658 self.shared_state.set_playback_state(PlaybackState::Playing);
659 }
660 }
661
662 pub fn stop(&mut self) {
664 self.shared_state.clear_playlist();
665 self.stop_playback_and_clear_state();
666 }
667
668 fn stop_engine(&mut self) {
675 if let Some(playback) = self.active_playback.take() {
676 let _ = playback.engine.stop();
678 playback.decode_handle.signal_stop();
680
681 let ActivePlayback {
686 engine,
687 decode_handle,
688 } = playback;
689 drop(engine);
690
691 thread::Builder::new()
695 .name("koan-cleanup".into())
696 .spawn(move || drop(decode_handle))
697 .ok();
698 }
699 }
700
701 fn stop_playback_and_clear_state(&mut self) {
703 self.stop_engine();
704 self.timeline.reset();
705 self.shared_state.set_playback_state(PlaybackState::Stopped);
706 self.shared_state.set_position_ms(0);
707 self.shared_state.set_track_info(None);
708 }
709
710 pub fn remove_from_playlist(&mut self, id: QueueItemId) {
712 let was_cursor = self.shared_state.is_cursor(id);
713 self.shared_state.remove_item(id);
714 if was_cursor {
715 self.next_track();
716 }
717 }
718
719 pub fn track_ready(&mut self, id: QueueItemId) {
722 self.shared_state.update_load_state(id, LoadState::Ready);
724
725 if !self.shared_state.is_cursor(id) {
726 return;
727 }
728
729 let is_playing = self.shared_state.playback_state() == PlaybackState::Playing;
730 let current_track_id = self.shared_state.track_info().map(|t| t.id);
731
732 if is_playing && current_track_id == Some(id) {
733 log::info!(
736 "track_ready: download complete while streaming {:?}, refreshing metadata",
737 id
738 );
739 self.refresh_track_metadata(id);
740 return;
741 }
742
743 if !is_playing && let Some(path) = self.shared_state.item_path_if_ready(id) {
745 log::info!("track_ready: starting playback for {:?}", id);
746 if let Err(e) = self.start_playback(id, &path, 0) {
747 log::error!("track_ready playback failed: {}", e);
748 }
749 }
750 }
751
752 pub fn track_stream_ready(&mut self, id: QueueItemId) {
755 if !self.shared_state.is_cursor(id) {
756 return;
757 }
758
759 let is_playing = self.shared_state.playback_state() == PlaybackState::Playing;
760 if is_playing {
761 return; }
763
764 match self.shared_state.item_playback_source(id) {
765 Some(PlaybackSource::Streaming {
766 path,
767 bytes_written,
768 total,
769 }) => {
770 log::info!(
771 "track_stream_ready: starting streaming playback for {:?}",
772 id
773 );
774 if let Err(e) = self.start_streaming_playback(id, &path, bytes_written, total) {
775 log::error!("track_stream_ready streaming failed: {}", e);
776 }
777 }
778 Some(PlaybackSource::Ready(path)) => {
779 log::info!(
781 "track_stream_ready: track already ready, starting normal playback for {:?}",
782 id
783 );
784 if let Err(e) = self.start_playback(id, &path, 0) {
785 log::error!("track_stream_ready playback failed: {}", e);
786 }
787 }
788 None => {} }
790 }
791
792 fn refresh_track_metadata(&mut self, id: QueueItemId) {
796 use crate::index::metadata;
797
798 let path = match self.shared_state.item_path_if_ready(id) {
799 Some(p) => p,
800 None => return,
801 };
802
803 match metadata::read_metadata(&path) {
804 Ok(meta) => {
805 self.shared_state.update_item_metadata(
807 id,
808 meta.title,
809 meta.artist,
810 meta.album_artist.unwrap_or_default(),
811 meta.album,
812 meta.duration_ms.map(|d| d as u64),
813 );
814
815 if let Ok(stream_info) = buffer::probe_file(&path)
820 && let Some(current) = self.shared_state.track_info()
821 && current.id == id
822 && stream_info.duration_ms > current.duration_ms
823 {
824 log::info!(
825 "track_ready: duration corrected {}ms → {}ms",
826 current.duration_ms,
827 stream_info.duration_ms
828 );
829 self.shared_state.set_track_info(Some(TrackInfo {
830 duration_ms: stream_info.duration_ms,
831 ..current
832 }));
833 }
834
835 self.shared_state.signal_metadata_refresh();
837 log::info!("track_ready: metadata refreshed for {:?}", id);
838 }
839 Err(e) => {
840 log::warn!("track_ready: metadata refresh failed for {:?}: {}", id, e);
841 }
842 }
843 }
844
845 pub fn update_playback_state(&self) {
848 if self.active_playback.is_none() {
849 return;
850 }
851
852 if let Some((id, path, info, position_ms)) = self.timeline.current_playback() {
853 self.shared_state.set_position_ms(position_ms);
854
855 let current_id = self.shared_state.track_info().map(|t| t.id);
858 if current_id != Some(id) {
859 log::info!("timeline: now playing {:?}", id);
860 self.shared_state.set_track_info(Some(TrackInfo {
861 id,
862 path,
863 codec: info.codec,
864 sample_rate: info.sample_rate,
865 bit_depth: info.bit_depth,
866 channels: info.channels,
867 duration_ms: info.duration_ms,
868 }));
869 self.shared_state.set_cursor(Some(id));
870 }
871 }
872 }
873
874 fn push_undo(&mut self, entry: UndoEntry) {
876 if let Some(ref mut batch) = self.batch_buffer {
877 batch.push(entry);
878 } else {
879 self.undo_stack.push(entry);
880 }
881 }
882
883 pub fn process_command(&mut self, cmd: PlayerCommand) {
885 match cmd {
886 PlayerCommand::Play(id) => self.play(id),
887 PlayerCommand::Pause => self.pause(),
888 PlayerCommand::Resume => self.resume(),
889 PlayerCommand::Stop => self.stop(),
890 PlayerCommand::Seek(pos) => self.seek(pos),
891 PlayerCommand::NextTrack => self.next_track(),
892 PlayerCommand::PrevTrack => self.prev_track(),
893 PlayerCommand::AddToPlaylist(items) => {
894 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
895 self.shared_state.add_items(items);
896 self.push_undo(UndoEntry::Added { ids });
897 }
898 PlayerCommand::UpdatePaths(updates) => {
899 self.shared_state.update_paths(&updates);
900 if let Some(info) = self.shared_state.track_info()
901 && let Some((_, new_path)) = updates.iter().find(|(id, _)| *id == info.id)
902 {
903 self.shared_state.set_track_info(Some(TrackInfo {
904 path: new_path.clone(),
905 ..info
906 }));
907 }
908 }
909 PlayerCommand::InsertInPlaylist { items, after } => {
910 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
911 self.shared_state.insert_items_after(items, after);
912 self.push_undo(UndoEntry::Inserted { ids });
913 }
914 PlayerCommand::ClearPlaylist => {
915 self.stop_playback_and_clear_state();
919 let (items, cursor) = self.shared_state.snapshot_playlist();
920 self.shared_state.clear_playlist();
921 self.push_undo(UndoEntry::Replaced { items, cursor });
922 }
923 PlayerCommand::RemoveFromPlaylist(id) => {
924 let item = self.shared_state.get_item(id);
925 let after = self.shared_state.item_before(id);
926 self.remove_from_playlist(id);
927 if let Some(item) = item {
928 self.push_undo(UndoEntry::Removed {
929 items: vec![(Box::new(item), after)],
930 });
931 }
932 }
933 PlayerCommand::RemoveFromPlaylistBatch(ids) => {
934 let items_with_pos: Vec<_> = ids
936 .iter()
937 .filter_map(|&id| {
938 let item = self.shared_state.get_item(id)?;
939 let after = self.shared_state.item_before(id);
940 Some((Box::new(item), after))
941 })
942 .collect();
943 for &id in &ids {
944 self.remove_from_playlist(id);
945 }
946 if !items_with_pos.is_empty() {
947 self.push_undo(UndoEntry::Removed {
948 items: items_with_pos,
949 });
950 }
951 }
952 PlayerCommand::MoveInPlaylist { id, target, after } => {
953 let was_after = self.shared_state.item_before(id);
954 self.shared_state.move_item(id, target, after);
955 self.push_undo(UndoEntry::Moved { id, was_after });
956 }
957 PlayerCommand::MoveItemsInPlaylist { ids, target, after } => {
958 let entries = self.shared_state.items_before(&ids);
959 self.shared_state.move_items(&ids, target, after);
960 self.push_undo(UndoEntry::MovedBatch { entries });
961 }
962 PlayerCommand::TrackReady(id) => self.track_ready(id),
963 PlayerCommand::TrackStreamReady(id) => self.track_stream_ready(id),
964 PlayerCommand::Undo => self.execute_undo(),
965 PlayerCommand::Redo => self.execute_redo(),
966 PlayerCommand::BeginUndoBatch => {
967 self.batch_buffer = Some(Vec::new());
968 }
969 PlayerCommand::EndUndoBatch => {
970 if let Some(entries) = self.batch_buffer.take() {
971 if entries.len() == 1 {
972 self.undo_stack.push(entries.into_iter().next().unwrap());
974 } else if !entries.is_empty() {
975 self.undo_stack.push(UndoEntry::Batch(entries));
976 }
977 }
978 }
979 PlayerCommand::SetOutputDevice(name) => self.set_output_device(name),
980 PlayerCommand::ClearOutputDevice => self.clear_output_device(),
981 }
982 }
983
984 fn apply_entry(&mut self, entry: UndoEntry) -> Option<UndoEntry> {
986 match entry {
987 UndoEntry::Added { ids } => {
988 let items_with_pos: Vec<_> = ids
990 .iter()
991 .filter_map(|&id| {
992 let item = self.shared_state.get_item(id)?;
993 let after = self.shared_state.item_before(id);
994 Some((Box::new(item), after))
995 })
996 .collect();
997 self.shared_state.remove_items(&ids);
998 Some(UndoEntry::Removed {
999 items: items_with_pos,
1000 })
1001 }
1002 UndoEntry::Removed { items } => {
1003 let mut ids = Vec::with_capacity(items.len());
1005 for (item, after) in items {
1006 ids.push(item.id);
1007 self.shared_state.insert_item_at(*item, after);
1008 }
1009 Some(UndoEntry::Added { ids })
1010 }
1011 UndoEntry::Inserted { ids } => {
1012 let items_with_pos: Vec<_> = ids
1014 .iter()
1015 .filter_map(|&id| {
1016 let item = self.shared_state.get_item(id)?;
1017 let after = self.shared_state.item_before(id);
1018 Some((Box::new(item), after))
1019 })
1020 .collect();
1021 self.shared_state.remove_items(&ids);
1022 Some(UndoEntry::Removed {
1023 items: items_with_pos,
1024 })
1025 }
1026 UndoEntry::Moved { id, was_after } => {
1027 let current_after = self.shared_state.item_before(id);
1028 self.shared_state.move_item_to(id, was_after);
1029 Some(UndoEntry::Moved {
1030 id,
1031 was_after: current_after,
1032 })
1033 }
1034 UndoEntry::MovedBatch { entries } => {
1035 let ids: Vec<QueueItemId> = entries.iter().map(|(id, _)| *id).collect();
1036 let current_positions = self.shared_state.items_before(&ids);
1037 self.shared_state.move_items_to(&entries);
1038 Some(UndoEntry::MovedBatch {
1039 entries: current_positions,
1040 })
1041 }
1042 UndoEntry::Replaced { items, cursor } => {
1043 let (current_items, current_cursor) = self.shared_state.snapshot_playlist();
1044 self.shared_state.restore_playlist(items, cursor);
1045 Some(UndoEntry::Replaced {
1046 items: current_items,
1047 cursor: current_cursor,
1048 })
1049 }
1050 UndoEntry::Batch(entries) => {
1051 let mut inverses = Vec::with_capacity(entries.len());
1053 for entry in entries.into_iter().rev() {
1054 if let Some(inverse) = self.apply_entry(entry) {
1055 inverses.push(inverse);
1056 }
1057 }
1058 inverses.reverse();
1059 Some(UndoEntry::Batch(inverses))
1060 }
1061 }
1062 }
1063
1064 fn execute_undo(&mut self) {
1066 let Some(entry) = self.undo_stack.pop_undo() else {
1067 return;
1068 };
1069 if let Some(inverse) = self.apply_entry(entry) {
1070 self.undo_stack.push_redo(inverse);
1071 }
1072 }
1073
1074 fn execute_redo(&mut self) {
1076 let Some(entry) = self.undo_stack.pop_redo() else {
1077 return;
1078 };
1079 if let Some(inverse) = self.apply_entry(entry) {
1080 self.undo_stack.push_undo_keep_redo(inverse);
1081 }
1082 }
1083
1084 pub fn run(&mut self) {
1086 use std::time::Duration;
1087
1088 let rx = self.commands.rx.clone();
1089 loop {
1090 match rx.recv_timeout(Duration::from_millis(50)) {
1092 Ok(cmd) => self.process_command(cmd),
1093 Err(crossbeam_channel::RecvTimeoutError::Timeout) => {}
1094 Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
1095 }
1096 self.update_playback_state();
1097 }
1098 self.stop();
1099 }
1100
1101 pub fn spawn() -> (
1104 Arc<SharedPlayerState>,
1105 Arc<PlaybackTimeline>,
1106 Arc<VizSnapshot>,
1107 crossbeam_channel::Sender<PlayerCommand>,
1108 ) {
1109 let mut player = Self::new();
1110 let state = player.shared_state();
1111 let timeline = player.timeline();
1112 let viz_snapshot = player.viz_snapshot();
1113 let tx = player.command_sender();
1114
1115 thread::Builder::new()
1116 .name("koan-player".into())
1117 .spawn(move || player.run())
1118 .expect("failed to spawn player thread");
1119
1120 (state, timeline, viz_snapshot, tx)
1121 }
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126 use super::*;
1127 use state::PlaylistItem;
1128 use std::path::PathBuf;
1129
1130 fn make_item(title: &str) -> PlaylistItem {
1131 PlaylistItem {
1132 id: QueueItemId::new(),
1133 db_id: None,
1134 path: PathBuf::from(format!("/music/{title}.flac")),
1135 title: title.to_string(),
1136 artist: String::new(),
1137 album_artist: String::new(),
1138 album: String::new(),
1139 year: None,
1140 codec: None,
1141 track_number: None,
1142 disc: None,
1143 duration_ms: None,
1144 load_state: LoadState::Ready,
1145 }
1146 }
1147
1148 fn playlist_ids(player: &Player) -> Vec<QueueItemId> {
1149 let (items, _) = player.shared_state.snapshot_playlist();
1150 items.iter().map(|i| i.id).collect()
1151 }
1152
1153 fn playlist_titles(player: &Player) -> Vec<String> {
1154 let (items, _) = player.shared_state.snapshot_playlist();
1155 items.iter().map(|i| i.title.clone()).collect()
1156 }
1157
1158 #[test]
1161 fn undo_add_removes_items() {
1162 let mut player = Player::new();
1163 let items = vec![make_item("A"), make_item("B")];
1164 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
1165
1166 player.process_command(PlayerCommand::AddToPlaylist(items));
1167 assert_eq!(playlist_ids(&player), ids);
1168 assert!(player.undo_stack().can_undo());
1169
1170 player.process_command(PlayerCommand::Undo);
1171 assert!(playlist_ids(&player).is_empty());
1172 assert!(player.undo_stack().can_redo());
1173 }
1174
1175 #[test]
1176 fn redo_add_restores_items() {
1177 let mut player = Player::new();
1178 let items = vec![make_item("A"), make_item("B")];
1179
1180 player.process_command(PlayerCommand::AddToPlaylist(items));
1181 player.process_command(PlayerCommand::Undo);
1182 assert!(playlist_ids(&player).is_empty());
1183
1184 player.process_command(PlayerCommand::Redo);
1185 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1186 }
1187
1188 #[test]
1191 fn undo_remove_restores_item_at_position() {
1192 let mut player = Player::new();
1193 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1194 let b_id = items[1].id;
1195
1196 player.process_command(PlayerCommand::AddToPlaylist(items));
1197 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1198 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1199
1200 player.process_command(PlayerCommand::Undo);
1201 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1202 }
1203
1204 #[test]
1205 fn undo_remove_first_item() {
1206 let mut player = Player::new();
1207 let items = vec![make_item("A"), make_item("B")];
1208 let a_id = items[0].id;
1209
1210 player.process_command(PlayerCommand::AddToPlaylist(items));
1211 player.process_command(PlayerCommand::RemoveFromPlaylist(a_id));
1212 assert_eq!(playlist_titles(&player), vec!["B"]);
1213
1214 player.process_command(PlayerCommand::Undo);
1215 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1216 }
1217
1218 #[test]
1219 fn undo_batch_remove_restores_all() {
1220 let mut player = Player::new();
1221 let items = vec![
1222 make_item("A"),
1223 make_item("B"),
1224 make_item("C"),
1225 make_item("D"),
1226 ];
1227 let b_id = items[1].id;
1228 let c_id = items[2].id;
1229
1230 player.process_command(PlayerCommand::AddToPlaylist(items));
1231 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![b_id, c_id]));
1232 assert_eq!(playlist_titles(&player), vec!["A", "D"]);
1233
1234 player.process_command(PlayerCommand::Undo);
1236 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
1237 }
1238
1239 #[test]
1240 fn redo_batch_remove() {
1241 let mut player = Player::new();
1242 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1243 let a_id = items[0].id;
1244 let b_id = items[1].id;
1245
1246 player.process_command(PlayerCommand::AddToPlaylist(items));
1247 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![a_id, b_id]));
1248 player.process_command(PlayerCommand::Undo);
1249 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1250
1251 player.process_command(PlayerCommand::Redo);
1252 assert_eq!(playlist_titles(&player), vec!["C"]);
1253 }
1254
1255 #[test]
1256 fn redo_remove() {
1257 let mut player = Player::new();
1258 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1259 let b_id = items[1].id;
1260
1261 player.process_command(PlayerCommand::AddToPlaylist(items));
1262 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1263 player.process_command(PlayerCommand::Undo);
1264 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1265
1266 player.process_command(PlayerCommand::Redo);
1267 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1268 }
1269
1270 #[test]
1273 fn undo_insert_removes_inserted_items() {
1274 let mut player = Player::new();
1275 let items = vec![make_item("A"), make_item("C")];
1276 let a_id = items[0].id;
1277
1278 player.process_command(PlayerCommand::AddToPlaylist(items));
1279
1280 let inserted = vec![make_item("B")];
1281 player.process_command(PlayerCommand::InsertInPlaylist {
1282 items: inserted,
1283 after: a_id,
1284 });
1285 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1286
1287 player.process_command(PlayerCommand::Undo);
1288 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1289 }
1290
1291 #[test]
1294 fn undo_move_restores_position() {
1295 let mut player = Player::new();
1296 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1297 let a_id = items[0].id;
1298 let c_id = items[2].id;
1299
1300 player.process_command(PlayerCommand::AddToPlaylist(items));
1301
1302 player.process_command(PlayerCommand::MoveInPlaylist {
1304 id: a_id,
1305 target: c_id,
1306 after: true,
1307 });
1308 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1309
1310 player.process_command(PlayerCommand::Undo);
1311 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1312 }
1313
1314 #[test]
1315 fn redo_move() {
1316 let mut player = Player::new();
1317 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1318 let a_id = items[0].id;
1319 let c_id = items[2].id;
1320
1321 player.process_command(PlayerCommand::AddToPlaylist(items));
1322 player.process_command(PlayerCommand::MoveInPlaylist {
1323 id: a_id,
1324 target: c_id,
1325 after: true,
1326 });
1327 player.process_command(PlayerCommand::Undo);
1328 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1329
1330 player.process_command(PlayerCommand::Redo);
1331 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1332 }
1333
1334 #[test]
1337 fn undo_batch_move() {
1338 let mut player = Player::new();
1339 let items = vec![
1340 make_item("A"),
1341 make_item("B"),
1342 make_item("C"),
1343 make_item("D"),
1344 ];
1345 let a_id = items[0].id;
1346 let b_id = items[1].id;
1347 let d_id = items[3].id;
1348
1349 player.process_command(PlayerCommand::AddToPlaylist(items));
1350
1351 player.process_command(PlayerCommand::MoveItemsInPlaylist {
1353 ids: vec![a_id, b_id],
1354 target: d_id,
1355 after: true,
1356 });
1357 assert_eq!(playlist_titles(&player), vec!["C", "D", "A", "B"]);
1358
1359 player.process_command(PlayerCommand::Undo);
1360 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
1361 }
1362
1363 #[test]
1366 fn undo_clear_restores_playlist() {
1367 let mut player = Player::new();
1368 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1369
1370 player.process_command(PlayerCommand::AddToPlaylist(items));
1371 player.process_command(PlayerCommand::ClearPlaylist);
1372 assert!(playlist_ids(&player).is_empty());
1373
1374 player.process_command(PlayerCommand::Undo);
1375 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1376 }
1377
1378 #[test]
1379 fn redo_clear() {
1380 let mut player = Player::new();
1381 let items = vec![make_item("A"), make_item("B")];
1382
1383 player.process_command(PlayerCommand::AddToPlaylist(items));
1384 player.process_command(PlayerCommand::ClearPlaylist);
1385 player.process_command(PlayerCommand::Undo);
1386 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1387
1388 player.process_command(PlayerCommand::Redo);
1389 assert!(playlist_ids(&player).is_empty());
1390 }
1391
1392 #[test]
1395 fn multiple_undos_in_sequence() {
1396 let mut player = Player::new();
1397
1398 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("A")]));
1399 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
1400 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("C")]));
1401 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1402
1403 player.process_command(PlayerCommand::Undo);
1404 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1405
1406 player.process_command(PlayerCommand::Undo);
1407 assert_eq!(playlist_titles(&player), vec!["A"]);
1408
1409 player.process_command(PlayerCommand::Undo);
1410 assert!(playlist_ids(&player).is_empty());
1411 }
1412
1413 #[test]
1414 fn undo_redo_undo_cycle() {
1415 let mut player = Player::new();
1416 let items = vec![make_item("A"), make_item("B")];
1417
1418 player.process_command(PlayerCommand::AddToPlaylist(items));
1419 player.process_command(PlayerCommand::Undo);
1420 assert!(playlist_ids(&player).is_empty());
1421
1422 player.process_command(PlayerCommand::Redo);
1423 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1424
1425 player.process_command(PlayerCommand::Undo);
1426 assert!(playlist_ids(&player).is_empty());
1427 }
1428
1429 #[test]
1430 fn new_action_clears_redo_stack() {
1431 let mut player = Player::new();
1432 let items = vec![make_item("A")];
1433
1434 player.process_command(PlayerCommand::AddToPlaylist(items));
1435 player.process_command(PlayerCommand::Undo);
1436 assert!(player.undo_stack().can_redo());
1437
1438 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
1440 assert!(!player.undo_stack().can_redo());
1441 }
1442
1443 #[test]
1444 fn undo_on_empty_stack_is_noop() {
1445 let mut player = Player::new();
1446 player.process_command(PlayerCommand::Undo);
1447 assert!(playlist_ids(&player).is_empty());
1448 }
1449
1450 #[test]
1451 fn redo_on_empty_stack_is_noop() {
1452 let mut player = Player::new();
1453 player.process_command(PlayerCommand::Redo);
1454 assert!(playlist_ids(&player).is_empty());
1455 }
1456
1457 #[test]
1460 fn playback_commands_not_undoable() {
1461 let mut player = Player::new();
1462 player.process_command(PlayerCommand::Pause);
1463 player.process_command(PlayerCommand::Resume);
1464 player.process_command(PlayerCommand::NextTrack);
1465 player.process_command(PlayerCommand::PrevTrack);
1466 assert!(!player.undo_stack().can_undo());
1467 }
1468
1469 #[test]
1470 fn update_paths_not_undoable() {
1471 let mut player = Player::new();
1472 let items = vec![make_item("A")];
1473 let id = items[0].id;
1474 player.process_command(PlayerCommand::AddToPlaylist(items));
1475
1476 let undo_count = player.undo_stack().undo_len();
1477 player.process_command(PlayerCommand::UpdatePaths(vec![(
1478 id,
1479 PathBuf::from("/new/path.flac"),
1480 )]));
1481 assert_eq!(player.undo_stack().undo_len(), undo_count);
1482 }
1483
1484 #[test]
1487 fn add_remove_undo_undo_produces_original() {
1488 let mut player = Player::new();
1489 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1490 let b_id = items[1].id;
1491 let original_titles = vec!["A", "B", "C"];
1492
1493 player.process_command(PlayerCommand::AddToPlaylist(items));
1494 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1495 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1496
1497 player.process_command(PlayerCommand::Undo);
1499 assert_eq!(playlist_titles(&player), original_titles);
1500
1501 player.process_command(PlayerCommand::Undo);
1503 assert!(playlist_ids(&player).is_empty());
1504 }
1505
1506 #[test]
1507 fn interleaved_adds_and_moves_undo() {
1508 let mut player = Player::new();
1509 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1510 let a_id = items[0].id;
1511 let c_id = items[2].id;
1512
1513 player.process_command(PlayerCommand::AddToPlaylist(items));
1514
1515 player.process_command(PlayerCommand::MoveInPlaylist {
1517 id: a_id,
1518 target: c_id,
1519 after: true,
1520 });
1521 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1522
1523 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("D")]));
1525 assert_eq!(playlist_titles(&player), vec!["B", "C", "A", "D"]);
1526
1527 player.process_command(PlayerCommand::Undo);
1529 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1530
1531 player.process_command(PlayerCommand::Undo);
1533 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1534 }
1535
1536 #[test]
1541 fn stop_engine_drops_engine_synchronously() {
1542 use std::sync::atomic::AtomicBool;
1543
1544 struct MockEngine {
1545 dropped: Arc<AtomicBool>,
1546 }
1547 impl AudioEngineHandle for MockEngine {
1548 fn start(&self) -> Result<(), BackendError> {
1549 Ok(())
1550 }
1551 fn stop(&self) -> Result<(), BackendError> {
1552 Ok(())
1553 }
1554 fn is_running(&self) -> bool {
1555 false
1556 }
1557 }
1558 impl Drop for MockEngine {
1559 fn drop(&mut self) {
1560 self.dropped.store(true, Ordering::SeqCst);
1561 }
1562 }
1563
1564 let dropped = Arc::new(AtomicBool::new(false));
1565
1566 let stop_flag = Arc::new(AtomicBool::new(false));
1568 let decode_handle = buffer::DecodeHandle::new_for_test(stop_flag);
1569
1570 let mut player = Player::new();
1571 player.active_playback = Some(ActivePlayback {
1572 engine: Box::new(MockEngine {
1573 dropped: dropped.clone(),
1574 }),
1575 decode_handle,
1576 });
1577
1578 player.stop_engine();
1579
1580 assert!(
1584 dropped.load(Ordering::SeqCst),
1585 "AudioEngine must be dropped synchronously in stop_engine (GitHub #89)"
1586 );
1587 }
1588}