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