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