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