koan_core/player/commands.rs
1use std::path::PathBuf;
2
3use crossbeam_channel::{Receiver, Sender, bounded};
4
5use super::state::{PlayMode, PlaylistItem, QueueItemId, Repeat};
6
7/// Commands from the UI layer to the audio engine.
8#[derive(Debug)]
9pub enum PlayerCommand {
10 /// Set the cursor and start playback.
11 Play(QueueItemId),
12 /// Set the cursor and load `id` at `position_ms`, playing or paused.
13 ///
14 /// What a restored session does. Play, Seek and Pause in turn would start
15 /// the track from the top and let a moment of it out before the pause.
16 Cue {
17 id: QueueItemId,
18 position_ms: u64,
19 play: bool,
20 },
21 Pause,
22 /// Pause, and answer with the playhead once the output has gone silent:
23 /// after the fade, where one runs. What a hand-off resumes from.
24 PauseAndReport(Sender<u64>),
25 Resume,
26 Stop,
27 Seek(u64), // position in ms
28 NextTrack,
29 PrevTrack,
30 AddToPlaylist(Vec<PlaylistItem>),
31 RemoveFromPlaylist(QueueItemId),
32 /// Batch remove: delete multiple items as a single undoable operation.
33 RemoveFromPlaylistBatch(Vec<QueueItemId>),
34 MoveInPlaylist {
35 id: QueueItemId,
36 target: QueueItemId,
37 after: bool,
38 },
39 /// Batch move: extract `ids` and reinsert them at `target` position.
40 MoveItemsInPlaylist {
41 ids: Vec<QueueItemId>,
42 target: QueueItemId,
43 after: bool,
44 },
45 /// Put the queue in exactly this order, keeping every item.
46 ///
47 /// A queue locked to a playlist follows it, and following a reorder means
48 /// moving the items that are already there — rebuilding them would issue
49 /// new ids and throw away what has played, what is mid-download and where
50 /// the cursor is. Ids not named keep their relative order at the end.
51 ReorderPlaylist(Vec<QueueItemId>),
52 /// Update file paths for playlist items after an organize operation.
53 /// On Unix, rename() doesn't invalidate open FDs so playback continues.
54 UpdatePaths(Vec<(QueueItemId, PathBuf)>),
55 /// Insert items after a specific queue item (for drag/drop at cursor position).
56 InsertInPlaylist {
57 items: Vec<PlaylistItem>,
58 after: QueueItemId,
59 },
60 /// Clear the entire playlist (stop + remove all items).
61 ClearPlaylist,
62 /// Replace the playlist and open the track at `start`, as one operation:
63 /// from `position_ms`, playing or paused.
64 ///
65 /// Doing this as ClearPlaylist + AddToPlaylist + Play sends three commands
66 /// down a bounded channel, and the player acts on each as it arrives: the
67 /// first track starts, then the cursor jumps, so clicking track nine of an
68 /// album shows track one playing first. It is three undo entries for one
69 /// user action. And between the clear and the add the playlist is empty,
70 /// which tells the download queue that nothing is wanted.
71 ///
72 /// `start` past the end starts at the beginning. It opens at
73 /// `position_ms`, playing or paused, as `Cue` does: a hand-off picks up
74 /// where the source stopped without the top of the track being heard.
75 ReplacePlaylist {
76 items: Vec<PlaylistItem>,
77 start: usize,
78 position_ms: u64,
79 play: bool,
80 },
81 /// Download complete — check if cursor is waiting on this item.
82 TrackReady(QueueItemId),
83 /// Enough data buffered for streaming playback — check if cursor is waiting.
84 TrackStreamReady(QueueItemId),
85 /// A partial file has been probed off-thread and can be started.
86 ///
87 /// Probing reads as much of the container as it takes to describe itself,
88 /// which for Ogg means its last page — the whole remaining download. That
89 /// cannot happen on this loop, so it happens on its own thread and arrives
90 /// here as a command like anything else. Stale by the time it lands is the
91 /// normal case, and simply ignored.
92 StreamProbed {
93 id: QueueItemId,
94 info: Box<crate::audio::buffer::StreamInfo>,
95 /// What the probe had to settle for. Decoding has to be opened the same
96 /// way — given a length, a container that went looking for its tail
97 /// once will do it again, on the decode thread, where the cost is
98 /// silence instead of a busy player.
99 mode: crate::audio::streaming::ProbeMode,
100 },
101 /// Download failed — a cursor parked on this item must stop waiting.
102 ///
103 /// Without it the player sits on a `Pending` item forever: `Ready` is the
104 /// only thing it listens for, and a track that cannot be fetched never
105 /// becomes Ready. That is the offline-library stall.
106 TrackFailed(QueueItemId),
107 /// Fetch these tracks into the cache, with no queue entry to play them.
108 CacheTracks(Vec<i64>),
109 /// Decode thread exhausted the playlist — auto-advance or stop. Carries
110 /// the session it came from, so one sent just before a play or seek is
111 /// recognised as stale.
112 DecodeFinished(u64),
113 /// The decoder queued the next track, so when the playhead reaches it is
114 /// now known.
115 TrackQueued,
116 /// Undo the last reversible playlist operation.
117 Undo,
118 /// Redo the last undone operation.
119 Redo,
120 /// Begin collecting undo entries into a single batch (e.g. drag operations).
121 BeginUndoBatch,
122 /// End the batch — collapse collected entries into one undo step.
123 EndUndoBatch,
124 /// Switch output audio device by name. Restarts engine on current track.
125 SetOutputDevice(String),
126 /// Clear the configured output device, reverting to system default.
127 ClearOutputDevice,
128 /// The DSP profiles, or the device they key on, changed: load them again
129 /// and carry on where playback is.
130 ReloadDsp,
131 /// Build the output again on the same device and carry on from where the
132 /// current track is, paused if it was. For an output the system stopped
133 /// underneath us: an iOS interruption (a call, Siri) or a reset of its
134 /// media services leaves the old unit unable to start again.
135 RestartOutput,
136 /// Play to this renderer from now on, carrying on from where the current
137 /// track is; `None` brings the music back to this device's own output.
138 /// The session is opened by the caller, off this thread: see
139 /// `upnp::connect`.
140 UseRenderer(Option<Box<crate::upnp::Connection>>),
141 /// The renderer last used, found on the network after launch. Taken as
142 /// `UseRenderer` would be, unless playback or the output has moved since
143 /// launch, in which case it is dropped: see `upnp::resume`.
144 ResumeRenderer(Box<crate::upnp::Connection>),
145 /// Set the volume of the renderer being played to, 0–100.
146 SetRendererVolume(u8),
147 /// Turn shuffle on or off: the items after the cursor reordered at
148 /// random, or put back as they were. One undo step.
149 SetShuffle(bool),
150 /// What follows a track at its end: the queue's next, the first again
151 /// after the last, or the same item.
152 SetRepeat(Repeat),
153 /// Take the mode a saved session had, its queue already restored in the
154 /// order it was saved. Shuffle reorders nothing here.
155 RestorePlayMode(PlayMode),
156 /// What the renderer was heard to do, during the session numbered
157 /// `session`. Dropped once that session is over, like `DecodeFinished`.
158 Renderer {
159 session: u64,
160 event: crate::upnp::session::Event,
161 },
162}
163
164/// Bounded command channel.
165///
166/// Small capacity — we don't want commands queuing up. If the engine is busy,
167/// the UI should know about it, not silently buffer 50 seeks.
168pub struct CommandChannel {
169 pub tx: Sender<PlayerCommand>,
170 pub rx: Receiver<PlayerCommand>,
171}
172
173impl Default for CommandChannel {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179impl CommandChannel {
180 pub fn new() -> Self {
181 let (tx, rx) = bounded(16);
182 Self { tx, rx }
183 }
184}
185
186impl PlayerCommand {
187 /// Whether it asks for something to be heard.
188 pub fn asks_to_play(&self) -> bool {
189 matches!(
190 self,
191 Self::Play(_)
192 | Self::Cue { play: true, .. }
193 | Self::Resume
194 | Self::NextTrack
195 | Self::PrevTrack
196 | Self::ReplacePlaylist { play: true, .. }
197 )
198 }
199}