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