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;
5use std::collections::HashMap;
6
7use log::{error, info, warn};
8
9use crate::container::info::*;
10use crate::container::play_settings::{PlaySettingsFile, PlaySettingsLegacy, SettingsTrack};
11use crate::dsp::effects::convolution_reverb::{
12    parse_impulse_response_spec, parse_impulse_response_tail_db, ImpulseResponseSpec,
13};
14use crate::dsp::effects::AudioEffect;
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<PathsTrack>>,
22    file_paths_dictionary: Option<Vec<String>>,
23    track_ids: Option<Vec<u32>>,
24    track_paths: Option<Vec<String>>,
25    duration: f64,
26    play_settings: Option<PlaySettingsFile>,
27    impulse_response_spec: Option<ImpulseResponseSpec>,
28    impulse_response_tail_db: Option<f32>,
29    effects: Option<Vec<AudioEffect>>,
30}
31
32impl Prot {
33    /// Load a single container file and resolve tracks.
34    pub fn new(file_path: &String) -> Self {
35        let info = Info::new(file_path.clone());
36
37        println!("Info: {:?}", info);
38
39        let mut this = Self {
40            info,
41            file_path: Some(file_path.clone()),
42            file_paths: None,
43            file_paths_dictionary: None,
44            track_ids: None,
45            track_paths: None,
46            duration: 0.0,
47            play_settings: None,
48            impulse_response_spec: None,
49            impulse_response_tail_db: None,
50            effects: None,
51        };
52
53        this.load_play_settings();
54        this.refresh_tracks();
55
56        this
57    }
58
59    /// Build a container from multiple standalone file path sets.
60    pub fn new_from_file_paths(file_paths: Vec<PathsTrack>) -> Self {
61        let mut file_paths_dictionary = Vec::new();
62        // Add all file paths to file_paths_dictionary
63        // but do not add duplicates
64        for track in file_paths.clone() {
65            for path in &track.file_paths {
66                if !file_paths_dictionary.contains(path) {
67                    file_paths_dictionary.push(path.clone());
68                }
69            }
70        }
71
72        let info = Info::new_from_file_paths(file_paths_dictionary.clone());
73
74        let mut this = Self {
75            info,
76            file_path: None,
77            file_paths: Some(file_paths),
78            file_paths_dictionary: Some(file_paths_dictionary),
79            track_ids: None,
80            track_paths: None,
81            duration: 0.0,
82            play_settings: None,
83            impulse_response_spec: None,
84            impulse_response_tail_db: None,
85            effects: None,
86        };
87
88        this.refresh_tracks();
89
90        this
91    }
92
93    /// Legacy constructor for backwards compatibility.
94    pub fn new_from_file_paths_legacy(file_paths: &Vec<Vec<String>>) -> Self {
95        let mut paths_track_list = Vec::new();
96        for track in file_paths {
97            paths_track_list.push(PathsTrack::new_from_file_paths(track.clone()));
98        }
99        Self::new_from_file_paths(paths_track_list)
100    }
101
102    // fn get_duration_from_file_path(file_path: &String) -> f64 {
103    //     let file = std::fs::File::open(file_path).unwrap();
104    //     let symphonia: Symphonia = Symphonia::open(file).expect("Could not open file");
105    // }
106
107    /// Rebuild the active track list (e.g., after shuffle).
108    pub fn refresh_tracks(&mut self) {
109        let mut longest_duration = 0.0;
110
111        if let Some(file_paths) = &self.file_paths {
112            // Choose random file path from each file_paths array
113            let mut track_paths: Vec<String> = Vec::new();
114            for track in file_paths {
115                if track.file_paths.is_empty() {
116                    continue;
117                }
118                let selections = track.selections_count as usize;
119                if selections == 0 {
120                    continue;
121                }
122                for _ in 0..selections {
123                    let random_number = rand::thread_rng().gen_range(0..track.file_paths.len());
124                    let track_path = track.file_paths[random_number].clone();
125
126                    let index_in_dictionary = self
127                        .file_paths_dictionary
128                        .as_ref()
129                        .unwrap()
130                        .iter()
131                        .position(|x| *x == track_path)
132                        .unwrap();
133                    let duration = self.info.get_duration(index_in_dictionary as u32).unwrap();
134
135                    if duration > longest_duration {
136                        longest_duration = duration;
137                        self.duration = longest_duration;
138                    }
139
140                    track_paths.push(track_path);
141                }
142            }
143
144            self.track_paths = Some(track_paths);
145
146            return;
147        }
148
149        if !self.file_path.is_some() {
150            return;
151        }
152
153        let mut track_index_array: Vec<u32> = Vec::new();
154        match self.play_settings.as_ref() {
155            Some(play_settings) => match play_settings {
156                PlaySettingsFile::Legacy(file) => {
157                    collect_legacy_tracks(
158                        file.settings.inner(),
159                        &mut track_index_array,
160                        &mut longest_duration,
161                        &self.info,
162                        &mut self.duration,
163                    );
164                }
165                PlaySettingsFile::V1(file) => {
166                    collect_tracks_from_ids(
167                        &file.settings.inner().tracks,
168                        &mut track_index_array,
169                        &mut longest_duration,
170                        &self.info,
171                        &mut self.duration,
172                    );
173                }
174                PlaySettingsFile::V2(file) => {
175                    collect_tracks_from_ids(
176                        &file.settings.inner().tracks,
177                        &mut track_index_array,
178                        &mut longest_duration,
179                        &self.info,
180                        &mut self.duration,
181                    );
182                }
183                PlaySettingsFile::V3(file) => {
184                    collect_tracks_from_ids(
185                        &file.settings.inner().tracks,
186                        &mut track_index_array,
187                        &mut longest_duration,
188                        &self.info,
189                        &mut self.duration,
190                    );
191                }
192                PlaySettingsFile::Unknown { .. } => {
193                    error!("Unknown file format");
194                }
195            },
196            None => {
197                warn!("No play_settings.json found; no tracks resolved.");
198            }
199        }
200
201        self.track_ids = Some(track_index_array);
202    }
203
204    /// Return effects parsed from play_settings, if any.
205    pub fn get_effects(&self) -> Option<Vec<AudioEffect>> {
206        self.effects.clone()
207    }
208
209    fn load_play_settings(&mut self) {
210        println!("Loading play settings...");
211        let Some(file_path) = self.file_path.as_ref() else {
212            return;
213        };
214
215        let file = std::fs::File::open(file_path).unwrap();
216        let mka: Matroska = Matroska::open(file).expect("Could not open file");
217
218        let mut parsed = None;
219
220        for attachment in &mka.attachments {
221            if attachment.name == "play_settings.json" {
222                match serde_json::from_slice::<PlaySettingsFile>(&attachment.data) {
223                    Ok(play_settings) => {
224                        parsed = Some(play_settings);
225                        break;
226                    }
227                    Err(err) => {
228                        error!("Failed to parse play_settings.json: {}", err);
229                    }
230                }
231            }
232        }
233
234        let Some(play_settings) = parsed else {
235            return;
236        };
237
238        info!("Parsed play_settings.json");
239
240        self.impulse_response_spec = parse_impulse_response_spec(&play_settings);
241        self.impulse_response_tail_db = parse_impulse_response_tail_db(&play_settings);
242
243        match &play_settings {
244            PlaySettingsFile::V1(file) => {
245                self.effects = Some(file.settings.inner().effects.clone());
246            }
247            PlaySettingsFile::V2(file) => {
248                self.effects = Some(file.settings.inner().effects.clone());
249            }
250            PlaySettingsFile::V3(file) => {
251                self.effects = Some(file.settings.inner().effects.clone());
252            }
253            _ => {}
254        }
255
256        if let Some(effects) = self.effects.as_ref() {
257            info!(
258                "Loaded play_settings effects ({}): {:?}",
259                effects.len(),
260                effects
261            );
262        }
263
264        self.play_settings = Some(play_settings);
265    }
266
267    /// Get the convolution impulse response spec, if configured.
268    pub fn get_impulse_response_spec(&self) -> Option<ImpulseResponseSpec> {
269        self.impulse_response_spec.clone()
270    }
271
272    /// Get the configured impulse response tail trim in dB, if any.
273    pub fn get_impulse_response_tail_db(&self) -> Option<f32> {
274        self.impulse_response_tail_db
275    }
276
277    /// Return the container path if this is a `.prot`/`.mka` file.
278    pub fn get_container_path(&self) -> Option<String> {
279        self.file_path.clone()
280    }
281
282    /// Override the impulse response spec at runtime.
283    pub fn set_impulse_response_spec(&mut self, spec: ImpulseResponseSpec) {
284        self.impulse_response_spec = Some(spec);
285    }
286
287    /// Override the impulse response tail trim at runtime.
288    pub fn set_impulse_response_tail_db(&mut self, tail_db: f32) {
289        self.impulse_response_tail_db = Some(tail_db);
290    }
291
292    /// Return per-track keys for UI selection.
293    pub fn get_keys(&self) -> Vec<u32> {
294        // This should just be a range from 0 to the length of the track_paths or track_ids array
295        if let Some(track_paths) = &self.track_paths {
296            return (0..track_paths.len() as u32).collect();
297        }
298
299        if let Some(track_ids) = &self.track_ids {
300            return (0..track_ids.len() as u32).collect();
301        }
302
303        Vec::new()
304    }
305
306    /// Return per-track identifiers or file paths for display.
307    pub fn get_ids(&self) -> Vec<String> {
308        if let Some(track_paths) = &self.track_paths {
309            return track_paths.clone();
310        }
311
312        if let Some(track_ids) = &self.track_ids {
313            return track_ids.into_iter().map(|id| format!("{}", id)).collect();
314        }
315
316        Vec::new()
317    }
318
319    /// Return a list of `(key, path, optional track_id)` for buffering.
320    pub fn enumerated_list(&self) -> Vec<(u16, String, Option<u32>)> {
321        let mut list: Vec<(u16, String, Option<u32>)> = Vec::new();
322        if let Some(track_paths) = &self.track_paths {
323            for (index, file_path) in track_paths.iter().enumerate() {
324                list.push((index as u16, String::from(file_path), None));
325            }
326
327            return list;
328        }
329
330        if let Some(track_ids) = &self.track_ids {
331            for (index, track_id) in track_ids.iter().enumerate() {
332                list.push((
333                    index as u16,
334                    String::from(self.file_path.as_ref().unwrap()),
335                    Some(*track_id),
336                ));
337            }
338
339            return list;
340        }
341
342        list
343    }
344
345    /// Return container track entries for shared container streaming.
346    pub fn container_track_entries(&self) -> Option<(String, Vec<(u16, u32)>)> {
347        let file_path = self.file_path.as_ref()?;
348        let track_ids = self.track_ids.as_ref()?;
349        let mut entries = Vec::new();
350        for (index, track_id) in track_ids.iter().enumerate() {
351            entries.push((index as u16, *track_id));
352        }
353        Some((file_path.clone(), entries))
354    }
355
356    /// Get the longest selected duration (seconds).
357    pub fn get_duration(&self) -> &f64 {
358        &self.duration
359    }
360
361    /// Return per-track `(level, pan)` settings keyed by track key.
362    pub fn get_track_mix_settings(&self) -> HashMap<u16, (f32, f32)> {
363        let mut settings = HashMap::new();
364
365        let tracks = match self.play_settings.as_ref() {
366            Some(PlaySettingsFile::V1(file)) => Some(&file.settings.inner().tracks),
367            Some(PlaySettingsFile::V2(file)) => Some(&file.settings.inner().tracks),
368            _ => None,
369        };
370
371        if let Some(tracks) = tracks {
372            for (index, track) in tracks.iter().enumerate() {
373                settings.insert(index as u16, (track.level, track.pan));
374            }
375        }
376
377        settings
378    }
379
380    /// Return the number of selected tracks.
381    pub fn get_length(&self) -> usize {
382        if let Some(track_paths) = &self.track_paths {
383            return track_paths.len();
384        }
385
386        if let Some(file_paths) = &self.file_paths {
387            return file_paths.len();
388        }
389
390        if let Some(track_ids) = &self.track_ids {
391            return track_ids.len();
392        }
393
394        0
395    }
396
397    /// Return the number of possible unique selections based on track settings.
398    pub fn count_possible_combinations(&self) -> Option<u128> {
399        if let Some(file_paths) = &self.file_paths {
400            return count_paths_track_combinations(file_paths);
401        }
402
403        let play_settings = self.play_settings.as_ref()?;
404        match play_settings {
405            PlaySettingsFile::Legacy(file) => {
406                count_legacy_track_combinations(file.settings.inner())
407            }
408            PlaySettingsFile::V1(file) => {
409                count_settings_track_combinations(&file.settings.inner().tracks)
410            }
411            PlaySettingsFile::V2(file) => {
412                count_settings_track_combinations(&file.settings.inner().tracks)
413            }
414            PlaySettingsFile::V3(file) => {
415                count_settings_track_combinations(&file.settings.inner().tracks)
416            }
417            PlaySettingsFile::Unknown { .. } => None,
418        }
419    }
420
421    /// Return the unique file paths used for a multi-file container.
422    pub fn get_file_paths_dictionary(&self) -> Vec<String> {
423        match &self.file_paths_dictionary {
424            Some(dictionary) => dictionary.to_vec(),
425            None => Vec::new(),
426        }
427    }
428}
429
430/// Standalone file-path track configuration.
431#[derive(Debug, Clone)]
432pub struct PathsTrack {
433    /// Candidate file paths for this track.
434    pub file_paths: Vec<String>,
435    /// Track gain scalar.
436    pub level: f32,
437    /// Track pan position.
438    pub pan: f32,
439    /// Number of selections to pick per refresh.
440    pub selections_count: u32,
441}
442
443fn count_settings_track_combinations(tracks: &[SettingsTrack]) -> Option<u128> {
444    let mut total: u128 = 1;
445    for track in tracks {
446        let choices = track.ids.len() as u128;
447        let selections = track.selections_count;
448        let count = checked_pow(choices, selections)?;
449        total = total.checked_mul(count)?;
450    }
451    Some(total)
452}
453
454fn count_paths_track_combinations(tracks: &[PathsTrack]) -> Option<u128> {
455    let mut total: u128 = 1;
456    for track in tracks {
457        let choices = track.file_paths.len() as u128;
458        let selections = track.selections_count;
459        let count = checked_pow(choices, selections)?;
460        total = total.checked_mul(count)?;
461    }
462    Some(total)
463}
464
465fn count_legacy_track_combinations(settings: &PlaySettingsLegacy) -> Option<u128> {
466    let mut total: u128 = 1;
467    for track in &settings.tracks {
468        let choices = track.length.unwrap_or(0) as u128;
469        let count = checked_pow(choices, 1)?;
470        total = total.checked_mul(count)?;
471    }
472    Some(total)
473}
474
475fn checked_pow(base: u128, exp: u32) -> Option<u128> {
476    if exp == 0 {
477        return Some(1);
478    }
479    if base == 0 {
480        return Some(1);
481    }
482    let mut result: u128 = 1;
483    for _ in 0..exp {
484        result = result.checked_mul(base)?;
485    }
486    Some(result)
487}
488
489impl PathsTrack {
490    /// Create a new PathsTrack from a vector of file paths.
491    pub fn new_from_file_paths(file_paths: Vec<String>) -> Self {
492        PathsTrack {
493            file_paths,
494            level: 1.0,
495            pan: 0.0,
496            selections_count: 1,
497        }
498    }
499}
500
501fn collect_tracks_from_ids(
502    tracks: &[SettingsTrack],
503    track_index_array: &mut Vec<u32>,
504    longest_duration: &mut f64,
505    info: &Info,
506    total_duration: &mut f64,
507) {
508    for track in tracks {
509        if track.ids.is_empty() {
510            continue;
511        }
512        let selections = track.selections_count as usize;
513        if selections == 0 {
514            continue;
515        }
516        for _ in 0..selections {
517            let random_number = rand::thread_rng().gen_range(0..track.ids.len());
518            let index = track.ids[random_number];
519            if let Some(track_duration) = info.get_duration(index) {
520                if track_duration > *longest_duration {
521                    *longest_duration = track_duration;
522                    *total_duration = *longest_duration;
523                }
524            }
525            track_index_array.push(index);
526        }
527    }
528}
529
530fn collect_legacy_tracks(
531    settings: &PlaySettingsLegacy,
532    track_index_array: &mut Vec<u32>,
533    longest_duration: &mut f64,
534    info: &Info,
535    total_duration: &mut f64,
536) {
537    for track in &settings.tracks {
538        let (Some(starting_index), Some(length)) = (track.starting_index, track.length) else {
539            continue;
540        };
541        let starting_index = starting_index + 1;
542        let index = rand::thread_rng().gen_range(starting_index..(starting_index + length));
543        if let Some(track_duration) = info.get_duration(index) {
544            if track_duration > *longest_duration {
545                *longest_duration = track_duration;
546                *total_duration = *longest_duration;
547            }
548        }
549        track_index_array.push(index);
550    }
551}