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/{}bit/{}ch, {}ms{}",
257 path.display(),
258 id,
259 info.codec,
260 info.sample_rate,
261 info.bit_depth,
262 info.channels,
263 info.duration_ms,
264 if seek_ms > 0 {
265 format!(" @{}ms", seek_ms)
266 } else {
267 String::new()
268 }
269 );
270
271 let device = self.resolve_device()?;
272 let device_rate = self.backend.get_device_sample_rate(&device)?;
273 let source_rate = info.sample_rate as f64;
274
275 let actual_rate = if (device_rate - source_rate).abs() > 0.1 {
276 log::info!(
277 "switching device sample rate: {}Hz → {}Hz",
278 device_rate,
279 source_rate
280 );
281 match self.backend.set_device_sample_rate(&device, source_rate) {
282 Ok(confirmed_rate) => {
283 if (confirmed_rate - source_rate).abs() > 0.1 {
284 log::warn!(
285 "device stayed at {}Hz (wanted {}Hz) — playback will use device rate",
286 confirmed_rate,
287 source_rate
288 );
289 }
290 confirmed_rate
291 }
292 Err(e) => {
293 log::warn!(
294 "failed to set sample rate (continuing at device rate): {}",
295 e
296 );
297 device_rate
298 }
299 }
300 } else {
301 device_rate
302 };
303
304 let (producer, consumer) = rtrb::RingBuffer::new(RING_BUFFER_SIZE);
305
306 let _generation = self.shared_state.bump_generation();
307
308 self.timeline.reset();
310
311 let advance_state = self.shared_state.clone();
315 let decode_cursor = std::sync::Mutex::new(Some(id));
316 let next_track = move || {
317 let current = decode_cursor.lock().ok()?.take()?;
318 let next = advance_state.peek_next_ready_after(current);
319 if let Some((next_id, _)) = &next
320 && let Ok(mut guard) = decode_cursor.lock()
321 {
322 *guard = Some(*next_id);
323 }
324 next
325 };
326
327 let cfg = crate::config::Config::load_or_default();
329 let rg_mode = cfg.playback.replaygain;
330 let pre_amp_db = cfg.playback.pre_amp_db;
331
332 let finish_tx = self.commands.tx.clone();
333 let (_stream_info, decode_handle) = buffer::start_decode_file(
334 id,
335 path,
336 producer,
337 seek_ms,
338 next_track,
339 self.timeline.clone(),
340 Some(self.viz_buffer.clone()),
341 rg_mode,
342 pre_amp_db,
343 move || {
344 finish_tx.send(PlayerCommand::DecodeFinished).ok();
345 },
346 )?;
347
348 let engine = self.backend.create_engine(
350 &device,
351 actual_rate,
352 info.channels as u32,
353 consumer,
354 self.timeline.samples_played_counter(),
355 )?;
356 engine.start()?;
357
358 self.shared_state.set_playback_state(PlaybackState::Playing);
359
360 self.active_playback = Some(ActivePlayback {
361 engine,
362 decode_handle,
363 });
364
365 Ok(())
366 }
367
368 fn start_streaming_playback(
375 &mut self,
376 id: QueueItemId,
377 path: &Path,
378 bytes_written: Arc<AtomicU64>,
379 total: u64,
380 ) -> Result<(), PlayerError> {
381 self.stop_engine();
382
383 let stream_buf = streaming::StreamBuffer::new(if total > 0 { Some(total) } else { None });
385
386 let pump_path = path.to_path_buf();
394 let pump_buf = stream_buf.clone();
395 let pump_written = bytes_written.clone();
396 thread::Builder::new()
397 .name("koan-stream-pump".into())
398 .spawn(move || {
399 use std::fs::File;
400 use std::io::Read;
401 let mut file = match File::open(&pump_path) {
402 Ok(f) => f,
403 Err(e) => {
404 log::error!("stream pump: failed to open {}: {}", pump_path.display(), e);
405 pump_buf.finish();
406 return;
407 }
408 };
409 let mut buf = vec![0u8; 65536];
410 let mut offset: u64 = 0;
411 loop {
412 let available = pump_written.load(Ordering::Acquire);
413 if offset >= available {
414 if total > 0 && available >= total {
415 break; }
417 thread::sleep(std::time::Duration::from_millis(10));
418 continue;
419 }
420 let to_read = ((available - offset) as usize).min(buf.len());
421 match file.read(&mut buf[..to_read]) {
422 Ok(0) => {
423 if total > 0 && offset >= total {
426 break;
427 }
428 let latest = pump_written.load(Ordering::Acquire);
429 if total > 0 && latest >= total && offset >= latest {
430 break;
431 }
432 thread::sleep(std::time::Duration::from_millis(1));
434 continue;
435 }
436 Ok(n) => {
437 pump_buf.push(&buf[..n]);
438 offset += n as u64;
439 }
440 Err(e) => {
441 log::warn!("stream pump read error: {}", e);
442 break;
443 }
444 }
445 }
446 pump_buf.finish();
447 })
448 .map_err(|e| PlayerError::Decode(buffer::DecodeError::Io(e)))?;
449
450 let probe_reader = stream_buf.reader();
452 let probe_hint = {
453 let mut h = symphonia::core::probe::Hint::new();
454 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
455 h.with_extension(ext);
456 }
457 h
458 };
459 let probe_mss =
460 symphonia::core::io::MediaSourceStream::new(Box::new(probe_reader), Default::default());
461 let info = buffer::probe_source(probe_mss, &probe_hint)?;
462
463 self.shared_state.set_track_info(Some(TrackInfo {
464 id,
465 path: path.to_path_buf(),
466 codec: info.codec.clone(),
467 sample_rate: info.sample_rate,
468 bit_depth: info.bit_depth,
469 channels: info.channels,
470 duration_ms: info.duration_ms,
471 }));
472 self.shared_state.set_position_ms(0);
473 log::info!(
474 "streaming: {} ({:?}) — {} {}Hz/{}bit/{}ch, {}ms",
475 path.display(),
476 id,
477 info.codec,
478 info.sample_rate,
479 info.bit_depth,
480 info.channels,
481 info.duration_ms,
482 );
483
484 let device = self.resolve_device()?;
485 let device_rate = self.backend.get_device_sample_rate(&device)?;
486 let source_rate = info.sample_rate as f64;
487
488 let actual_rate = if (device_rate - source_rate).abs() > 0.1 {
489 log::info!(
490 "switching device sample rate: {}Hz → {}Hz",
491 device_rate,
492 source_rate
493 );
494 match self.backend.set_device_sample_rate(&device, source_rate) {
495 Ok(confirmed_rate) => {
496 if (confirmed_rate - source_rate).abs() > 0.1 {
497 log::warn!(
498 "device stayed at {}Hz (wanted {}Hz) — playback will use device rate",
499 confirmed_rate,
500 source_rate
501 );
502 }
503 confirmed_rate
504 }
505 Err(e) => {
506 log::warn!(
507 "failed to set sample rate (continuing at device rate): {}",
508 e
509 );
510 device_rate
511 }
512 }
513 } else {
514 device_rate
515 };
516
517 let (producer, consumer) = rtrb::RingBuffer::new(RING_BUFFER_SIZE);
518
519 let _generation = self.shared_state.bump_generation();
520 self.timeline.reset();
521
522 let advance_state = self.shared_state.clone();
524 let decode_cursor = std::sync::Mutex::new(Some(id));
525 let next_track = move || {
526 let current = decode_cursor.lock().ok()?.take()?;
527 let next = advance_state.peek_next_ready_after(current);
528 if let Some((next_id, _)) = &next
529 && let Ok(mut guard) = decode_cursor.lock()
530 {
531 *guard = Some(*next_id);
532 }
533 next
534 };
535
536 let decode_reader = stream_buf.reader();
541 let path_buf = path.to_path_buf();
542 let ext = path
543 .extension()
544 .and_then(|e| e.to_str())
545 .unwrap_or("")
546 .to_string();
547 let mut decode_hint = symphonia::core::probe::Hint::new();
548 if !ext.is_empty() {
549 decode_hint.with_extension(&ext);
550 }
551 let first = buffer::SourceEntry {
552 id,
553 path: path_buf,
554 hint: decode_hint,
555 make_mss: Box::new(move || {
556 Ok(symphonia::core::io::MediaSourceStream::new(
557 Box::new(decode_reader),
558 Default::default(),
559 ))
560 }),
561 };
562
563 let cfg = crate::config::Config::load_or_default();
565 let rg_mode = cfg.playback.replaygain;
566 let pre_amp_db = cfg.playback.pre_amp_db;
567
568 let finish_tx = self.commands.tx.clone();
569 let (_stream_info, decode_handle) = buffer::start_decode(
570 first,
571 producer,
572 0,
573 move || {
574 let (next_id, next_path) = next_track()?;
575 Some(buffer::SourceEntry::from_file(next_id, next_path))
576 },
577 self.timeline.clone(),
578 Some(self.viz_buffer.clone()),
579 rg_mode,
580 pre_amp_db,
581 move || {
582 finish_tx.send(PlayerCommand::DecodeFinished).ok();
583 },
584 )?;
585
586 let engine = self.backend.create_engine(
588 &device,
589 actual_rate,
590 info.channels as u32,
591 consumer,
592 self.timeline.samples_played_counter(),
593 )?;
594 engine.start()?;
595
596 self.shared_state.set_playback_state(PlaybackState::Playing);
597
598 self.active_playback = Some(ActivePlayback {
599 engine,
600 decode_handle,
601 });
602
603 Ok(())
604 }
605
606 pub fn seek(&mut self, position_ms: u64) {
609 let info = match self.shared_state.track_info() {
610 Some(info) => info,
611 None => return,
612 };
613 let id = info.id;
614 let path = info.path.clone();
615 let duration = info.duration_ms;
616
617 let mut clamped = if duration > 0 {
619 position_ms.min(duration.saturating_sub(500))
620 } else {
621 position_ms
622 };
623
624 if let Some(dl_frac) = self.shared_state.current_download_fraction() {
627 let max_ms = (dl_frac * duration as f64) as u64;
628 if max_ms > 5_000 {
629 clamped = clamped.min(max_ms - 5_000);
630 }
631 }
632
633 let was_paused = self.shared_state.playback_state() == PlaybackState::Paused;
634
635 if let Err(e) = self.start_playback(id, &path, clamped) {
636 log::error!("seek failed: {}", e);
637 return;
638 }
639
640 if was_paused {
641 self.pause();
642 }
643 }
644
645 pub fn next_track(&mut self) {
647 match self.shared_state.advance_cursor() {
649 Some((id, path)) => {
650 if let Err(e) = self.start_playback(id, &path, 0) {
651 log::error!("next track failed: {}", e);
652 }
653 }
654 None => {
655 log::info!("no more tracks in playlist");
656 self.stop_playback_and_clear_state();
657 }
658 }
659 }
660
661 pub fn prev_track(&mut self) {
663 match self.shared_state.retreat_cursor() {
664 Some((id, path)) => {
665 if matches!(path.try_exists(), Ok(true)) {
666 if let Err(e) = self.start_playback(id, &path, 0) {
667 log::error!("prev track failed: {}", e);
668 }
669 } else {
670 log::warn!("prev track path doesn't exist: {}", path.display());
671 }
672 }
673 None => {
674 if let Some(info) = self.shared_state.track_info()
676 && let Err(e) = self.start_playback(info.id, &info.path, 0)
677 {
678 log::error!("restart failed: {}", e);
679 }
680 }
681 }
682 }
683
684 pub fn pause(&mut self) {
686 if let Some(ref playback) = self.active_playback {
687 let _ = playback.engine.stop();
688 self.shared_state.set_playback_state(PlaybackState::Paused);
689 }
690 }
691
692 pub fn resume(&mut self) {
694 if let Some(ref playback) = self.active_playback {
695 let _ = playback.engine.start();
696 self.shared_state.set_playback_state(PlaybackState::Playing);
697 }
698 }
699
700 pub fn stop(&mut self) {
702 self.shared_state.clear_playlist();
703 self.stop_playback_and_clear_state();
704 }
705
706 fn stop_engine(&mut self) {
713 if let Some(playback) = self.active_playback.take() {
714 let _ = playback.engine.stop();
716 playback.decode_handle.signal_stop();
718
719 let ActivePlayback {
724 engine,
725 decode_handle,
726 } = playback;
727 drop(engine);
728
729 thread::Builder::new()
733 .name("koan-cleanup".into())
734 .spawn(move || drop(decode_handle))
735 .ok();
736 }
737 }
738
739 fn stop_playback_and_clear_state(&mut self) {
741 self.stop_engine();
742 self.timeline.reset();
743 self.shared_state.set_playback_state(PlaybackState::Stopped);
744 self.shared_state.set_position_ms(0);
745 self.shared_state.set_track_info(None);
746 }
747
748 pub fn remove_from_playlist(&mut self, id: QueueItemId) {
750 let was_cursor = self.shared_state.is_cursor(id);
751 self.shared_state.remove_item(id);
752 if was_cursor {
753 self.next_track();
754 }
755 }
756
757 pub fn track_ready(&mut self, id: QueueItemId) {
760 self.shared_state.update_load_state(id, LoadState::Ready);
762
763 if !self.shared_state.is_cursor(id) {
764 return;
765 }
766
767 let is_playing = self.shared_state.playback_state() == PlaybackState::Playing;
768 let current_track_id = self.shared_state.track_info().map(|t| t.id);
769
770 if is_playing && current_track_id == Some(id) {
771 log::info!(
774 "track_ready: download complete while streaming {:?}, refreshing metadata",
775 id
776 );
777 self.refresh_track_metadata(id);
778 return;
779 }
780
781 if !is_playing && let Some(path) = self.shared_state.item_path_if_ready(id) {
783 log::info!("track_ready: starting playback for {:?}", id);
784 if let Err(e) = self.start_playback(id, &path, 0) {
785 log::error!("track_ready playback failed: {}", e);
786 }
787 }
788 }
789
790 pub fn track_stream_ready(&mut self, id: QueueItemId) {
793 if !self.shared_state.is_cursor(id) {
794 return;
795 }
796
797 let is_playing = self.shared_state.playback_state() == PlaybackState::Playing;
798 if is_playing {
799 return; }
801
802 match self.shared_state.item_playback_source(id) {
803 Some(PlaybackSource::Streaming {
804 path,
805 bytes_written,
806 total,
807 }) => {
808 log::info!(
809 "track_stream_ready: starting streaming playback for {:?}",
810 id
811 );
812 if let Err(e) = self.start_streaming_playback(id, &path, bytes_written, total) {
813 log::error!("track_stream_ready streaming failed: {}", e);
814 }
815 }
816 Some(PlaybackSource::Ready(path)) => {
817 log::info!(
819 "track_stream_ready: track already ready, starting normal playback for {:?}",
820 id
821 );
822 if let Err(e) = self.start_playback(id, &path, 0) {
823 log::error!("track_stream_ready playback failed: {}", e);
824 }
825 }
826 None => {} }
828 }
829
830 fn refresh_track_metadata(&mut self, id: QueueItemId) {
834 use crate::index::metadata;
835
836 let path = match self.shared_state.item_path_if_ready(id) {
837 Some(p) => p,
838 None => return,
839 };
840
841 match metadata::read_metadata(&path) {
842 Ok(meta) => {
843 self.shared_state.update_item_metadata(
845 id,
846 meta.title,
847 meta.artist,
848 meta.album_artist.unwrap_or_default(),
849 meta.album,
850 meta.duration_ms.map(|d| d as u64),
851 );
852
853 if let Ok(stream_info) = buffer::probe_file(&path)
858 && let Some(current) = self.shared_state.track_info()
859 && current.id == id
860 && stream_info.duration_ms > current.duration_ms
861 {
862 log::info!(
863 "track_ready: duration corrected {}ms → {}ms",
864 current.duration_ms,
865 stream_info.duration_ms
866 );
867 self.shared_state.set_track_info(Some(TrackInfo {
868 duration_ms: stream_info.duration_ms,
869 ..current
870 }));
871 }
872
873 self.shared_state.signal_metadata_refresh();
875 log::info!("track_ready: metadata refreshed for {:?}", id);
876 }
877 Err(e) => {
878 log::warn!("track_ready: metadata refresh failed for {:?}: {}", id, e);
879 }
880 }
881 }
882
883 pub fn update_playback_state(&self) {
886 if self.active_playback.is_none() {
887 return;
888 }
889
890 if let Some((id, path, info, position_ms)) = self.timeline.current_playback() {
891 self.shared_state.set_position_ms(position_ms);
892
893 let current_id = self.shared_state.track_info().map(|t| t.id);
896 if current_id != Some(id) {
897 log::info!("timeline: now playing {:?}", id);
898 self.shared_state.set_track_info(Some(TrackInfo {
899 id,
900 path,
901 codec: info.codec,
902 sample_rate: info.sample_rate,
903 bit_depth: info.bit_depth,
904 channels: info.channels,
905 duration_ms: info.duration_ms,
906 }));
907 self.shared_state.set_cursor(Some(id));
908 }
909 }
910 }
911
912 fn on_decode_finished(&mut self) {
915 log::info!("decode finished, checking for next track");
916 match self.shared_state.advance_cursor() {
917 Some((id, path)) => {
918 if let Err(e) = self.start_playback(id, &path, 0) {
919 log::error!("auto-advance after decode finished failed: {}", e);
920 self.stop_playback_and_clear_state();
921 }
922 }
923 None => {
924 log::info!("no more tracks — stopping");
925 self.stop_playback_and_clear_state();
926 }
927 }
928 }
929
930 fn push_undo(&mut self, entry: UndoEntry) {
932 if let Some(ref mut batch) = self.batch_buffer {
933 batch.push(entry);
934 } else {
935 self.undo_stack.push(entry);
936 }
937 }
938
939 pub fn process_command(&mut self, cmd: PlayerCommand) {
941 match cmd {
942 PlayerCommand::Play(id) => self.play(id),
943 PlayerCommand::Pause => self.pause(),
944 PlayerCommand::Resume => self.resume(),
945 PlayerCommand::Stop => self.stop(),
946 PlayerCommand::Seek(pos) => self.seek(pos),
947 PlayerCommand::NextTrack => {
948 let now = std::time::Instant::now();
950 if now.duration_since(self.last_skip).as_millis() >= 150 {
951 self.last_skip = now;
952 self.next_track();
953 }
954 }
955 PlayerCommand::PrevTrack => {
956 let now = std::time::Instant::now();
957 if now.duration_since(self.last_skip).as_millis() >= 150 {
958 self.last_skip = now;
959 self.prev_track();
960 }
961 }
962 PlayerCommand::AddToPlaylist(items) => {
963 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
964 self.shared_state.add_items(items);
965 self.push_undo(UndoEntry::Added { ids });
966 }
967 PlayerCommand::UpdatePaths(updates) => {
968 self.shared_state.update_paths(&updates);
969 if let Some(info) = self.shared_state.track_info()
970 && let Some((_, new_path)) = updates.iter().find(|(id, _)| *id == info.id)
971 {
972 self.shared_state.set_track_info(Some(TrackInfo {
973 path: new_path.clone(),
974 ..info
975 }));
976 }
977 }
978 PlayerCommand::InsertInPlaylist { items, after } => {
979 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
980 self.shared_state.insert_items_after(items, after);
981 self.push_undo(UndoEntry::Inserted { ids });
982 }
983 PlayerCommand::ClearPlaylist => {
984 self.stop_playback_and_clear_state();
988 let (items, cursor) = self.shared_state.snapshot_playlist();
989 self.shared_state.clear_playlist();
990 self.push_undo(UndoEntry::Replaced { items, cursor });
991 }
992 PlayerCommand::RemoveFromPlaylist(id) => {
993 let item = self.shared_state.get_item(id);
994 let after = self.shared_state.item_before(id);
995 self.remove_from_playlist(id);
996 if let Some(item) = item {
997 self.push_undo(UndoEntry::Removed {
998 items: vec![(Box::new(item), after)],
999 });
1000 }
1001 }
1002 PlayerCommand::RemoveFromPlaylistBatch(ids) => {
1003 let items_with_pos: Vec<_> = ids
1005 .iter()
1006 .filter_map(|&id| {
1007 let item = self.shared_state.get_item(id)?;
1008 let after = self.shared_state.item_before(id);
1009 Some((Box::new(item), after))
1010 })
1011 .collect();
1012 for &id in &ids {
1013 self.remove_from_playlist(id);
1014 }
1015 if !items_with_pos.is_empty() {
1016 self.push_undo(UndoEntry::Removed {
1017 items: items_with_pos,
1018 });
1019 }
1020 }
1021 PlayerCommand::MoveInPlaylist { id, target, after } => {
1022 let was_after = self.shared_state.item_before(id);
1023 self.shared_state.move_item(id, target, after);
1024 self.push_undo(UndoEntry::Moved { id, was_after });
1025 }
1026 PlayerCommand::MoveItemsInPlaylist { ids, target, after } => {
1027 let entries = self.shared_state.items_before(&ids);
1028 self.shared_state.move_items(&ids, target, after);
1029 self.push_undo(UndoEntry::MovedBatch { entries });
1030 }
1031 PlayerCommand::TrackReady(id) => self.track_ready(id),
1032 PlayerCommand::DecodeFinished => self.on_decode_finished(),
1033 PlayerCommand::TrackStreamReady(id) => self.track_stream_ready(id),
1034 PlayerCommand::Undo => self.execute_undo(),
1035 PlayerCommand::Redo => self.execute_redo(),
1036 PlayerCommand::BeginUndoBatch => {
1037 self.batch_buffer = Some(Vec::new());
1038 }
1039 PlayerCommand::EndUndoBatch => {
1040 if let Some(entries) = self.batch_buffer.take() {
1041 if entries.len() == 1 {
1042 self.undo_stack.push(entries.into_iter().next().unwrap());
1044 } else if !entries.is_empty() {
1045 self.undo_stack.push(UndoEntry::Batch(entries));
1046 }
1047 }
1048 }
1049 PlayerCommand::SetOutputDevice(name) => self.set_output_device(name),
1050 PlayerCommand::ClearOutputDevice => self.clear_output_device(),
1051 }
1052 }
1053
1054 fn apply_entry(&mut self, entry: UndoEntry) -> Option<UndoEntry> {
1056 match entry {
1057 UndoEntry::Added { ids } => {
1058 let items_with_pos: Vec<_> = ids
1060 .iter()
1061 .filter_map(|&id| {
1062 let item = self.shared_state.get_item(id)?;
1063 let after = self.shared_state.item_before(id);
1064 Some((Box::new(item), after))
1065 })
1066 .collect();
1067 self.shared_state.remove_items(&ids);
1068 Some(UndoEntry::Removed {
1069 items: items_with_pos,
1070 })
1071 }
1072 UndoEntry::Removed { items } => {
1073 let mut ids = Vec::with_capacity(items.len());
1075 for (item, after) in items {
1076 ids.push(item.id);
1077 self.shared_state.insert_item_at(*item, after);
1078 }
1079 Some(UndoEntry::Added { ids })
1080 }
1081 UndoEntry::Inserted { ids } => {
1082 let items_with_pos: Vec<_> = ids
1084 .iter()
1085 .filter_map(|&id| {
1086 let item = self.shared_state.get_item(id)?;
1087 let after = self.shared_state.item_before(id);
1088 Some((Box::new(item), after))
1089 })
1090 .collect();
1091 self.shared_state.remove_items(&ids);
1092 Some(UndoEntry::Removed {
1093 items: items_with_pos,
1094 })
1095 }
1096 UndoEntry::Moved { id, was_after } => {
1097 let current_after = self.shared_state.item_before(id);
1098 self.shared_state.move_item_to(id, was_after);
1099 Some(UndoEntry::Moved {
1100 id,
1101 was_after: current_after,
1102 })
1103 }
1104 UndoEntry::MovedBatch { entries } => {
1105 let ids: Vec<QueueItemId> = entries.iter().map(|(id, _)| *id).collect();
1106 let current_positions = self.shared_state.items_before(&ids);
1107 self.shared_state.move_items_to(&entries);
1108 Some(UndoEntry::MovedBatch {
1109 entries: current_positions,
1110 })
1111 }
1112 UndoEntry::Replaced { items, cursor } => {
1113 let (current_items, current_cursor) = self.shared_state.snapshot_playlist();
1114 self.shared_state.restore_playlist(items, cursor);
1115 Some(UndoEntry::Replaced {
1116 items: current_items,
1117 cursor: current_cursor,
1118 })
1119 }
1120 UndoEntry::Batch(entries) => {
1121 let mut inverses = Vec::with_capacity(entries.len());
1123 for entry in entries.into_iter().rev() {
1124 if let Some(inverse) = self.apply_entry(entry) {
1125 inverses.push(inverse);
1126 }
1127 }
1128 inverses.reverse();
1129 Some(UndoEntry::Batch(inverses))
1130 }
1131 }
1132 }
1133
1134 fn execute_undo(&mut self) {
1136 let Some(entry) = self.undo_stack.pop_undo() else {
1137 return;
1138 };
1139 if let Some(inverse) = self.apply_entry(entry) {
1140 self.undo_stack.push_redo(inverse);
1141 }
1142 }
1143
1144 fn execute_redo(&mut self) {
1146 let Some(entry) = self.undo_stack.pop_redo() else {
1147 return;
1148 };
1149 if let Some(inverse) = self.apply_entry(entry) {
1150 self.undo_stack.push_undo_keep_redo(inverse);
1151 }
1152 }
1153
1154 pub fn run(&mut self) {
1156 use std::time::Duration;
1157
1158 let rx = self.commands.rx.clone();
1159 loop {
1160 match rx.recv_timeout(Duration::from_millis(50)) {
1162 Ok(cmd) => self.process_command(cmd),
1163 Err(crossbeam_channel::RecvTimeoutError::Timeout) => {}
1164 Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
1165 }
1166 self.update_playback_state();
1167 }
1168 self.stop();
1169 }
1170
1171 pub fn spawn() -> (
1174 Arc<SharedPlayerState>,
1175 Arc<PlaybackTimeline>,
1176 Arc<VizSnapshot>,
1177 crossbeam_channel::Sender<PlayerCommand>,
1178 ) {
1179 let mut player = Self::new();
1180 let state = player.shared_state();
1181 let timeline = player.timeline();
1182 let viz_snapshot = player.viz_snapshot();
1183 let tx = player.command_sender();
1184
1185 thread::Builder::new()
1186 .name("koan-player".into())
1187 .spawn(move || player.run())
1188 .expect("failed to spawn player thread");
1189
1190 (state, timeline, viz_snapshot, tx)
1191 }
1192}
1193
1194#[cfg(test)]
1195mod tests {
1196 use super::*;
1197 use state::PlaylistItem;
1198 use std::path::PathBuf;
1199
1200 fn make_item(title: &str) -> PlaylistItem {
1201 PlaylistItem {
1202 id: QueueItemId::new(),
1203 db_id: None,
1204 path: PathBuf::from(format!("/music/{title}.flac")),
1205 title: title.to_string(),
1206 artist: String::new(),
1207 album_artist: String::new(),
1208 album: String::new(),
1209 year: None,
1210 codec: None,
1211 track_number: None,
1212 disc: None,
1213 duration_ms: None,
1214 load_state: LoadState::Ready,
1215 }
1216 }
1217
1218 fn playlist_ids(player: &Player) -> Vec<QueueItemId> {
1219 let (items, _) = player.shared_state.snapshot_playlist();
1220 items.iter().map(|i| i.id).collect()
1221 }
1222
1223 fn playlist_titles(player: &Player) -> Vec<String> {
1224 let (items, _) = player.shared_state.snapshot_playlist();
1225 items.iter().map(|i| i.title.clone()).collect()
1226 }
1227
1228 #[test]
1231 fn undo_add_removes_items() {
1232 let mut player = Player::new();
1233 let items = vec![make_item("A"), make_item("B")];
1234 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
1235
1236 player.process_command(PlayerCommand::AddToPlaylist(items));
1237 assert_eq!(playlist_ids(&player), ids);
1238 assert!(player.undo_stack().can_undo());
1239
1240 player.process_command(PlayerCommand::Undo);
1241 assert!(playlist_ids(&player).is_empty());
1242 assert!(player.undo_stack().can_redo());
1243 }
1244
1245 #[test]
1246 fn redo_add_restores_items() {
1247 let mut player = Player::new();
1248 let items = vec![make_item("A"), make_item("B")];
1249
1250 player.process_command(PlayerCommand::AddToPlaylist(items));
1251 player.process_command(PlayerCommand::Undo);
1252 assert!(playlist_ids(&player).is_empty());
1253
1254 player.process_command(PlayerCommand::Redo);
1255 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1256 }
1257
1258 #[test]
1261 fn undo_remove_restores_item_at_position() {
1262 let mut player = Player::new();
1263 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1264 let b_id = items[1].id;
1265
1266 player.process_command(PlayerCommand::AddToPlaylist(items));
1267 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1268 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1269
1270 player.process_command(PlayerCommand::Undo);
1271 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1272 }
1273
1274 #[test]
1275 fn undo_remove_first_item() {
1276 let mut player = Player::new();
1277 let items = vec![make_item("A"), make_item("B")];
1278 let a_id = items[0].id;
1279
1280 player.process_command(PlayerCommand::AddToPlaylist(items));
1281 player.process_command(PlayerCommand::RemoveFromPlaylist(a_id));
1282 assert_eq!(playlist_titles(&player), vec!["B"]);
1283
1284 player.process_command(PlayerCommand::Undo);
1285 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1286 }
1287
1288 #[test]
1289 fn undo_batch_remove_restores_all() {
1290 let mut player = Player::new();
1291 let items = vec![
1292 make_item("A"),
1293 make_item("B"),
1294 make_item("C"),
1295 make_item("D"),
1296 ];
1297 let b_id = items[1].id;
1298 let c_id = items[2].id;
1299
1300 player.process_command(PlayerCommand::AddToPlaylist(items));
1301 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![b_id, c_id]));
1302 assert_eq!(playlist_titles(&player), vec!["A", "D"]);
1303
1304 player.process_command(PlayerCommand::Undo);
1306 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
1307 }
1308
1309 #[test]
1310 fn redo_batch_remove() {
1311 let mut player = Player::new();
1312 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1313 let a_id = items[0].id;
1314 let b_id = items[1].id;
1315
1316 player.process_command(PlayerCommand::AddToPlaylist(items));
1317 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![a_id, b_id]));
1318 player.process_command(PlayerCommand::Undo);
1319 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1320
1321 player.process_command(PlayerCommand::Redo);
1322 assert_eq!(playlist_titles(&player), vec!["C"]);
1323 }
1324
1325 #[test]
1326 fn redo_remove() {
1327 let mut player = Player::new();
1328 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1329 let b_id = items[1].id;
1330
1331 player.process_command(PlayerCommand::AddToPlaylist(items));
1332 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1333 player.process_command(PlayerCommand::Undo);
1334 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1335
1336 player.process_command(PlayerCommand::Redo);
1337 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1338 }
1339
1340 #[test]
1343 fn undo_insert_removes_inserted_items() {
1344 let mut player = Player::new();
1345 let items = vec![make_item("A"), make_item("C")];
1346 let a_id = items[0].id;
1347
1348 player.process_command(PlayerCommand::AddToPlaylist(items));
1349
1350 let inserted = vec![make_item("B")];
1351 player.process_command(PlayerCommand::InsertInPlaylist {
1352 items: inserted,
1353 after: a_id,
1354 });
1355 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1356
1357 player.process_command(PlayerCommand::Undo);
1358 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1359 }
1360
1361 #[test]
1364 fn undo_move_restores_position() {
1365 let mut player = Player::new();
1366 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1367 let a_id = items[0].id;
1368 let c_id = items[2].id;
1369
1370 player.process_command(PlayerCommand::AddToPlaylist(items));
1371
1372 player.process_command(PlayerCommand::MoveInPlaylist {
1374 id: a_id,
1375 target: c_id,
1376 after: true,
1377 });
1378 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1379
1380 player.process_command(PlayerCommand::Undo);
1381 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1382 }
1383
1384 #[test]
1385 fn redo_move() {
1386 let mut player = Player::new();
1387 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1388 let a_id = items[0].id;
1389 let c_id = items[2].id;
1390
1391 player.process_command(PlayerCommand::AddToPlaylist(items));
1392 player.process_command(PlayerCommand::MoveInPlaylist {
1393 id: a_id,
1394 target: c_id,
1395 after: true,
1396 });
1397 player.process_command(PlayerCommand::Undo);
1398 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1399
1400 player.process_command(PlayerCommand::Redo);
1401 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1402 }
1403
1404 #[test]
1407 fn undo_batch_move() {
1408 let mut player = Player::new();
1409 let items = vec![
1410 make_item("A"),
1411 make_item("B"),
1412 make_item("C"),
1413 make_item("D"),
1414 ];
1415 let a_id = items[0].id;
1416 let b_id = items[1].id;
1417 let d_id = items[3].id;
1418
1419 player.process_command(PlayerCommand::AddToPlaylist(items));
1420
1421 player.process_command(PlayerCommand::MoveItemsInPlaylist {
1423 ids: vec![a_id, b_id],
1424 target: d_id,
1425 after: true,
1426 });
1427 assert_eq!(playlist_titles(&player), vec!["C", "D", "A", "B"]);
1428
1429 player.process_command(PlayerCommand::Undo);
1430 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
1431 }
1432
1433 #[test]
1436 fn undo_clear_restores_playlist() {
1437 let mut player = Player::new();
1438 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1439
1440 player.process_command(PlayerCommand::AddToPlaylist(items));
1441 player.process_command(PlayerCommand::ClearPlaylist);
1442 assert!(playlist_ids(&player).is_empty());
1443
1444 player.process_command(PlayerCommand::Undo);
1445 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1446 }
1447
1448 #[test]
1449 fn redo_clear() {
1450 let mut player = Player::new();
1451 let items = vec![make_item("A"), make_item("B")];
1452
1453 player.process_command(PlayerCommand::AddToPlaylist(items));
1454 player.process_command(PlayerCommand::ClearPlaylist);
1455 player.process_command(PlayerCommand::Undo);
1456 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1457
1458 player.process_command(PlayerCommand::Redo);
1459 assert!(playlist_ids(&player).is_empty());
1460 }
1461
1462 #[test]
1465 fn multiple_undos_in_sequence() {
1466 let mut player = Player::new();
1467
1468 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("A")]));
1469 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
1470 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("C")]));
1471 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1472
1473 player.process_command(PlayerCommand::Undo);
1474 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1475
1476 player.process_command(PlayerCommand::Undo);
1477 assert_eq!(playlist_titles(&player), vec!["A"]);
1478
1479 player.process_command(PlayerCommand::Undo);
1480 assert!(playlist_ids(&player).is_empty());
1481 }
1482
1483 #[test]
1484 fn undo_redo_undo_cycle() {
1485 let mut player = Player::new();
1486 let items = vec![make_item("A"), make_item("B")];
1487
1488 player.process_command(PlayerCommand::AddToPlaylist(items));
1489 player.process_command(PlayerCommand::Undo);
1490 assert!(playlist_ids(&player).is_empty());
1491
1492 player.process_command(PlayerCommand::Redo);
1493 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1494
1495 player.process_command(PlayerCommand::Undo);
1496 assert!(playlist_ids(&player).is_empty());
1497 }
1498
1499 #[test]
1500 fn new_action_clears_redo_stack() {
1501 let mut player = Player::new();
1502 let items = vec![make_item("A")];
1503
1504 player.process_command(PlayerCommand::AddToPlaylist(items));
1505 player.process_command(PlayerCommand::Undo);
1506 assert!(player.undo_stack().can_redo());
1507
1508 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
1510 assert!(!player.undo_stack().can_redo());
1511 }
1512
1513 #[test]
1514 fn undo_on_empty_stack_is_noop() {
1515 let mut player = Player::new();
1516 player.process_command(PlayerCommand::Undo);
1517 assert!(playlist_ids(&player).is_empty());
1518 }
1519
1520 #[test]
1521 fn redo_on_empty_stack_is_noop() {
1522 let mut player = Player::new();
1523 player.process_command(PlayerCommand::Redo);
1524 assert!(playlist_ids(&player).is_empty());
1525 }
1526
1527 #[test]
1530 fn playback_commands_not_undoable() {
1531 let mut player = Player::new();
1532 player.process_command(PlayerCommand::Pause);
1533 player.process_command(PlayerCommand::Resume);
1534 player.process_command(PlayerCommand::NextTrack);
1535 player.process_command(PlayerCommand::PrevTrack);
1536 assert!(!player.undo_stack().can_undo());
1537 }
1538
1539 #[test]
1540 fn update_paths_not_undoable() {
1541 let mut player = Player::new();
1542 let items = vec![make_item("A")];
1543 let id = items[0].id;
1544 player.process_command(PlayerCommand::AddToPlaylist(items));
1545
1546 let undo_count = player.undo_stack().undo_len();
1547 player.process_command(PlayerCommand::UpdatePaths(vec![(
1548 id,
1549 PathBuf::from("/new/path.flac"),
1550 )]));
1551 assert_eq!(player.undo_stack().undo_len(), undo_count);
1552 }
1553
1554 #[test]
1557 fn add_remove_undo_undo_produces_original() {
1558 let mut player = Player::new();
1559 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1560 let b_id = items[1].id;
1561 let original_titles = vec!["A", "B", "C"];
1562
1563 player.process_command(PlayerCommand::AddToPlaylist(items));
1564 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1565 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1566
1567 player.process_command(PlayerCommand::Undo);
1569 assert_eq!(playlist_titles(&player), original_titles);
1570
1571 player.process_command(PlayerCommand::Undo);
1573 assert!(playlist_ids(&player).is_empty());
1574 }
1575
1576 #[test]
1577 fn interleaved_adds_and_moves_undo() {
1578 let mut player = Player::new();
1579 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1580 let a_id = items[0].id;
1581 let c_id = items[2].id;
1582
1583 player.process_command(PlayerCommand::AddToPlaylist(items));
1584
1585 player.process_command(PlayerCommand::MoveInPlaylist {
1587 id: a_id,
1588 target: c_id,
1589 after: true,
1590 });
1591 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1592
1593 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("D")]));
1595 assert_eq!(playlist_titles(&player), vec!["B", "C", "A", "D"]);
1596
1597 player.process_command(PlayerCommand::Undo);
1599 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1600
1601 player.process_command(PlayerCommand::Undo);
1603 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1604 }
1605
1606 #[test]
1611 fn stop_engine_drops_engine_synchronously() {
1612 use std::sync::atomic::AtomicBool;
1613
1614 struct MockEngine {
1615 dropped: Arc<AtomicBool>,
1616 }
1617 impl AudioEngineHandle for MockEngine {
1618 fn start(&self) -> Result<(), BackendError> {
1619 Ok(())
1620 }
1621 fn stop(&self) -> Result<(), BackendError> {
1622 Ok(())
1623 }
1624 fn is_running(&self) -> bool {
1625 false
1626 }
1627 }
1628 impl Drop for MockEngine {
1629 fn drop(&mut self) {
1630 self.dropped.store(true, Ordering::SeqCst);
1631 }
1632 }
1633
1634 let dropped = Arc::new(AtomicBool::new(false));
1635
1636 let stop_flag = Arc::new(AtomicBool::new(false));
1638 let decode_handle = buffer::DecodeHandle::new_for_test(stop_flag);
1639
1640 let mut player = Player::new();
1641 player.active_playback = Some(ActivePlayback {
1642 engine: Box::new(MockEngine {
1643 dropped: dropped.clone(),
1644 }),
1645 decode_handle,
1646 });
1647
1648 player.stop_engine();
1649
1650 assert!(
1654 dropped.load(Ordering::SeqCst),
1655 "AudioEngine must be dropped synchronously in stop_engine (GitHub #89)"
1656 );
1657 }
1658}