Skip to main content

proteus_lib/playback/player/
mod.rs

1//! High-level playback controller for the Proteus library.
2//!
3//! `Player` is the primary integration point for consumers that need to load a
4//! container or file list, control transport state, and inspect DSP/runtime
5//! telemetry. Implementation details are split into focused submodules:
6//! - `controls`: transport operations and lifecycle orchestration.
7//! - `effects`: DSP-chain and metering controls.
8//! - `settings`: runtime tuning and debug surface.
9//! - `runtime`: internal playback thread bootstrap and worker loop.
10
11mod controls;
12mod effects;
13mod runtime;
14mod settings;
15
16use rodio::Sink;
17use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
18use std::sync::{Arc, Mutex};
19
20use crate::container::prot::{PathsTrack, Prot};
21use crate::diagnostics::reporter::Reporter;
22use crate::dsp::effects::convolution_reverb::ImpulseResponseSpec;
23use crate::playback::output_meter::OutputMeter;
24use crate::{
25    container::info::Info,
26    dsp::effects::AudioEffect,
27    playback::engine::{
28        DspChainMetrics, InlineEffectsUpdate, InlineTrackMixUpdate, PlaybackBufferSettings,
29    },
30};
31
32/// High-level playback state for the player.
33///
34/// The public transport methods mostly request transitions (`Pausing`,
35/// `Resuming`, `Stopping`) that are resolved on the playback thread.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum PlayerState {
38    Init,
39    Resuming,
40    Playing,
41    Pausing,
42    Paused,
43    Stopping,
44    Stopped,
45    Finished,
46}
47
48/// Snapshot of active reverb settings for UI consumers.
49///
50/// Values are derived from the first matching reverb in the current effect
51/// chain, with precedence handled in `effects::get_reverb_settings`.
52#[derive(Debug, Clone, Copy)]
53pub struct ReverbSettingsSnapshot {
54    pub enabled: bool,
55    pub dry_wet: f32,
56}
57
58const OUTPUT_METER_REFRESH_HZ: f32 = 30.0;
59const OUTPUT_STREAM_OPEN_RETRIES: usize = 20;
60const OUTPUT_STREAM_OPEN_RETRY_MS: u64 = 100;
61
62/// Primary playback controller.
63///
64/// `Player` owns the playback threads, buffering state, and runtime settings
65/// such as volume and reverb configuration.
66#[derive(Clone)]
67pub struct Player {
68    pub info: Info,
69    pub finished_tracks: Arc<Mutex<Vec<i32>>>,
70    pub ts: Arc<Mutex<f64>>,
71    state: Arc<Mutex<PlayerState>>,
72    abort: Arc<AtomicBool>,
73    playback_thread_exists: Arc<AtomicBool>,
74    playback_id: Arc<AtomicU64>,
75    duration: Arc<Mutex<f64>>,
76    prot: Arc<Mutex<Prot>>,
77    audio_heard: Arc<AtomicBool>,
78    volume: Arc<Mutex<f32>>,
79    sink: Arc<Mutex<Sink>>,
80    reporter: Option<Arc<Mutex<Reporter>>>,
81    buffer_settings: Arc<Mutex<PlaybackBufferSettings>>,
82    effects: Arc<Mutex<Vec<AudioEffect>>>,
83    inline_effects_update: Arc<Mutex<Option<InlineEffectsUpdate>>>,
84    inline_track_mix_updates: Arc<Mutex<Vec<InlineTrackMixUpdate>>>,
85    dsp_metrics: Arc<Mutex<DspChainMetrics>>,
86    effects_reset: Arc<AtomicU64>,
87    output_meter: Arc<Mutex<OutputMeter>>,
88    buffering_done: Arc<AtomicBool>,
89    last_chunk_ms: Arc<AtomicU64>,
90    last_time_update_ms: Arc<AtomicU64>,
91    next_resume_fade_ms: Arc<Mutex<Option<f32>>>,
92    impulse_response_override: Option<ImpulseResponseSpec>,
93    impulse_response_tail_override: Option<f32>,
94}
95
96impl Player {
97    /// Create a new player for a single container path.
98    ///
99    /// # Arguments
100    ///
101    /// * `file_path` - Path to a `.prot`/`.mka` container file.
102    pub fn new(file_path: &String) -> Self {
103        Self::new_from_path_or_paths(Some(file_path), None)
104    }
105
106    /// Create a new player for a set of standalone file paths.
107    ///
108    /// # Arguments
109    ///
110    /// * `file_paths` - Pre-normalized track path groups.
111    pub fn new_from_file_paths(file_paths: Vec<PathsTrack>) -> Self {
112        Self::new_from_path_or_paths(None, Some(file_paths))
113    }
114
115    /// Create a new player for legacy standalone file-path groups.
116    ///
117    /// # Arguments
118    ///
119    /// * `file_paths` - Legacy track grouping shape where each inner vector is
120    ///   interpreted as one track candidate set.
121    pub fn new_from_file_paths_legacy(file_paths: Vec<Vec<String>>) -> Self {
122        Self::new_from_path_or_paths(
123            None,
124            Some(
125                file_paths
126                    .into_iter()
127                    .map(PathsTrack::new_from_file_paths)
128                    .collect(),
129            ),
130        )
131    }
132
133    /// Create a player from either a container path or standalone file paths.
134    ///
135    /// Exactly one input source is expected. `path` takes precedence when
136    /// provided; otherwise `paths` is used for file-based playback.
137    ///
138    /// # Arguments
139    ///
140    /// * `path` - Optional container path.
141    /// * `paths` - Optional standalone track path groups.
142    pub fn new_from_path_or_paths(path: Option<&String>, paths: Option<Vec<PathsTrack>>) -> Self {
143        let (prot, info) = match path {
144            Some(path) => {
145                let prot = Arc::new(Mutex::new(Prot::new(path)));
146                let info = Info::new(path.clone());
147                (prot, info)
148            }
149            None => {
150                let prot = Arc::new(Mutex::new(Prot::new_from_file_paths(paths.unwrap())));
151                let locked_prot = prot.lock().unwrap();
152                let info = Info::new_from_file_paths(locked_prot.get_file_paths_dictionary());
153                drop(locked_prot);
154                (prot, info)
155            }
156        };
157
158        let (sink, _queue) = Sink::new();
159        let sink: Arc<Mutex<Sink>> = Arc::new(Mutex::new(sink));
160
161        let channels = info.channels as usize;
162        let sample_rate = info.sample_rate;
163        let effects = {
164            let prot_locked = prot.lock().unwrap();
165            match prot_locked.get_effects() {
166                Some(effects) => Arc::new(Mutex::new(effects)),
167                None => Arc::new(Mutex::new(vec![])),
168            }
169        };
170
171        let mut this = Self {
172            info,
173            finished_tracks: Arc::new(Mutex::new(Vec::new())),
174            state: Arc::new(Mutex::new(PlayerState::Stopped)),
175            abort: Arc::new(AtomicBool::new(false)),
176            ts: Arc::new(Mutex::new(0.0)),
177            playback_thread_exists: Arc::new(AtomicBool::new(true)),
178            playback_id: Arc::new(AtomicU64::new(0)),
179            duration: Arc::new(Mutex::new(0.0)),
180            audio_heard: Arc::new(AtomicBool::new(false)),
181            volume: Arc::new(Mutex::new(0.8)),
182            sink,
183            prot,
184            reporter: None,
185            buffer_settings: Arc::new(Mutex::new(PlaybackBufferSettings::new(20.0))),
186            effects,
187            inline_effects_update: Arc::new(Mutex::new(None)),
188            inline_track_mix_updates: Arc::new(Mutex::new(Vec::new())),
189            dsp_metrics: Arc::new(Mutex::new(DspChainMetrics::default())),
190            effects_reset: Arc::new(AtomicU64::new(0)),
191            output_meter: Arc::new(Mutex::new(OutputMeter::new(
192                channels,
193                sample_rate,
194                OUTPUT_METER_REFRESH_HZ,
195            ))),
196            buffering_done: Arc::new(AtomicBool::new(false)),
197            last_chunk_ms: Arc::new(AtomicU64::new(0)),
198            last_time_update_ms: Arc::new(AtomicU64::new(0)),
199            next_resume_fade_ms: Arc::new(Mutex::new(None)),
200            impulse_response_override: None,
201            impulse_response_tail_override: None,
202        };
203
204        this.initialize_thread(None);
205
206        this
207    }
208}
209
210impl Drop for Player {
211    fn drop(&mut self) {
212        // `Player` is `Clone` and shares state through Arcs. Only the final
213        // handle should tear down the shared runtime resources.
214        if Arc::strong_count(&self.state) != 1 {
215            return;
216        }
217
218        if let Some(reporter) = self.reporter.take() {
219            reporter.lock().unwrap().stop();
220        }
221
222        if self
223            .playback_thread_exists
224            .load(std::sync::atomic::Ordering::SeqCst)
225        {
226            self.kill_current();
227        } else {
228            self.abort.store(true, Ordering::SeqCst);
229        }
230
231        {
232            let sink = self.sink.lock().unwrap();
233            sink.stop();
234            sink.clear();
235        }
236
237        {
238            let mut finished_tracks = self.finished_tracks.lock().unwrap();
239            finished_tracks.clear();
240            finished_tracks.shrink_to_fit();
241        }
242
243        {
244            let mut effects = self.effects.lock().unwrap();
245            effects.clear();
246            effects.shrink_to_fit();
247        }
248
249        {
250            let mut inline_effects_update = self.inline_effects_update.lock().unwrap();
251            *inline_effects_update = None;
252        }
253
254        {
255            let mut inline_track_mix_updates = self.inline_track_mix_updates.lock().unwrap();
256            inline_track_mix_updates.clear();
257            inline_track_mix_updates.shrink_to_fit();
258        }
259
260        {
261            let mut dsp_metrics = self.dsp_metrics.lock().unwrap();
262            *dsp_metrics = DspChainMetrics::default();
263        }
264
265        {
266            let mut output_meter = self.output_meter.lock().unwrap();
267            output_meter.reset();
268        }
269
270        *self.duration.lock().unwrap() = 0.0;
271        *self.ts.lock().unwrap() = 0.0;
272        *self.next_resume_fade_ms.lock().unwrap() = None;
273        self.buffering_done.store(false, Ordering::Relaxed);
274        self.last_chunk_ms.store(0, Ordering::Relaxed);
275        self.last_time_update_ms.store(0, Ordering::Relaxed);
276        self.audio_heard.store(false, Ordering::Relaxed);
277        self.impulse_response_override = None;
278        self.impulse_response_tail_override = None;
279    }
280}