Skip to main content

proteus_lib/container/
prot.rs

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