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::{BTreeSet, HashMap, HashSet};
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#[derive(Debug, Clone, PartialEq, Eq)]
17pub(crate) enum ShuffleSource {
18    TrackId(u32),
19    FilePath(String),
20}
21
22#[derive(Debug, Clone)]
23pub(crate) struct ShuffleScheduleEntry {
24    pub at_ms: u64,
25    pub sources: Vec<ShuffleSource>,
26}
27
28#[derive(Debug, Clone)]
29pub(crate) struct ShuffleRuntimePlan {
30    pub current_sources: Vec<ShuffleSource>,
31    pub upcoming_events: Vec<ShuffleScheduleEntry>,
32}
33
34/// Parsed `.prot` container with resolved tracks and playback metadata.
35#[derive(Debug, Clone)]
36pub struct Prot {
37    pub info: Info,
38    file_path: Option<String>,
39    file_paths: Option<Vec<PathsTrack>>,
40    file_paths_dictionary: Option<Vec<String>>,
41    track_ids: Option<Vec<u32>>,
42    track_paths: Option<Vec<String>>,
43    duration: f64,
44    shuffle_schedule: Vec<ShuffleScheduleEntry>,
45    play_settings: Option<PlaySettingsFile>,
46    impulse_response_spec: Option<ImpulseResponseSpec>,
47    impulse_response_tail_db: Option<f32>,
48    effects: Option<Vec<AudioEffect>>,
49}
50
51impl Prot {
52    /// Load a single container file and resolve tracks.
53    pub fn new(file_path: &String) -> Self {
54        let info = Info::new(file_path.clone());
55
56        println!("Info: {:?}", info);
57
58        let mut this = Self {
59            info,
60            file_path: Some(file_path.clone()),
61            file_paths: None,
62            file_paths_dictionary: None,
63            track_ids: None,
64            track_paths: None,
65            duration: 0.0,
66            shuffle_schedule: Vec::new(),
67            play_settings: None,
68            impulse_response_spec: None,
69            impulse_response_tail_db: None,
70            effects: None,
71        };
72
73        this.load_play_settings();
74        this.refresh_tracks();
75
76        this
77    }
78
79    /// Build a container from multiple standalone file path sets.
80    pub fn new_from_file_paths(file_paths: Vec<PathsTrack>) -> Self {
81        let mut file_paths_dictionary = Vec::new();
82        // Add all file paths to file_paths_dictionary
83        // but do not add duplicates
84        for track in file_paths.clone() {
85            for path in &track.file_paths {
86                if !file_paths_dictionary.contains(path) {
87                    file_paths_dictionary.push(path.clone());
88                }
89            }
90        }
91
92        let info = Info::new_from_file_paths(file_paths_dictionary.clone());
93
94        let mut this = Self {
95            info,
96            file_path: None,
97            file_paths: Some(file_paths),
98            file_paths_dictionary: Some(file_paths_dictionary),
99            track_ids: None,
100            track_paths: None,
101            duration: 0.0,
102            shuffle_schedule: Vec::new(),
103            play_settings: None,
104            impulse_response_spec: None,
105            impulse_response_tail_db: None,
106            effects: None,
107        };
108
109        this.refresh_tracks();
110
111        this
112    }
113
114    /// Legacy constructor for backwards compatibility.
115    pub fn new_from_file_paths_legacy(file_paths: &Vec<Vec<String>>) -> Self {
116        let mut paths_track_list = Vec::new();
117        for track in file_paths {
118            paths_track_list.push(PathsTrack::new_from_file_paths(track.clone()));
119        }
120        Self::new_from_file_paths(paths_track_list)
121    }
122
123    // fn get_duration_from_file_path(file_path: &String) -> f64 {
124    //     let file = std::fs::File::open(file_path).unwrap();
125    //     let symphonia: Symphonia = Symphonia::open(file).expect("Could not open file");
126    // }
127
128    /// Rebuild the active track list (e.g., after shuffle).
129    pub fn refresh_tracks(&mut self) {
130        self.track_ids = None;
131        self.track_paths = None;
132        self.shuffle_schedule.clear();
133        self.duration = 0.0;
134
135        if let Some(file_paths) = &self.file_paths {
136            let (schedule, longest_duration) = build_paths_shuffle_schedule(
137                file_paths,
138                &self.info,
139                self.file_paths_dictionary.as_deref().unwrap_or(&[]),
140            );
141            self.shuffle_schedule = schedule;
142            self.duration = longest_duration;
143
144            if let Some(entry) = self.shuffle_schedule.first() {
145                self.track_paths = Some(sources_to_track_paths(&entry.sources));
146            }
147
148            return;
149        }
150
151        if !self.file_path.is_some() {
152            return;
153        }
154
155        match self.play_settings.as_ref() {
156            Some(play_settings) => match play_settings {
157                PlaySettingsFile::Legacy(file) => {
158                    let mut longest_duration = 0.0;
159                    let mut track_index_array: Vec<u32> = Vec::new();
160                    collect_legacy_tracks(
161                        file.settings.inner(),
162                        &mut track_index_array,
163                        &mut longest_duration,
164                        &self.info,
165                        &mut self.duration,
166                    );
167                    self.track_ids = Some(track_index_array.clone());
168                    self.shuffle_schedule = vec![ShuffleScheduleEntry {
169                        at_ms: 0,
170                        sources: track_index_array
171                            .into_iter()
172                            .map(ShuffleSource::TrackId)
173                            .collect(),
174                    }];
175                }
176                PlaySettingsFile::V1(file) => {
177                    let (schedule, longest_duration) =
178                        build_id_shuffle_schedule(&file.settings.inner().tracks, &self.info);
179                    self.shuffle_schedule = schedule;
180                    self.duration = longest_duration;
181                }
182                PlaySettingsFile::V2(file) => {
183                    let (schedule, longest_duration) =
184                        build_id_shuffle_schedule(&file.settings.inner().tracks, &self.info);
185                    self.shuffle_schedule = schedule;
186                    self.duration = longest_duration;
187                }
188                PlaySettingsFile::V3(file) => {
189                    let (schedule, longest_duration) =
190                        build_id_shuffle_schedule(&file.settings.inner().tracks, &self.info);
191                    self.shuffle_schedule = schedule;
192                    self.duration = longest_duration;
193                }
194                PlaySettingsFile::Unknown { .. } => {
195                    error!("Unknown file format");
196                }
197            },
198            None => {
199                warn!("No play_settings.json found; no tracks resolved.");
200            }
201        }
202
203        if let Some(entry) = self.shuffle_schedule.first() {
204            self.track_ids = Some(sources_to_track_ids(&entry.sources));
205        }
206    }
207
208    /// Return effects parsed from play_settings, if any.
209    pub fn get_effects(&self) -> Option<Vec<AudioEffect>> {
210        self.effects.clone()
211    }
212
213    fn load_play_settings(&mut self) {
214        println!("Loading play settings...");
215        let Some(file_path) = self.file_path.as_ref() else {
216            return;
217        };
218
219        let file = std::fs::File::open(file_path).unwrap();
220        let mka: Matroska = Matroska::open(file).expect("Could not open file");
221
222        let mut parsed = None;
223
224        for attachment in &mka.attachments {
225            if attachment.name == "play_settings.json" {
226                match serde_json::from_slice::<PlaySettingsFile>(&attachment.data) {
227                    Ok(play_settings) => {
228                        parsed = Some(play_settings);
229                        break;
230                    }
231                    Err(err) => {
232                        error!("Failed to parse play_settings.json: {}", err);
233                    }
234                }
235            }
236        }
237
238        let Some(play_settings) = parsed else {
239            return;
240        };
241
242        info!("Parsed play_settings.json");
243
244        self.impulse_response_spec = parse_impulse_response_spec(&play_settings);
245        self.impulse_response_tail_db = parse_impulse_response_tail_db(&play_settings);
246
247        match &play_settings {
248            PlaySettingsFile::V1(file) => {
249                self.effects = Some(file.settings.inner().effects.clone());
250            }
251            PlaySettingsFile::V2(file) => {
252                self.effects = Some(file.settings.inner().effects.clone());
253            }
254            PlaySettingsFile::V3(file) => {
255                self.effects = Some(file.settings.inner().effects.clone());
256            }
257            _ => {}
258        }
259
260        if let Some(effects) = self.effects.as_ref() {
261            info!(
262                "Loaded play_settings effects ({}): {:?}",
263                effects.len(),
264                effects
265            );
266        }
267
268        self.play_settings = Some(play_settings);
269    }
270
271    /// Get the convolution impulse response spec, if configured.
272    pub fn get_impulse_response_spec(&self) -> Option<ImpulseResponseSpec> {
273        self.impulse_response_spec.clone()
274    }
275
276    /// Get the configured impulse response tail trim in dB, if any.
277    pub fn get_impulse_response_tail_db(&self) -> Option<f32> {
278        self.impulse_response_tail_db
279    }
280
281    /// Return the container path if this is a `.prot`/`.mka` file.
282    pub fn get_container_path(&self) -> Option<String> {
283        self.file_path.clone()
284    }
285
286    /// Override the impulse response spec at runtime.
287    pub fn set_impulse_response_spec(&mut self, spec: ImpulseResponseSpec) {
288        self.impulse_response_spec = Some(spec);
289    }
290
291    /// Override the impulse response tail trim at runtime.
292    pub fn set_impulse_response_tail_db(&mut self, tail_db: f32) {
293        self.impulse_response_tail_db = Some(tail_db);
294    }
295
296    /// Return per-track keys for UI selection.
297    pub fn get_keys(&self) -> Vec<u32> {
298        // This should just be a range from 0 to the length of the track_paths or track_ids array
299        if let Some(track_paths) = &self.track_paths {
300            return (0..track_paths.len() as u32).collect();
301        }
302
303        if let Some(track_ids) = &self.track_ids {
304            return (0..track_ids.len() as u32).collect();
305        }
306
307        Vec::new()
308    }
309
310    /// Return per-track identifiers or file paths for display.
311    pub fn get_ids(&self) -> Vec<String> {
312        if let Some(track_paths) = &self.track_paths {
313            return track_paths.clone();
314        }
315
316        if let Some(track_ids) = &self.track_ids {
317            return track_ids.into_iter().map(|id| format!("{}", id)).collect();
318        }
319
320        Vec::new()
321    }
322
323    /// Return the full timestamped shuffle schedule for display.
324    ///
325    /// Each entry is `(time_seconds, selected_ids_or_paths)`.
326    pub fn get_shuffle_schedule(&self) -> Vec<(f64, Vec<String>)> {
327        if self.shuffle_schedule.is_empty() {
328            let current = self.get_ids();
329            if current.is_empty() {
330                return Vec::new();
331            }
332            return vec![(0.0, current)];
333        }
334
335        self.shuffle_schedule
336            .iter()
337            .map(|entry| {
338                let ids = entry
339                    .sources
340                    .iter()
341                    .map(|source| match source {
342                        ShuffleSource::TrackId(track_id) => track_id.to_string(),
343                        ShuffleSource::FilePath(path) => path.clone(),
344                    })
345                    .collect();
346                (entry.at_ms as f64 / 1000.0, ids)
347            })
348            .collect()
349    }
350
351    /// Return a list of `(key, path, optional track_id)` for buffering.
352    pub fn enumerated_list(&self) -> Vec<(u16, String, Option<u32>)> {
353        let mut list: Vec<(u16, String, Option<u32>)> = Vec::new();
354        if let Some(track_paths) = &self.track_paths {
355            for (index, file_path) in track_paths.iter().enumerate() {
356                list.push((index as u16, String::from(file_path), None));
357            }
358
359            return list;
360        }
361
362        if let Some(track_ids) = &self.track_ids {
363            for (index, track_id) in track_ids.iter().enumerate() {
364                list.push((
365                    index as u16,
366                    String::from(self.file_path.as_ref().unwrap()),
367                    Some(*track_id),
368                ));
369            }
370
371            return list;
372        }
373
374        list
375    }
376
377    /// Return container track entries for shared container streaming.
378    pub fn container_track_entries(&self) -> Option<(String, Vec<(u16, u32)>)> {
379        let file_path = self.file_path.as_ref()?;
380        let track_ids = self.track_ids.as_ref()?;
381        let mut entries = Vec::new();
382        for (index, track_id) in track_ids.iter().enumerate() {
383            entries.push((index as u16, *track_id));
384        }
385        Some((file_path.clone(), entries))
386    }
387
388    /// Get the longest selected duration (seconds).
389    pub fn get_duration(&self) -> &f64 {
390        &self.duration
391    }
392
393    pub(crate) fn build_runtime_shuffle_plan(&self, start_time: f64) -> ShuffleRuntimePlan {
394        if self.shuffle_schedule.is_empty() {
395            let mut sources = Vec::new();
396            if let Some(track_ids) = &self.track_ids {
397                sources.extend(track_ids.iter().copied().map(ShuffleSource::TrackId));
398            } else if let Some(track_paths) = &self.track_paths {
399                sources.extend(track_paths.iter().cloned().map(ShuffleSource::FilePath));
400            }
401            return ShuffleRuntimePlan {
402                current_sources: sources,
403                upcoming_events: Vec::new(),
404            };
405        }
406
407        let start_ms = seconds_to_ms(start_time);
408        let mut current_index = 0usize;
409        for (index, entry) in self.shuffle_schedule.iter().enumerate() {
410            if entry.at_ms <= start_ms {
411                current_index = index;
412            } else {
413                break;
414            }
415        }
416
417        ShuffleRuntimePlan {
418            current_sources: self.shuffle_schedule[current_index].sources.clone(),
419            upcoming_events: self.shuffle_schedule[(current_index + 1)..].to_vec(),
420        }
421    }
422
423    /// Return per-track `(level, pan)` settings keyed by track key.
424    pub fn get_track_mix_settings(&self) -> HashMap<u16, (f32, f32)> {
425        let mut settings = HashMap::new();
426
427        let tracks = match self.play_settings.as_ref() {
428            Some(PlaySettingsFile::V1(file)) => Some(&file.settings.inner().tracks),
429            Some(PlaySettingsFile::V2(file)) => Some(&file.settings.inner().tracks),
430            _ => None,
431        };
432
433        if let Some(tracks) = tracks {
434            for (index, track) in tracks.iter().enumerate() {
435                settings.insert(index as u16, (track.level, track.pan));
436            }
437        }
438
439        settings
440    }
441
442    /// Return the number of selected tracks.
443    pub fn get_length(&self) -> usize {
444        if let Some(track_paths) = &self.track_paths {
445            return track_paths.len();
446        }
447
448        if let Some(file_paths) = &self.file_paths {
449            return file_paths.len();
450        }
451
452        if let Some(track_ids) = &self.track_ids {
453            return track_ids.len();
454        }
455
456        0
457    }
458
459    /// Return the number of possible unique selections based on track settings.
460    pub fn count_possible_combinations(&self) -> Option<u128> {
461        if let Some(file_paths) = &self.file_paths {
462            return count_paths_track_combinations(file_paths);
463        }
464
465        let play_settings = self.play_settings.as_ref()?;
466        match play_settings {
467            PlaySettingsFile::Legacy(file) => {
468                count_legacy_track_combinations(file.settings.inner())
469            }
470            PlaySettingsFile::V1(file) => {
471                count_settings_track_combinations(&file.settings.inner().tracks)
472            }
473            PlaySettingsFile::V2(file) => {
474                count_settings_track_combinations(&file.settings.inner().tracks)
475            }
476            PlaySettingsFile::V3(file) => {
477                count_settings_track_combinations(&file.settings.inner().tracks)
478            }
479            PlaySettingsFile::Unknown { .. } => None,
480        }
481    }
482
483    /// Return the unique file paths used for a multi-file container.
484    pub fn get_file_paths_dictionary(&self) -> Vec<String> {
485        match &self.file_paths_dictionary {
486            Some(dictionary) => dictionary.to_vec(),
487            None => Vec::new(),
488        }
489    }
490}
491
492/// Standalone file-path track configuration.
493#[derive(Debug, Clone)]
494pub struct PathsTrack {
495    /// Candidate file paths for this track.
496    pub file_paths: Vec<String>,
497    /// Track gain scalar.
498    pub level: f32,
499    /// Track pan position.
500    pub pan: f32,
501    /// Number of selections to pick per refresh.
502    pub selections_count: u32,
503    /// Timestamps where this track is reshuffled.
504    pub shuffle_points: Vec<String>,
505}
506
507fn count_settings_track_combinations(tracks: &[SettingsTrack]) -> Option<u128> {
508    let mut total: u128 = 1;
509    for track in tracks {
510        let choices = track.ids.len() as u128;
511        let reshuffle_events = parse_shuffle_points(&track.shuffle_points).len() as u32;
512        let total_draws = track
513            .selections_count
514            .checked_mul(reshuffle_events.checked_add(1)?)?;
515        let count = checked_pow(choices, total_draws)?;
516        total = total.checked_mul(count)?;
517    }
518    Some(total)
519}
520
521fn count_paths_track_combinations(tracks: &[PathsTrack]) -> Option<u128> {
522    let mut total: u128 = 1;
523    for track in tracks {
524        let choices = track.file_paths.len() as u128;
525        let reshuffle_events = parse_shuffle_points(&track.shuffle_points).len() as u32;
526        let total_draws = track
527            .selections_count
528            .checked_mul(reshuffle_events.checked_add(1)?)?;
529        let count = checked_pow(choices, total_draws)?;
530        total = total.checked_mul(count)?;
531    }
532    Some(total)
533}
534
535fn count_legacy_track_combinations(settings: &PlaySettingsLegacy) -> Option<u128> {
536    let mut total: u128 = 1;
537    for track in &settings.tracks {
538        let choices = track.length.unwrap_or(0) as u128;
539        let count = checked_pow(choices, 1)?;
540        total = total.checked_mul(count)?;
541    }
542    Some(total)
543}
544
545fn checked_pow(base: u128, exp: u32) -> Option<u128> {
546    if exp == 0 {
547        return Some(1);
548    }
549    if base == 0 {
550        return Some(1);
551    }
552    let mut result: u128 = 1;
553    for _ in 0..exp {
554        result = result.checked_mul(base)?;
555    }
556    Some(result)
557}
558
559impl PathsTrack {
560    /// Create a new PathsTrack from a vector of file paths.
561    pub fn new_from_file_paths(file_paths: Vec<String>) -> Self {
562        PathsTrack {
563            file_paths,
564            level: 1.0,
565            pan: 0.0,
566            selections_count: 1,
567            shuffle_points: Vec::new(),
568        }
569    }
570}
571
572fn build_id_shuffle_schedule(
573    tracks: &[SettingsTrack],
574    info: &Info,
575) -> (Vec<ShuffleScheduleEntry>, f64) {
576    let mut shuffle_timestamps = BTreeSet::new();
577    let mut slot_candidates: Vec<Vec<u32>> = Vec::new();
578    let mut slot_points: Vec<HashSet<u64>> = Vec::new();
579    let mut current_ids: Vec<u32> = Vec::new();
580    let mut longest_duration = 0.0_f64;
581    shuffle_timestamps.insert(0);
582
583    for track in tracks {
584        if track.ids.is_empty() {
585            continue;
586        }
587        let selections = track.selections_count as usize;
588        if selections == 0 {
589            continue;
590        }
591        let points = parse_shuffle_points(&track.shuffle_points);
592        for point in &points {
593            shuffle_timestamps.insert(*point);
594        }
595        let point_set: HashSet<u64> = points.into_iter().collect();
596        for _ in 0..selections {
597            slot_candidates.push(track.ids.clone());
598            slot_points.push(point_set.clone());
599            let choice = random_id(&track.ids);
600            if let Some(duration) = info.get_duration(choice) {
601                longest_duration = longest_duration.max(duration);
602            }
603            current_ids.push(choice);
604        }
605    }
606
607    let mut schedule = Vec::new();
608    if current_ids.is_empty() {
609        return (schedule, longest_duration);
610    }
611
612    schedule.push(ShuffleScheduleEntry {
613        at_ms: 0,
614        sources: current_ids
615            .iter()
616            .copied()
617            .map(ShuffleSource::TrackId)
618            .collect(),
619    });
620
621    for timestamp in shuffle_timestamps.into_iter().filter(|point| *point > 0) {
622        for slot_index in 0..current_ids.len() {
623            if slot_points[slot_index].contains(&timestamp) {
624                current_ids[slot_index] = random_id(&slot_candidates[slot_index]);
625                if let Some(duration) = info.get_duration(current_ids[slot_index]) {
626                    longest_duration = longest_duration.max(duration);
627                }
628            }
629        }
630        schedule.push(ShuffleScheduleEntry {
631            at_ms: timestamp,
632            sources: current_ids
633                .iter()
634                .copied()
635                .map(ShuffleSource::TrackId)
636                .collect(),
637        });
638    }
639
640    (schedule, longest_duration)
641}
642
643fn build_paths_shuffle_schedule(
644    tracks: &[PathsTrack],
645    info: &Info,
646    dictionary: &[String],
647) -> (Vec<ShuffleScheduleEntry>, f64) {
648    let mut shuffle_timestamps = BTreeSet::new();
649    let mut slot_candidates: Vec<Vec<String>> = Vec::new();
650    let mut slot_points: Vec<HashSet<u64>> = Vec::new();
651    let mut current_paths: Vec<String> = Vec::new();
652    let mut longest_duration = 0.0_f64;
653    let dictionary_lookup: HashMap<&str, u32> = dictionary
654        .iter()
655        .enumerate()
656        .map(|(index, path)| (path.as_str(), index as u32))
657        .collect();
658    shuffle_timestamps.insert(0);
659
660    for track in tracks {
661        if track.file_paths.is_empty() {
662            continue;
663        }
664        let selections = track.selections_count as usize;
665        if selections == 0 {
666            continue;
667        }
668        let points = parse_shuffle_points(&track.shuffle_points);
669        for point in &points {
670            shuffle_timestamps.insert(*point);
671        }
672        let point_set: HashSet<u64> = points.into_iter().collect();
673        for _ in 0..selections {
674            slot_candidates.push(track.file_paths.clone());
675            slot_points.push(point_set.clone());
676            let choice = random_path(&track.file_paths);
677            if let Some(index) = dictionary_lookup.get(choice.as_str()).copied() {
678                if let Some(duration) = info.get_duration(index) {
679                    longest_duration = longest_duration.max(duration);
680                }
681            }
682            current_paths.push(choice);
683        }
684    }
685
686    let mut schedule = Vec::new();
687    if current_paths.is_empty() {
688        return (schedule, longest_duration);
689    }
690
691    schedule.push(ShuffleScheduleEntry {
692        at_ms: 0,
693        sources: current_paths
694            .iter()
695            .cloned()
696            .map(ShuffleSource::FilePath)
697            .collect(),
698    });
699
700    for timestamp in shuffle_timestamps.into_iter().filter(|point| *point > 0) {
701        for slot_index in 0..current_paths.len() {
702            if slot_points[slot_index].contains(&timestamp) {
703                current_paths[slot_index] = random_path(&slot_candidates[slot_index]);
704                if let Some(index) = dictionary_lookup
705                    .get(current_paths[slot_index].as_str())
706                    .copied()
707                {
708                    if let Some(duration) = info.get_duration(index) {
709                        longest_duration = longest_duration.max(duration);
710                    }
711                }
712            }
713        }
714        schedule.push(ShuffleScheduleEntry {
715            at_ms: timestamp,
716            sources: current_paths
717                .iter()
718                .cloned()
719                .map(ShuffleSource::FilePath)
720                .collect(),
721        });
722    }
723
724    (schedule, longest_duration)
725}
726
727fn parse_shuffle_points(points: &[String]) -> Vec<u64> {
728    let mut parsed = Vec::new();
729    for point in points {
730        match parse_timestamp_ms(point) {
731            Some(value) => parsed.push(value),
732            None => warn!("Invalid shuffle point timestamp: {}", point),
733        }
734    }
735    parsed.sort_unstable();
736    parsed.dedup();
737    parsed
738}
739
740fn parse_timestamp_ms(value: &str) -> Option<u64> {
741    let parts: Vec<&str> = value.trim().split(':').collect();
742    if parts.is_empty() || parts.len() > 3 {
743        return None;
744    }
745
746    let seconds_component = parts.last()?.parse::<f64>().ok()?;
747    if seconds_component.is_sign_negative() {
748        return None;
749    }
750
751    let minutes = if parts.len() >= 2 {
752        parts[parts.len() - 2].parse::<u64>().ok()?
753    } else {
754        0
755    };
756    let hours = if parts.len() == 3 {
757        parts[0].parse::<u64>().ok()?
758    } else {
759        0
760    };
761
762    let total_seconds = (hours as f64 * 3600.0) + (minutes as f64 * 60.0) + seconds_component;
763    if total_seconds.is_sign_negative() || !total_seconds.is_finite() {
764        return None;
765    }
766    Some((total_seconds * 1000.0).round() as u64)
767}
768
769fn seconds_to_ms(seconds: f64) -> u64 {
770    if !seconds.is_finite() || seconds <= 0.0 {
771        return 0;
772    }
773    (seconds * 1000.0).round() as u64
774}
775
776fn random_id(ids: &[u32]) -> u32 {
777    let random_index = rand::thread_rng().gen_range(0..ids.len());
778    ids[random_index]
779}
780
781fn random_path(paths: &[String]) -> String {
782    let random_index = rand::thread_rng().gen_range(0..paths.len());
783    paths[random_index].clone()
784}
785
786fn sources_to_track_ids(sources: &[ShuffleSource]) -> Vec<u32> {
787    sources
788        .iter()
789        .filter_map(|source| match source {
790            ShuffleSource::TrackId(track_id) => Some(*track_id),
791            ShuffleSource::FilePath(_) => None,
792        })
793        .collect()
794}
795
796fn sources_to_track_paths(sources: &[ShuffleSource]) -> Vec<String> {
797    sources
798        .iter()
799        .filter_map(|source| match source {
800            ShuffleSource::TrackId(_) => None,
801            ShuffleSource::FilePath(path) => Some(path.clone()),
802        })
803        .collect()
804}
805
806fn collect_legacy_tracks(
807    settings: &PlaySettingsLegacy,
808    track_index_array: &mut Vec<u32>,
809    longest_duration: &mut f64,
810    info: &Info,
811    total_duration: &mut f64,
812) {
813    for track in &settings.tracks {
814        let (Some(starting_index), Some(length)) = (track.starting_index, track.length) else {
815            continue;
816        };
817        let starting_index = starting_index + 1;
818        let index = rand::thread_rng().gen_range(starting_index..(starting_index + length));
819        if let Some(track_duration) = info.get_duration(index) {
820            if track_duration > *longest_duration {
821                *longest_duration = track_duration;
822                *total_duration = *longest_duration;
823            }
824        }
825        track_index_array.push(index);
826    }
827}
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832
833    fn settings_track(
834        ids: Vec<u32>,
835        selections_count: u32,
836        shuffle_points: Vec<&str>,
837    ) -> SettingsTrack {
838        SettingsTrack {
839            level: 1.0,
840            pan: 0.0,
841            ids,
842            name: "Track".to_string(),
843            safe_name: "Track".to_string(),
844            selections_count,
845            shuffle_points: shuffle_points.into_iter().map(|v| v.to_string()).collect(),
846        }
847    }
848
849    #[test]
850    fn count_settings_combinations_without_shuffle_points() {
851        let tracks = vec![settings_track(vec![1, 2, 3], 2, vec![])];
852        assert_eq!(count_settings_track_combinations(&tracks), Some(9));
853    }
854
855    #[test]
856    fn count_settings_combinations_with_shuffle_points() {
857        let tracks = vec![settings_track(vec![1, 2, 3], 2, vec!["0:30", "1:00"])];
858        assert_eq!(count_settings_track_combinations(&tracks), Some(729));
859    }
860
861    #[test]
862    fn count_settings_combinations_uses_unique_valid_shuffle_points() {
863        let tracks = vec![settings_track(
864            vec![1, 2, 3, 4],
865            1,
866            vec!["1:00", "bad", "1:00"],
867        )];
868        assert_eq!(count_settings_track_combinations(&tracks), Some(16));
869    }
870
871    #[test]
872    fn count_paths_combinations_with_shuffle_points() {
873        let tracks = vec![PathsTrack {
874            file_paths: vec!["a.wav".to_string(), "b.wav".to_string()],
875            level: 1.0,
876            pan: 0.0,
877            selections_count: 1,
878            shuffle_points: vec!["0:15".to_string(), "0:45".to_string()],
879        }];
880        assert_eq!(count_paths_track_combinations(&tracks), Some(8));
881    }
882}