Skip to main content

proteus_lib/playback/player/
controls.rs

1//! Transport and lifecycle operations for `Player`.
2//!
3//! Methods here coordinate playback-state transitions with the runtime thread
4//! and expose user-facing control primitives (play/pause/seek/stop, volume,
5//! reporting hooks, and schedule inspection).
6
7use std::sync::{Arc, Mutex};
8use std::thread;
9use std::time::{Duration, Instant};
10
11use log::{info, warn};
12
13use crate::diagnostics::reporter::{Report, Reporter};
14
15use super::{Player, PlayerState};
16
17impl Player {
18    /// Start playback from a specific timestamp (seconds).
19    ///
20    /// # Arguments
21    ///
22    /// * `ts` - Target start position in seconds.
23    pub fn play_at(&mut self, ts: f64) {
24        let mut timestamp = self.ts.lock().unwrap();
25        *timestamp = ts;
26        drop(timestamp);
27
28        self.request_effects_reset();
29        self.clear_inline_effects_update();
30        self.kill_current();
31        self.initialize_thread(Some(ts));
32
33        self.resume();
34
35        self.wait_for_audio_heard(Duration::from_secs(5));
36    }
37
38    /// Start playback from the current timestamp.
39    ///
40    /// If no playback thread is currently alive, a new runtime is created.
41    pub fn play(&mut self) {
42        info!("Playing audio");
43        let thread_exists = self
44            .playback_thread_exists
45            .load(std::sync::atomic::Ordering::SeqCst);
46
47        if !thread_exists {
48            self.initialize_thread(None);
49        }
50
51        self.resume();
52
53        self.wait_for_audio_heard(Duration::from_secs(5));
54    }
55
56    /// Pause playback.
57    pub fn pause(&self) {
58        self.state.lock().unwrap().clone_from(&PlayerState::Pausing);
59    }
60
61    /// Resume playback if paused.
62    pub fn resume(&self) {
63        self.state
64            .lock()
65            .unwrap()
66            .clone_from(&PlayerState::Resuming);
67    }
68
69    /// Stop the current playback thread and wait for it to exit.
70    ///
71    /// Internal state is moved through `Stopping` and finalized as `Stopped`.
72    pub fn kill_current(&self) {
73        self.state
74            .lock()
75            .unwrap()
76            .clone_from(&PlayerState::Stopping);
77        {
78            let sink = self.sink.lock().unwrap();
79            sink.stop();
80        }
81        self.abort.store(true, std::sync::atomic::Ordering::SeqCst);
82
83        while !self.thread_finished() {
84            thread::sleep(Duration::from_millis(10));
85        }
86
87        self.state.lock().unwrap().clone_from(&PlayerState::Stopped);
88    }
89
90    /// Stop playback and reset timing state.
91    pub fn stop(&self) {
92        self.kill_current();
93        self.ts.lock().unwrap().clone_from(&0.0);
94    }
95
96    /// Return true if playback is currently active.
97    pub fn is_playing(&self) -> bool {
98        let state = self.state.lock().unwrap();
99        *state == PlayerState::Playing
100    }
101
102    /// Return true if playback is currently paused.
103    pub fn is_paused(&self) -> bool {
104        let state = self.state.lock().unwrap();
105        *state == PlayerState::Paused
106    }
107
108    /// Get the current playback time in seconds.
109    pub fn get_time(&self) -> f64 {
110        let ts = self.ts.lock().unwrap();
111        *ts
112    }
113
114    /// Return `true` when no playback worker thread is alive.
115    pub(super) fn thread_finished(&self) -> bool {
116        let playback_thread_exists = self
117            .playback_thread_exists
118            .load(std::sync::atomic::Ordering::SeqCst);
119        !playback_thread_exists
120    }
121
122    /// Return true if playback has reached the end.
123    pub fn is_finished(&self) -> bool {
124        self.thread_finished()
125    }
126
127    /// Block the current thread until playback finishes.
128    pub fn sleep_until_end(&self) {
129        loop {
130            if self.thread_finished() {
131                break;
132            }
133            thread::sleep(Duration::from_millis(100));
134        }
135    }
136
137    /// Get the total duration (seconds) of the active selection.
138    pub fn get_duration(&self) -> f64 {
139        let duration = self.duration.lock().unwrap();
140        *duration
141    }
142
143    /// Seek to the given timestamp (seconds).
144    ///
145    /// Seeking rebuilds the playback runtime at `ts` and applies configured
146    /// seek fade-out/fade-in behavior when currently playing.
147    ///
148    /// # Arguments
149    ///
150    /// * `ts` - New playback position in seconds.
151    pub fn seek(&mut self, ts: f64) {
152        let mut timestamp = self.ts.lock().unwrap();
153        *timestamp = ts;
154        drop(timestamp);
155
156        let state = self.state.lock().unwrap().clone();
157        let was_active = matches!(state, PlayerState::Playing | PlayerState::Resuming);
158        let (seek_fade_out_ms, seek_fade_in_ms) = {
159            let settings = self.buffer_settings.lock().unwrap();
160            (settings.seek_fade_out_ms, settings.seek_fade_in_ms)
161        };
162        if was_active && seek_fade_out_ms > 0.0 {
163            self.fade_current_sink_out(seek_fade_out_ms);
164        }
165        self.request_effects_reset();
166        self.clear_inline_effects_update();
167
168        self.kill_current();
169        self.initialize_thread(Some(ts));
170        if was_active {
171            *self.next_resume_fade_ms.lock().unwrap() = Some(seek_fade_in_ms);
172            self.resume();
173        } else {
174            self.state.lock().unwrap().clone_from(&state);
175        }
176    }
177
178    /// Apply a short linear fade-out to the current sink before disruptive ops.
179    ///
180    /// # Arguments
181    ///
182    /// * `fade_ms` - Fade duration in milliseconds.
183    fn fade_current_sink_out(&self, fade_ms: f32) {
184        let steps = ((fade_ms / 5.0).ceil() as u32).max(1);
185        let step_ms = (fade_ms / steps as f32).max(1.0) as u64;
186        let sink = self.sink.lock().unwrap();
187        let start_volume = sink.volume().max(0.0);
188        if start_volume <= 0.0 {
189            return;
190        }
191        for step in 1..=steps {
192            let t = step as f32 / steps as f32;
193            let gain = start_volume * (1.0 - t);
194            sink.set_volume(gain.max(0.0));
195            thread::sleep(Duration::from_millis(step_ms));
196        }
197    }
198
199    /// Refresh active track selections from the underlying container.
200    ///
201    /// Existing reverb overrides are re-applied and active playback is
202    /// restarted at the current timestamp.
203    pub fn refresh_tracks(&mut self) {
204        let mut prot = self.prot.lock().unwrap();
205        prot.refresh_tracks();
206        if let Some(spec) = self.impulse_response_override.clone() {
207            prot.set_impulse_response_spec(spec);
208        }
209        if let Some(tail_db) = self.impulse_response_tail_override {
210            prot.set_impulse_response_tail_db(tail_db);
211        }
212        drop(prot);
213
214        self.request_effects_reset();
215        self.clear_inline_effects_update();
216        if self.thread_finished() {
217            return;
218        }
219
220        let ts = self.get_time();
221        self.seek(ts);
222
223        if self.is_playing() {
224            self.resume();
225        }
226
227        self.wait_for_audio_heard(Duration::from_secs(5));
228    }
229
230    /// Wait until the runtime reports that at least one chunk was appended.
231    ///
232    /// # Arguments
233    ///
234    /// * `timeout` - Maximum wait duration before returning `false`.
235    ///
236    /// # Returns
237    ///
238    /// `true` once audio has been observed, `false` on timeout or early thread
239    /// termination.
240    pub(super) fn wait_for_audio_heard(&self, timeout: Duration) -> bool {
241        let start = Instant::now();
242        loop {
243            if self.audio_heard.load(std::sync::atomic::Ordering::Relaxed) {
244                return true;
245            }
246            if self.thread_finished() {
247                warn!("playback thread ended before audio was heard");
248                return false;
249            }
250            if start.elapsed() >= timeout {
251                warn!("timed out waiting for audio to start");
252                return false;
253            }
254            thread::sleep(Duration::from_millis(10));
255        }
256    }
257
258    /// Shuffle track selections and restart playback.
259    pub fn shuffle(&mut self) {
260        self.refresh_tracks();
261    }
262
263    /// Set the playback volume (linear gain).
264    ///
265    /// # Arguments
266    ///
267    /// * `new_volume` - Desired sink gain multiplier.
268    pub fn set_volume(&mut self, new_volume: f32) {
269        let sink = self.sink.lock().unwrap();
270        sink.set_volume(new_volume);
271        drop(sink);
272
273        let mut volume = self.volume.lock().unwrap();
274        *volume = new_volume;
275        drop(volume);
276    }
277
278    /// Get the current playback volume.
279    pub fn get_volume(&self) -> f32 {
280        *self.volume.lock().unwrap()
281    }
282
283    /// Get the track identifiers used for display.
284    pub fn get_ids(&self) -> Vec<String> {
285        let prot = self.prot.lock().unwrap();
286        prot.get_ids()
287    }
288
289    /// Get the full timestamped shuffle schedule used by playback.
290    ///
291    /// Each entry is `(time_seconds, selected_ids_or_paths)`.
292    pub fn get_shuffle_schedule(&self) -> Vec<(f64, Vec<String>)> {
293        let prot = self.prot.lock().unwrap();
294        prot.get_shuffle_schedule()
295    }
296
297    /// Enable periodic reporting of playback status for UI consumers.
298    ///
299    /// Any previous reporter instance is stopped before a new one is started.
300    ///
301    /// # Arguments
302    ///
303    /// * `reporting` - Callback invoked with periodic playback snapshots.
304    /// * `reporting_interval` - Time between callback invocations.
305    pub fn set_reporting(
306        &mut self,
307        reporting: Arc<Mutex<dyn Fn(Report) + Send>>,
308        reporting_interval: Duration,
309    ) {
310        if self.reporter.is_some() {
311            self.reporter.as_ref().unwrap().lock().unwrap().stop();
312        }
313
314        let reporter = Arc::new(Mutex::new(Reporter::new(
315            Arc::new(Mutex::new(self.clone())),
316            reporting,
317            reporting_interval,
318        )));
319
320        reporter.lock().unwrap().start();
321
322        self.reporter = Some(reporter);
323    }
324}