Skip to main content

proteus_lib/container/
prot.rs

1//! Container model and play settings parsing for `.prot`/`.mka`.
2
3use matroska::{Audio, Matroska, Settings};
4use rand::Rng;
5use symphonia::core::audio::Channels;
6use symphonia::core::sample::SampleFormat;
7
8use log::{error, warn};
9
10use crate::container::info::*;
11use crate::container::play_settings::{PlaySettingsFile, PlaySettingsLegacy, SettingsTrack};
12use crate::dsp::effects::convolution_reverb::{
13    parse_impulse_response_spec, parse_impulse_response_tail_db, ImpulseResponseSpec,
14};
15
16/// Parsed `.prot` container with resolved tracks and playback metadata.
17#[derive(Debug, Clone)]
18pub struct Prot {
19    pub info: Info,
20    file_path: Option<String>,
21    file_paths: Option<Vec<Vec<String>>>,
22    file_paths_dictionary: Option<Vec<String>>,
23    track_ids: Option<Vec<u32>>,
24    track_paths: Option<Vec<String>>,
25    duration: f64,
26    impulse_response_spec: Option<ImpulseResponseSpec>,
27    impulse_response_tail_db: Option<f32>,
28}
29
30impl Prot {
31    /// Load a single container file and resolve tracks.
32    pub fn new(file_path: &String) -> Self {
33        let info = Info::new(file_path.clone());
34
35        let mut this = Self {
36            info,
37            file_path: Some(file_path.clone()),
38            file_paths: None,
39            file_paths_dictionary: None,
40            track_ids: None,
41            track_paths: None,
42            duration: 0.0,
43            impulse_response_spec: None,
44            impulse_response_tail_db: None,
45        };
46
47        this.refresh_tracks();
48
49        this
50    }
51
52    /// Build a container from multiple standalone file path sets.
53    pub fn new_from_file_paths(file_paths: &Vec<Vec<String>>) -> Self {
54        let mut file_paths_dictionary = Vec::new();
55        // Add all file paths to file_paths_dictionary
56        // but do not add duplicates
57        for file_path in file_paths {
58            for path in file_path {
59                if !file_paths_dictionary.contains(path) {
60                    file_paths_dictionary.push(path.clone());
61                }
62            }
63        }
64
65        let info = Info::new_from_file_paths(file_paths_dictionary.clone());
66
67        let mut this = Self {
68            info,
69            file_path: None,
70            file_paths: Some(file_paths.clone()),
71            file_paths_dictionary: Some(file_paths_dictionary),
72            track_ids: None,
73            track_paths: None,
74            duration: 0.0,
75            impulse_response_spec: None,
76            impulse_response_tail_db: None,
77        };
78
79        this.refresh_tracks();
80
81        this
82    }
83
84    // fn get_duration_from_file_path(file_path: &String) -> f64 {
85    //     let file = std::fs::File::open(file_path).unwrap();
86    //     let symphonia: Symphonia = Symphonia::open(file).expect("Could not open file");
87    // }
88
89    /// Rebuild the active track list (e.g., after shuffle).
90    pub fn refresh_tracks(&mut self) {
91        let mut longest_duration = 0.0;
92        self.impulse_response_spec = None;
93        self.impulse_response_tail_db = None;
94
95        if let Some(file_paths) = &self.file_paths {
96            // Choose random file path from each file_paths array
97            let mut track_paths: Vec<String> = Vec::new();
98            for file_path in file_paths {
99                let random_number = rand::thread_rng().gen_range(0..file_path.len());
100                let track_path = file_path[random_number].clone();
101
102                let index_in_dictionary = self
103                    .file_paths_dictionary
104                    .as_ref()
105                    .unwrap()
106                    .iter()
107                    .position(|x| *x == track_path)
108                    .unwrap();
109                let duration = self.info.get_duration(index_in_dictionary as u32).unwrap();
110
111                if duration > longest_duration {
112                    longest_duration = duration;
113                    self.duration = longest_duration;
114                }
115
116                track_paths.push(track_path);
117            }
118
119            self.track_paths = Some(track_paths);
120
121            return;
122        }
123
124        if !self.file_path.is_some() {
125            return;
126        }
127
128        let file_path = self.file_path.as_ref().unwrap();
129        let file = std::fs::File::open(file_path).unwrap();
130
131        let mka: Matroska = Matroska::open(file).expect("Could not open file");
132
133        let mut track_index_array: Vec<u32> = Vec::new();
134        mka.attachments.iter().for_each(|attachment| {
135            // Only print if name is "play_settings.json"
136            if attachment.name == "play_settings.json" {
137                // read json data from attachment.data to object
138                let play_settings: PlaySettingsFile =
139                    serde_json::from_slice(&attachment.data).unwrap();
140
141                self.impulse_response_spec = parse_impulse_response_spec(&play_settings);
142                self.impulse_response_tail_db = parse_impulse_response_tail_db(&play_settings);
143
144                match &play_settings {
145                    PlaySettingsFile::Legacy(file) => {
146                        collect_legacy_tracks(
147                            file.settings.inner(),
148                            &mut track_index_array,
149                            &mut longest_duration,
150                            &self.info,
151                            &mut self.duration,
152                        );
153                    }
154                    PlaySettingsFile::V1(file) => {
155                        collect_tracks_from_ids(
156                            &file.settings.inner().tracks,
157                            &mut track_index_array,
158                            &mut longest_duration,
159                            &self.info,
160                            &mut self.duration,
161                        );
162                    }
163                    PlaySettingsFile::V2(file) => {
164                        collect_tracks_from_ids(
165                            &file.settings.inner().tracks,
166                            &mut track_index_array,
167                            &mut longest_duration,
168                            &self.info,
169                            &mut self.duration,
170                        );
171                    }
172                    PlaySettingsFile::Unknown { .. } => {
173                        error!("Unknown file format");
174                    }
175                }
176            }
177        });
178
179        self.track_ids = Some(track_index_array);
180    }
181
182    fn get_audio_settings(file_path: &str) -> Audio {
183        let file = std::fs::File::open(file_path).unwrap();
184
185        let symph = match get_probe_result_from_string(file_path) {
186            Ok(probed) => probed,
187            Err(err) => {
188                warn!("Failed to probe audio settings: {}", err);
189                return Audio {
190                    sample_rate: 0.0,
191                    channels: 0,
192                    bit_depth: None,
193                };
194            }
195        };
196
197        let first_track = match symph.format.tracks().first() {
198            Some(track) => &track.codec_params,
199            None => {
200                warn!("No audio tracks found in {}", file_path);
201                return Audio {
202                    sample_rate: 0.0,
203                    channels: 0,
204                    bit_depth: None,
205                };
206            }
207        };
208
209        let channels = {
210            let channels_option = first_track.channels.unwrap_or(Channels::FRONT_CENTRE);
211            channels_option.iter().count()
212        };
213
214        let mut bit_depth = None;
215
216        let bits_per_sample = first_track
217            .bits_per_sample
218            .or_else(|| sample_format_bits(first_track.sample_format));
219        if let Some(bits) = bits_per_sample {
220            bit_depth = Some(bits as u64);
221        }
222
223        let audio = Audio {
224            sample_rate: first_track.sample_rate.unwrap_or(0) as f64,
225            channels: channels as u64,
226            bit_depth,
227        };
228
229        audio
230
231        // let mka: Matroska = Matroska::open(file).expect("Could not open file");
232
233        // let first_audio_settings = mka
234        //     .tracks
235        //     .iter()
236        //     .find_map(|track| {
237        //         if let Settings::Audio(audio_settings) = &track.settings {
238        //             Some(audio_settings.clone()) // assuming you want to keep the settings, and they are cloneable
239        //         } else {
240        //             None
241        //         }
242        //     })
243        //     .expect("Could not find audio settings");
244
245        // first_audio_settings
246    }
247
248    /// Get the convolution impulse response spec, if configured.
249    pub fn get_impulse_response_spec(&self) -> Option<ImpulseResponseSpec> {
250        self.impulse_response_spec.clone()
251    }
252
253    /// Get the configured impulse response tail trim in dB, if any.
254    pub fn get_impulse_response_tail_db(&self) -> Option<f32> {
255        self.impulse_response_tail_db
256    }
257
258    /// Return the container path if this is a `.prot`/`.mka` file.
259    pub fn get_container_path(&self) -> Option<String> {
260        self.file_path.clone()
261    }
262
263    /// Override the impulse response spec at runtime.
264    pub fn set_impulse_response_spec(&mut self, spec: ImpulseResponseSpec) {
265        self.impulse_response_spec = Some(spec);
266    }
267
268    /// Override the impulse response tail trim at runtime.
269    pub fn set_impulse_response_tail_db(&mut self, tail_db: f32) {
270        self.impulse_response_tail_db = Some(tail_db);
271    }
272
273    /// Return per-track keys for UI selection.
274    pub fn get_keys(&self) -> Vec<u32> {
275        // This should just be a range from 0 to the length of the track_paths or track_ids array
276        if let Some(track_paths) = &self.track_paths {
277            return (0..track_paths.len() as u32).collect();
278        }
279
280        if let Some(track_ids) = &self.track_ids {
281            return (0..track_ids.len() as u32).collect();
282        }
283
284        Vec::new()
285    }
286
287    /// Return per-track identifiers or file paths for display.
288    pub fn get_ids(&self) -> Vec<String> {
289        if let Some(track_paths) = &self.track_paths {
290            return track_paths.clone();
291        }
292
293        if let Some(track_ids) = &self.track_ids {
294            return track_ids.into_iter().map(|id| format!("{}", id)).collect();
295        }
296
297        Vec::new()
298    }
299
300    /// Return a list of `(key, path, optional track_id)` for buffering.
301    pub fn enumerated_list(&self) -> Vec<(u16, String, Option<u32>)> {
302        let mut list: Vec<(u16, String, Option<u32>)> = Vec::new();
303        if let Some(track_paths) = &self.track_paths {
304            for (index, file_path) in track_paths.iter().enumerate() {
305                list.push((index as u16, String::from(file_path), None));
306            }
307
308            return list;
309        }
310
311        if let Some(track_ids) = &self.track_ids {
312            for (index, track_id) in track_ids.iter().enumerate() {
313                list.push((
314                    index as u16,
315                    String::from(self.file_path.as_ref().unwrap()),
316                    Some(*track_id),
317                ));
318            }
319
320            return list;
321        }
322
323        list
324    }
325
326    /// Return container track entries for shared container streaming.
327    pub fn container_track_entries(&self) -> Option<(String, Vec<(u16, u32)>)> {
328        let file_path = self.file_path.as_ref()?;
329        let track_ids = self.track_ids.as_ref()?;
330        let mut entries = Vec::new();
331        for (index, track_id) in track_ids.iter().enumerate() {
332            entries.push((index as u16, *track_id));
333        }
334        Some((file_path.clone(), entries))
335    }
336
337    /// Get the longest selected duration (seconds).
338    pub fn get_duration(&self) -> &f64 {
339        &self.duration
340    }
341
342    /// Return the number of selected tracks.
343    pub fn get_length(&self) -> usize {
344        if let Some(file_paths) = &self.file_paths {
345            return file_paths.len();
346        }
347
348        if let Some(track_ids) = &self.track_ids {
349            return track_ids.len();
350        }
351
352        0
353    }
354
355    /// Return the unique file paths used for a multi-file container.
356    pub fn get_file_paths_dictionary(&self) -> Vec<String> {
357        match &self.file_paths_dictionary {
358            Some(dictionary) => dictionary.to_vec(),
359            None => Vec::new(),
360        }
361    }
362}
363
364fn collect_tracks_from_ids(
365    tracks: &[SettingsTrack],
366    track_index_array: &mut Vec<u32>,
367    longest_duration: &mut f64,
368    info: &Info,
369    total_duration: &mut f64,
370) {
371    for track in tracks {
372        if track.ids.is_empty() {
373            continue;
374        }
375        let random_number = rand::thread_rng().gen_range(0..track.ids.len());
376        let index = track.ids[random_number];
377        if let Some(track_duration) = info.get_duration(index) {
378            if track_duration > *longest_duration {
379                *longest_duration = track_duration;
380                *total_duration = *longest_duration;
381            }
382        }
383        track_index_array.push(index);
384    }
385}
386
387fn collect_legacy_tracks(
388    settings: &PlaySettingsLegacy,
389    track_index_array: &mut Vec<u32>,
390    longest_duration: &mut f64,
391    info: &Info,
392    total_duration: &mut f64,
393) {
394    for track in &settings.tracks {
395        let (Some(starting_index), Some(length)) = (track.starting_index, track.length) else {
396            continue;
397        };
398        let starting_index = starting_index + 1;
399        let index = rand::thread_rng().gen_range(starting_index..(starting_index + length));
400        if let Some(track_duration) = info.get_duration(index) {
401            if track_duration > *longest_duration {
402                *longest_duration = track_duration;
403                *total_duration = *longest_duration;
404            }
405        }
406        track_index_array.push(index);
407    }
408}
409
410fn sample_format_bits(sample_format: Option<SampleFormat>) -> Option<u32> {
411    match sample_format {
412        Some(SampleFormat::U8 | SampleFormat::S8) => Some(8),
413        Some(SampleFormat::U16 | SampleFormat::S16) => Some(16),
414        Some(SampleFormat::U24 | SampleFormat::S24) => Some(24),
415        Some(SampleFormat::U32 | SampleFormat::S32 | SampleFormat::F32) => Some(32),
416        Some(SampleFormat::F64) => Some(64),
417        None => None,
418    }
419}