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 bitrate_kbps: info.bitrate_kbps,
252 channels: info.channels,
253 duration_ms: info.duration_ms,
254 }));
255 self.shared_state.set_position_ms(seek_ms);
256 log::info!(
257 "playing: {} ({:?}) — {} {}Hz/{}ch, {}ms{}",
258 path.display(),
259 id,
260 info.codec,
261 info.sample_rate,
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 bitrate_kbps: info.bitrate_kbps,
470 channels: info.channels,
471 duration_ms: info.duration_ms,
472 }));
473 self.shared_state.set_position_ms(0);
474 log::info!(
475 "streaming: {} ({:?}) — {} {}Hz/{}ch, {}ms",
476 path.display(),
477 id,
478 info.codec,
479 info.sample_rate,
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 bitrate_kbps: info.bitrate_kbps,
905 channels: info.channels,
906 duration_ms: info.duration_ms,
907 }));
908 self.shared_state.set_cursor(Some(id));
909 }
910 }
911 }
912
913 fn on_decode_finished(&mut self) {
916 log::info!("decode finished, checking for next track");
917 match self.shared_state.advance_cursor() {
918 Some((id, path)) => {
919 if let Err(e) = self.start_playback(id, &path, 0) {
920 log::error!("auto-advance after decode finished failed: {}", e);
921 self.stop_playback_and_clear_state();
922 }
923 }
924 None => {
925 log::info!("no more tracks — stopping");
926 self.stop_playback_and_clear_state();
927 }
928 }
929 }
930
931 fn push_undo(&mut self, entry: UndoEntry) {
933 if let Some(ref mut batch) = self.batch_buffer {
934 batch.push(entry);
935 } else {
936 self.undo_stack.push(entry);
937 }
938 }
939
940 pub fn process_command(&mut self, cmd: PlayerCommand) {
942 match cmd {
943 PlayerCommand::Play(id) => self.play(id),
944 PlayerCommand::Pause => self.pause(),
945 PlayerCommand::Resume => self.resume(),
946 PlayerCommand::Stop => self.stop(),
947 PlayerCommand::Seek(pos) => self.seek(pos),
948 PlayerCommand::NextTrack => {
949 let now = std::time::Instant::now();
951 if now.duration_since(self.last_skip).as_millis() >= 150 {
952 self.last_skip = now;
953 self.next_track();
954 }
955 }
956 PlayerCommand::PrevTrack => {
957 let now = std::time::Instant::now();
958 if now.duration_since(self.last_skip).as_millis() >= 150 {
959 self.last_skip = now;
960 self.prev_track();
961 }
962 }
963 PlayerCommand::AddToPlaylist(items) => {
964 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
965 self.shared_state.add_items(items);
966 self.push_undo(UndoEntry::Added { ids });
967 }
968 PlayerCommand::UpdatePaths(updates) => {
969 self.shared_state.update_paths(&updates);
970 if let Some(info) = self.shared_state.track_info()
971 && let Some((_, new_path)) = updates.iter().find(|(id, _)| *id == info.id)
972 {
973 self.shared_state.set_track_info(Some(TrackInfo {
974 path: new_path.clone(),
975 ..info
976 }));
977 }
978 }
979 PlayerCommand::InsertInPlaylist { items, after } => {
980 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
981 self.shared_state.insert_items_after(items, after);
982 self.push_undo(UndoEntry::Inserted { ids });
983 }
984 PlayerCommand::ClearPlaylist => {
985 self.stop_playback_and_clear_state();
989 let (items, cursor) = self.shared_state.snapshot_playlist();
990 self.shared_state.clear_playlist();
991 self.push_undo(UndoEntry::Replaced { items, cursor });
992 }
993 PlayerCommand::RemoveFromPlaylist(id) => {
994 let item = self.shared_state.get_item(id);
995 let after = self.shared_state.item_before(id);
996 self.remove_from_playlist(id);
997 if let Some(item) = item {
998 self.push_undo(UndoEntry::Removed {
999 items: vec![(Box::new(item), after)],
1000 });
1001 }
1002 }
1003 PlayerCommand::RemoveFromPlaylistBatch(ids) => {
1004 let items_with_pos: Vec<_> = ids
1006 .iter()
1007 .filter_map(|&id| {
1008 let item = self.shared_state.get_item(id)?;
1009 let after = self.shared_state.item_before(id);
1010 Some((Box::new(item), after))
1011 })
1012 .collect();
1013 for &id in &ids {
1014 self.remove_from_playlist(id);
1015 }
1016 if !items_with_pos.is_empty() {
1017 self.push_undo(UndoEntry::Removed {
1018 items: items_with_pos,
1019 });
1020 }
1021 }
1022 PlayerCommand::MoveInPlaylist { id, target, after } => {
1023 let was_after = self.shared_state.item_before(id);
1024 self.shared_state.move_item(id, target, after);
1025 self.push_undo(UndoEntry::Moved { id, was_after });
1026 }
1027 PlayerCommand::MoveItemsInPlaylist { ids, target, after } => {
1028 let entries = self.shared_state.items_before(&ids);
1029 self.shared_state.move_items(&ids, target, after);
1030 self.push_undo(UndoEntry::MovedBatch { entries });
1031 }
1032 PlayerCommand::TrackReady(id) => self.track_ready(id),
1033 PlayerCommand::DecodeFinished => self.on_decode_finished(),
1034 PlayerCommand::TrackStreamReady(id) => self.track_stream_ready(id),
1035 PlayerCommand::Undo => self.execute_undo(),
1036 PlayerCommand::Redo => self.execute_redo(),
1037 PlayerCommand::BeginUndoBatch => {
1038 self.batch_buffer = Some(Vec::new());
1039 }
1040 PlayerCommand::EndUndoBatch => {
1041 if let Some(entries) = self.batch_buffer.take() {
1042 if entries.len() == 1 {
1043 self.undo_stack.push(entries.into_iter().next().unwrap());
1045 } else if !entries.is_empty() {
1046 self.undo_stack.push(UndoEntry::Batch(entries));
1047 }
1048 }
1049 }
1050 PlayerCommand::SetOutputDevice(name) => self.set_output_device(name),
1051 PlayerCommand::ClearOutputDevice => self.clear_output_device(),
1052 }
1053 }
1054
1055 fn apply_entry(&mut self, entry: UndoEntry) -> Option<UndoEntry> {
1057 match entry {
1058 UndoEntry::Added { ids } => {
1059 let items_with_pos: Vec<_> = ids
1061 .iter()
1062 .filter_map(|&id| {
1063 let item = self.shared_state.get_item(id)?;
1064 let after = self.shared_state.item_before(id);
1065 Some((Box::new(item), after))
1066 })
1067 .collect();
1068 self.shared_state.remove_items(&ids);
1069 Some(UndoEntry::Removed {
1070 items: items_with_pos,
1071 })
1072 }
1073 UndoEntry::Removed { items } => {
1074 let mut ids = Vec::with_capacity(items.len());
1076 for (item, after) in items {
1077 ids.push(item.id);
1078 self.shared_state.insert_item_at(*item, after);
1079 }
1080 Some(UndoEntry::Added { ids })
1081 }
1082 UndoEntry::Inserted { ids } => {
1083 let items_with_pos: Vec<_> = ids
1085 .iter()
1086 .filter_map(|&id| {
1087 let item = self.shared_state.get_item(id)?;
1088 let after = self.shared_state.item_before(id);
1089 Some((Box::new(item), after))
1090 })
1091 .collect();
1092 self.shared_state.remove_items(&ids);
1093 Some(UndoEntry::Removed {
1094 items: items_with_pos,
1095 })
1096 }
1097 UndoEntry::Moved { id, was_after } => {
1098 let current_after = self.shared_state.item_before(id);
1099 self.shared_state.move_item_to(id, was_after);
1100 Some(UndoEntry::Moved {
1101 id,
1102 was_after: current_after,
1103 })
1104 }
1105 UndoEntry::MovedBatch { entries } => {
1106 let ids: Vec<QueueItemId> = entries.iter().map(|(id, _)| *id).collect();
1107 let current_positions = self.shared_state.items_before(&ids);
1108 self.shared_state.move_items_to(&entries);
1109 Some(UndoEntry::MovedBatch {
1110 entries: current_positions,
1111 })
1112 }
1113 UndoEntry::Replaced { items, cursor } => {
1114 let (current_items, current_cursor) = self.shared_state.snapshot_playlist();
1115 self.shared_state.restore_playlist(items, cursor);
1116 Some(UndoEntry::Replaced {
1117 items: current_items,
1118 cursor: current_cursor,
1119 })
1120 }
1121 UndoEntry::Batch(entries) => {
1122 let mut inverses = Vec::with_capacity(entries.len());
1124 for entry in entries.into_iter().rev() {
1125 if let Some(inverse) = self.apply_entry(entry) {
1126 inverses.push(inverse);
1127 }
1128 }
1129 inverses.reverse();
1130 Some(UndoEntry::Batch(inverses))
1131 }
1132 }
1133 }
1134
1135 fn execute_undo(&mut self) {
1137 let Some(entry) = self.undo_stack.pop_undo() else {
1138 return;
1139 };
1140 if let Some(inverse) = self.apply_entry(entry) {
1141 self.undo_stack.push_redo(inverse);
1142 }
1143 }
1144
1145 fn execute_redo(&mut self) {
1147 let Some(entry) = self.undo_stack.pop_redo() else {
1148 return;
1149 };
1150 if let Some(inverse) = self.apply_entry(entry) {
1151 self.undo_stack.push_undo_keep_redo(inverse);
1152 }
1153 }
1154
1155 pub fn run(&mut self) {
1157 use std::time::Duration;
1158
1159 let rx = self.commands.rx.clone();
1160 loop {
1161 match rx.recv_timeout(Duration::from_millis(50)) {
1163 Ok(cmd) => self.process_command(cmd),
1164 Err(crossbeam_channel::RecvTimeoutError::Timeout) => {}
1165 Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
1166 }
1167 self.update_playback_state();
1168 }
1169 self.stop();
1170 }
1171
1172 pub fn spawn() -> (
1175 Arc<SharedPlayerState>,
1176 Arc<PlaybackTimeline>,
1177 Arc<VizSnapshot>,
1178 crossbeam_channel::Sender<PlayerCommand>,
1179 ) {
1180 let mut player = Self::new();
1181 let state = player.shared_state();
1182 let timeline = player.timeline();
1183 let viz_snapshot = player.viz_snapshot();
1184 let tx = player.command_sender();
1185
1186 thread::Builder::new()
1187 .name("koan-player".into())
1188 .spawn(move || player.run())
1189 .expect("failed to spawn player thread");
1190
1191 (state, timeline, viz_snapshot, tx)
1192 }
1193}
1194
1195#[cfg(test)]
1196mod tests {
1197 use super::*;
1198 use state::PlaylistItem;
1199 use std::path::PathBuf;
1200
1201 fn make_item(title: &str) -> PlaylistItem {
1202 PlaylistItem {
1203 id: QueueItemId::new(),
1204 db_id: None,
1205 path: PathBuf::from(format!("/music/{title}.flac")),
1206 title: title.to_string(),
1207 artist: String::new(),
1208 album_artist: String::new(),
1209 album: String::new(),
1210 year: None,
1211 codec: None,
1212 track_number: None,
1213 disc: None,
1214 duration_ms: None,
1215 load_state: LoadState::Ready,
1216 }
1217 }
1218
1219 fn playlist_ids(player: &Player) -> Vec<QueueItemId> {
1220 let (items, _) = player.shared_state.snapshot_playlist();
1221 items.iter().map(|i| i.id).collect()
1222 }
1223
1224 fn playlist_titles(player: &Player) -> Vec<String> {
1225 let (items, _) = player.shared_state.snapshot_playlist();
1226 items.iter().map(|i| i.title.clone()).collect()
1227 }
1228
1229 #[test]
1232 fn undo_add_removes_items() {
1233 let mut player = Player::new();
1234 let items = vec![make_item("A"), make_item("B")];
1235 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
1236
1237 player.process_command(PlayerCommand::AddToPlaylist(items));
1238 assert_eq!(playlist_ids(&player), ids);
1239 assert!(player.undo_stack().can_undo());
1240
1241 player.process_command(PlayerCommand::Undo);
1242 assert!(playlist_ids(&player).is_empty());
1243 assert!(player.undo_stack().can_redo());
1244 }
1245
1246 #[test]
1247 fn redo_add_restores_items() {
1248 let mut player = Player::new();
1249 let items = vec![make_item("A"), make_item("B")];
1250
1251 player.process_command(PlayerCommand::AddToPlaylist(items));
1252 player.process_command(PlayerCommand::Undo);
1253 assert!(playlist_ids(&player).is_empty());
1254
1255 player.process_command(PlayerCommand::Redo);
1256 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1257 }
1258
1259 #[test]
1262 fn undo_remove_restores_item_at_position() {
1263 let mut player = Player::new();
1264 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1265 let b_id = items[1].id;
1266
1267 player.process_command(PlayerCommand::AddToPlaylist(items));
1268 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1269 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1270
1271 player.process_command(PlayerCommand::Undo);
1272 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1273 }
1274
1275 #[test]
1276 fn undo_remove_first_item() {
1277 let mut player = Player::new();
1278 let items = vec![make_item("A"), make_item("B")];
1279 let a_id = items[0].id;
1280
1281 player.process_command(PlayerCommand::AddToPlaylist(items));
1282 player.process_command(PlayerCommand::RemoveFromPlaylist(a_id));
1283 assert_eq!(playlist_titles(&player), vec!["B"]);
1284
1285 player.process_command(PlayerCommand::Undo);
1286 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1287 }
1288
1289 #[test]
1290 fn undo_batch_remove_restores_all() {
1291 let mut player = Player::new();
1292 let items = vec![
1293 make_item("A"),
1294 make_item("B"),
1295 make_item("C"),
1296 make_item("D"),
1297 ];
1298 let b_id = items[1].id;
1299 let c_id = items[2].id;
1300
1301 player.process_command(PlayerCommand::AddToPlaylist(items));
1302 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![b_id, c_id]));
1303 assert_eq!(playlist_titles(&player), vec!["A", "D"]);
1304
1305 player.process_command(PlayerCommand::Undo);
1307 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
1308 }
1309
1310 #[test]
1311 fn redo_batch_remove() {
1312 let mut player = Player::new();
1313 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1314 let a_id = items[0].id;
1315 let b_id = items[1].id;
1316
1317 player.process_command(PlayerCommand::AddToPlaylist(items));
1318 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![a_id, b_id]));
1319 player.process_command(PlayerCommand::Undo);
1320 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1321
1322 player.process_command(PlayerCommand::Redo);
1323 assert_eq!(playlist_titles(&player), vec!["C"]);
1324 }
1325
1326 #[test]
1327 fn redo_remove() {
1328 let mut player = Player::new();
1329 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1330 let b_id = items[1].id;
1331
1332 player.process_command(PlayerCommand::AddToPlaylist(items));
1333 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1334 player.process_command(PlayerCommand::Undo);
1335 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1336
1337 player.process_command(PlayerCommand::Redo);
1338 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1339 }
1340
1341 #[test]
1344 fn undo_insert_removes_inserted_items() {
1345 let mut player = Player::new();
1346 let items = vec![make_item("A"), make_item("C")];
1347 let a_id = items[0].id;
1348
1349 player.process_command(PlayerCommand::AddToPlaylist(items));
1350
1351 let inserted = vec![make_item("B")];
1352 player.process_command(PlayerCommand::InsertInPlaylist {
1353 items: inserted,
1354 after: a_id,
1355 });
1356 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1357
1358 player.process_command(PlayerCommand::Undo);
1359 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1360 }
1361
1362 #[test]
1365 fn undo_move_restores_position() {
1366 let mut player = Player::new();
1367 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1368 let a_id = items[0].id;
1369 let c_id = items[2].id;
1370
1371 player.process_command(PlayerCommand::AddToPlaylist(items));
1372
1373 player.process_command(PlayerCommand::MoveInPlaylist {
1375 id: a_id,
1376 target: c_id,
1377 after: true,
1378 });
1379 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1380
1381 player.process_command(PlayerCommand::Undo);
1382 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1383 }
1384
1385 #[test]
1386 fn redo_move() {
1387 let mut player = Player::new();
1388 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1389 let a_id = items[0].id;
1390 let c_id = items[2].id;
1391
1392 player.process_command(PlayerCommand::AddToPlaylist(items));
1393 player.process_command(PlayerCommand::MoveInPlaylist {
1394 id: a_id,
1395 target: c_id,
1396 after: true,
1397 });
1398 player.process_command(PlayerCommand::Undo);
1399 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1400
1401 player.process_command(PlayerCommand::Redo);
1402 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1403 }
1404
1405 #[test]
1408 fn undo_batch_move() {
1409 let mut player = Player::new();
1410 let items = vec![
1411 make_item("A"),
1412 make_item("B"),
1413 make_item("C"),
1414 make_item("D"),
1415 ];
1416 let a_id = items[0].id;
1417 let b_id = items[1].id;
1418 let d_id = items[3].id;
1419
1420 player.process_command(PlayerCommand::AddToPlaylist(items));
1421
1422 player.process_command(PlayerCommand::MoveItemsInPlaylist {
1424 ids: vec![a_id, b_id],
1425 target: d_id,
1426 after: true,
1427 });
1428 assert_eq!(playlist_titles(&player), vec!["C", "D", "A", "B"]);
1429
1430 player.process_command(PlayerCommand::Undo);
1431 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
1432 }
1433
1434 #[test]
1437 fn undo_clear_restores_playlist() {
1438 let mut player = Player::new();
1439 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1440
1441 player.process_command(PlayerCommand::AddToPlaylist(items));
1442 player.process_command(PlayerCommand::ClearPlaylist);
1443 assert!(playlist_ids(&player).is_empty());
1444
1445 player.process_command(PlayerCommand::Undo);
1446 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1447 }
1448
1449 #[test]
1450 fn redo_clear() {
1451 let mut player = Player::new();
1452 let items = vec![make_item("A"), make_item("B")];
1453
1454 player.process_command(PlayerCommand::AddToPlaylist(items));
1455 player.process_command(PlayerCommand::ClearPlaylist);
1456 player.process_command(PlayerCommand::Undo);
1457 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1458
1459 player.process_command(PlayerCommand::Redo);
1460 assert!(playlist_ids(&player).is_empty());
1461 }
1462
1463 #[test]
1466 fn multiple_undos_in_sequence() {
1467 let mut player = Player::new();
1468
1469 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("A")]));
1470 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
1471 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("C")]));
1472 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1473
1474 player.process_command(PlayerCommand::Undo);
1475 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1476
1477 player.process_command(PlayerCommand::Undo);
1478 assert_eq!(playlist_titles(&player), vec!["A"]);
1479
1480 player.process_command(PlayerCommand::Undo);
1481 assert!(playlist_ids(&player).is_empty());
1482 }
1483
1484 #[test]
1485 fn undo_redo_undo_cycle() {
1486 let mut player = Player::new();
1487 let items = vec![make_item("A"), make_item("B")];
1488
1489 player.process_command(PlayerCommand::AddToPlaylist(items));
1490 player.process_command(PlayerCommand::Undo);
1491 assert!(playlist_ids(&player).is_empty());
1492
1493 player.process_command(PlayerCommand::Redo);
1494 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1495
1496 player.process_command(PlayerCommand::Undo);
1497 assert!(playlist_ids(&player).is_empty());
1498 }
1499
1500 #[test]
1501 fn new_action_clears_redo_stack() {
1502 let mut player = Player::new();
1503 let items = vec![make_item("A")];
1504
1505 player.process_command(PlayerCommand::AddToPlaylist(items));
1506 player.process_command(PlayerCommand::Undo);
1507 assert!(player.undo_stack().can_redo());
1508
1509 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
1511 assert!(!player.undo_stack().can_redo());
1512 }
1513
1514 #[test]
1515 fn undo_on_empty_stack_is_noop() {
1516 let mut player = Player::new();
1517 player.process_command(PlayerCommand::Undo);
1518 assert!(playlist_ids(&player).is_empty());
1519 }
1520
1521 #[test]
1522 fn redo_on_empty_stack_is_noop() {
1523 let mut player = Player::new();
1524 player.process_command(PlayerCommand::Redo);
1525 assert!(playlist_ids(&player).is_empty());
1526 }
1527
1528 #[test]
1531 fn playback_commands_not_undoable() {
1532 let mut player = Player::new();
1533 player.process_command(PlayerCommand::Pause);
1534 player.process_command(PlayerCommand::Resume);
1535 player.process_command(PlayerCommand::NextTrack);
1536 player.process_command(PlayerCommand::PrevTrack);
1537 assert!(!player.undo_stack().can_undo());
1538 }
1539
1540 #[test]
1541 fn update_paths_not_undoable() {
1542 let mut player = Player::new();
1543 let items = vec![make_item("A")];
1544 let id = items[0].id;
1545 player.process_command(PlayerCommand::AddToPlaylist(items));
1546
1547 let undo_count = player.undo_stack().undo_len();
1548 player.process_command(PlayerCommand::UpdatePaths(vec![(
1549 id,
1550 PathBuf::from("/new/path.flac"),
1551 )]));
1552 assert_eq!(player.undo_stack().undo_len(), undo_count);
1553 }
1554
1555 #[test]
1558 fn add_remove_undo_undo_produces_original() {
1559 let mut player = Player::new();
1560 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1561 let b_id = items[1].id;
1562 let original_titles = vec!["A", "B", "C"];
1563
1564 player.process_command(PlayerCommand::AddToPlaylist(items));
1565 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1566 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1567
1568 player.process_command(PlayerCommand::Undo);
1570 assert_eq!(playlist_titles(&player), original_titles);
1571
1572 player.process_command(PlayerCommand::Undo);
1574 assert!(playlist_ids(&player).is_empty());
1575 }
1576
1577 #[test]
1578 fn interleaved_adds_and_moves_undo() {
1579 let mut player = Player::new();
1580 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1581 let a_id = items[0].id;
1582 let c_id = items[2].id;
1583
1584 player.process_command(PlayerCommand::AddToPlaylist(items));
1585
1586 player.process_command(PlayerCommand::MoveInPlaylist {
1588 id: a_id,
1589 target: c_id,
1590 after: true,
1591 });
1592 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1593
1594 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("D")]));
1596 assert_eq!(playlist_titles(&player), vec!["B", "C", "A", "D"]);
1597
1598 player.process_command(PlayerCommand::Undo);
1600 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1601
1602 player.process_command(PlayerCommand::Undo);
1604 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1605 }
1606
1607 #[test]
1612 fn stop_engine_drops_engine_synchronously() {
1613 use std::sync::atomic::AtomicBool;
1614
1615 struct MockEngine {
1616 dropped: Arc<AtomicBool>,
1617 }
1618 impl AudioEngineHandle for MockEngine {
1619 fn start(&self) -> Result<(), BackendError> {
1620 Ok(())
1621 }
1622 fn stop(&self) -> Result<(), BackendError> {
1623 Ok(())
1624 }
1625 fn is_running(&self) -> bool {
1626 false
1627 }
1628 }
1629 impl Drop for MockEngine {
1630 fn drop(&mut self) {
1631 self.dropped.store(true, Ordering::SeqCst);
1632 }
1633 }
1634
1635 let dropped = Arc::new(AtomicBool::new(false));
1636
1637 let stop_flag = Arc::new(AtomicBool::new(false));
1639 let decode_handle = buffer::DecodeHandle::new_for_test(stop_flag);
1640
1641 let mut player = Player::new();
1642 player.active_playback = Some(ActivePlayback {
1643 engine: Box::new(MockEngine {
1644 dropped: dropped.clone(),
1645 }),
1646 decode_handle,
1647 });
1648
1649 player.stop_engine();
1650
1651 assert!(
1655 dropped.load(Ordering::SeqCst),
1656 "AudioEngine must be dropped synchronously in stop_engine (GitHub #89)"
1657 );
1658 }
1659}