1use 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#[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 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 pub fn new_from_file_paths(file_paths: Vec<PathsTrack>) -> Self {
81 let mut file_paths_dictionary = Vec::new();
82 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 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 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 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 pub fn get_impulse_response_spec(&self) -> Option<ImpulseResponseSpec> {
273 self.impulse_response_spec.clone()
274 }
275
276 pub fn get_impulse_response_tail_db(&self) -> Option<f32> {
278 self.impulse_response_tail_db
279 }
280
281 pub fn get_container_path(&self) -> Option<String> {
283 self.file_path.clone()
284 }
285
286 pub fn set_impulse_response_spec(&mut self, spec: ImpulseResponseSpec) {
288 self.impulse_response_spec = Some(spec);
289 }
290
291 pub fn set_impulse_response_tail_db(&mut self, tail_db: f32) {
293 self.impulse_response_tail_db = Some(tail_db);
294 }
295
296 pub fn get_keys(&self) -> Vec<u32> {
298 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 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 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 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 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 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 pub fn get_track_mix_settings(&self) -> HashMap<u16, (f32, f32)> {
425 let mut settings = HashMap::new();
426
427 if let Some(file_paths) = self.file_paths.as_ref() {
428 let mut slot_index: u16 = 0;
429 for track in file_paths {
430 let selections = track.selections_count.max(1);
431 for _ in 0..selections {
432 settings.insert(slot_index, (track.level, track.pan));
433 slot_index = slot_index.saturating_add(1);
434 }
435 }
436 return settings;
437 }
438
439 let tracks = match self.play_settings.as_ref() {
440 Some(PlaySettingsFile::V1(file)) => Some(&file.settings.inner().tracks),
441 Some(PlaySettingsFile::V2(file)) => Some(&file.settings.inner().tracks),
442 Some(PlaySettingsFile::V3(file)) => Some(&file.settings.inner().tracks),
443 _ => None,
444 };
445
446 if let Some(tracks) = tracks {
447 let mut slot_index: u16 = 0;
448 for track in tracks {
449 let selections = track.selections_count.max(1);
450 for _ in 0..selections {
451 settings.insert(slot_index, (track.level, track.pan));
452 slot_index = slot_index.saturating_add(1);
453 }
454 }
455 }
456
457 settings
458 }
459
460 pub fn set_slot_mix_settings(&mut self, slot_index: usize, level: f32, pan: f32) -> bool {
464 let level = sanitize_level(level);
465 let pan = sanitize_pan(pan);
466
467 if let Some(file_paths) = self.file_paths.as_mut() {
468 if let Some(track) = get_paths_track_for_slot_mut(file_paths, slot_index) {
469 track.level = level;
470 track.pan = pan;
471 return true;
472 }
473 return false;
474 }
475
476 match self.play_settings.as_mut() {
477 Some(PlaySettingsFile::V1(file)) => update_settings_track_slot(
478 file.settings.inner_mut().tracks.as_mut_slice(),
479 slot_index,
480 level,
481 pan,
482 ),
483 Some(PlaySettingsFile::V2(file)) => update_settings_track_slot(
484 file.settings.inner_mut().tracks.as_mut_slice(),
485 slot_index,
486 level,
487 pan,
488 ),
489 Some(PlaySettingsFile::V3(file)) => update_settings_track_slot(
490 file.settings.inner_mut().tracks.as_mut_slice(),
491 slot_index,
492 level,
493 pan,
494 ),
495 _ => false,
496 }
497 }
498
499 pub fn linked_slot_indices(&self, slot_index: usize) -> Option<Vec<usize>> {
501 if let Some(file_paths) = self.file_paths.as_ref() {
502 return linked_paths_slots(file_paths, slot_index);
503 }
504
505 let tracks = match self.play_settings.as_ref() {
506 Some(PlaySettingsFile::V1(file)) => Some(&file.settings.inner().tracks),
507 Some(PlaySettingsFile::V2(file)) => Some(&file.settings.inner().tracks),
508 Some(PlaySettingsFile::V3(file)) => Some(&file.settings.inner().tracks),
509 _ => None,
510 }?;
511
512 linked_settings_slots(tracks, slot_index)
513 }
514
515 pub fn get_length(&self) -> usize {
517 if let Some(track_paths) = &self.track_paths {
518 return track_paths.len();
519 }
520
521 if let Some(file_paths) = &self.file_paths {
522 return file_paths.len();
523 }
524
525 if let Some(track_ids) = &self.track_ids {
526 return track_ids.len();
527 }
528
529 0
530 }
531
532 pub fn count_possible_combinations(&self) -> Option<u128> {
534 if let Some(file_paths) = &self.file_paths {
535 return count_paths_track_combinations(file_paths);
536 }
537
538 let play_settings = self.play_settings.as_ref()?;
539 match play_settings {
540 PlaySettingsFile::Legacy(file) => {
541 count_legacy_track_combinations(file.settings.inner())
542 }
543 PlaySettingsFile::V1(file) => {
544 count_settings_track_combinations(&file.settings.inner().tracks)
545 }
546 PlaySettingsFile::V2(file) => {
547 count_settings_track_combinations(&file.settings.inner().tracks)
548 }
549 PlaySettingsFile::V3(file) => {
550 count_settings_track_combinations(&file.settings.inner().tracks)
551 }
552 PlaySettingsFile::Unknown { .. } => None,
553 }
554 }
555
556 pub fn get_file_paths_dictionary(&self) -> Vec<String> {
558 match &self.file_paths_dictionary {
559 Some(dictionary) => dictionary.to_vec(),
560 None => Vec::new(),
561 }
562 }
563}
564
565#[derive(Debug, Clone)]
567pub struct PathsTrack {
568 pub file_paths: Vec<String>,
570 pub level: f32,
572 pub pan: f32,
574 pub selections_count: u32,
576 pub shuffle_points: Vec<String>,
578}
579
580fn count_settings_track_combinations(tracks: &[SettingsTrack]) -> Option<u128> {
581 let mut total: u128 = 1;
582 for track in tracks {
583 let choices = track.ids.len() as u128;
584 let reshuffle_events = parse_shuffle_points(&track.shuffle_points).len() as u32;
585 let total_draws = track
586 .selections_count
587 .checked_mul(reshuffle_events.checked_add(1)?)?;
588 let count = checked_pow(choices, total_draws)?;
589 total = total.checked_mul(count)?;
590 }
591 Some(total)
592}
593
594fn count_paths_track_combinations(tracks: &[PathsTrack]) -> Option<u128> {
595 let mut total: u128 = 1;
596 for track in tracks {
597 let choices = track.file_paths.len() as u128;
598 let reshuffle_events = parse_shuffle_points(&track.shuffle_points).len() as u32;
599 let total_draws = track
600 .selections_count
601 .checked_mul(reshuffle_events.checked_add(1)?)?;
602 let count = checked_pow(choices, total_draws)?;
603 total = total.checked_mul(count)?;
604 }
605 Some(total)
606}
607
608fn count_legacy_track_combinations(settings: &PlaySettingsLegacy) -> Option<u128> {
609 let mut total: u128 = 1;
610 for track in &settings.tracks {
611 let choices = track.length.unwrap_or(0) as u128;
612 let count = checked_pow(choices, 1)?;
613 total = total.checked_mul(count)?;
614 }
615 Some(total)
616}
617
618fn checked_pow(base: u128, exp: u32) -> Option<u128> {
619 if exp == 0 {
620 return Some(1);
621 }
622 if base == 0 {
623 return Some(1);
624 }
625 let mut result: u128 = 1;
626 for _ in 0..exp {
627 result = result.checked_mul(base)?;
628 }
629 Some(result)
630}
631
632impl PathsTrack {
633 pub fn new_from_file_paths(file_paths: Vec<String>) -> Self {
635 PathsTrack {
636 file_paths,
637 level: 1.0,
638 pan: 0.0,
639 selections_count: 1,
640 shuffle_points: Vec::new(),
641 }
642 }
643}
644
645fn build_id_shuffle_schedule(
646 tracks: &[SettingsTrack],
647 info: &Info,
648) -> (Vec<ShuffleScheduleEntry>, f64) {
649 let mut shuffle_timestamps = BTreeSet::new();
650 let mut slot_candidates: Vec<Vec<u32>> = Vec::new();
651 let mut slot_points: Vec<HashSet<u64>> = Vec::new();
652 let mut current_ids: Vec<u32> = Vec::new();
653 let mut longest_duration = 0.0_f64;
654 shuffle_timestamps.insert(0);
655
656 for track in tracks {
657 if track.ids.is_empty() {
658 continue;
659 }
660 let selections = track.selections_count as usize;
661 if selections == 0 {
662 continue;
663 }
664 let points = parse_shuffle_points(&track.shuffle_points);
665 for point in &points {
666 shuffle_timestamps.insert(*point);
667 }
668 let point_set: HashSet<u64> = points.into_iter().collect();
669 for _ in 0..selections {
670 slot_candidates.push(track.ids.clone());
671 slot_points.push(point_set.clone());
672 let choice = random_id(&track.ids);
673 if let Some(duration) = info.get_duration(choice) {
674 longest_duration = longest_duration.max(duration);
675 }
676 current_ids.push(choice);
677 }
678 }
679
680 let mut schedule = Vec::new();
681 if current_ids.is_empty() {
682 return (schedule, longest_duration);
683 }
684
685 schedule.push(ShuffleScheduleEntry {
686 at_ms: 0,
687 sources: current_ids
688 .iter()
689 .copied()
690 .map(ShuffleSource::TrackId)
691 .collect(),
692 });
693
694 for timestamp in shuffle_timestamps.into_iter().filter(|point| *point > 0) {
695 for slot_index in 0..current_ids.len() {
696 if slot_points[slot_index].contains(×tamp) {
697 current_ids[slot_index] = random_id(&slot_candidates[slot_index]);
698 if let Some(duration) = info.get_duration(current_ids[slot_index]) {
699 longest_duration = longest_duration.max(duration);
700 }
701 }
702 }
703 schedule.push(ShuffleScheduleEntry {
704 at_ms: timestamp,
705 sources: current_ids
706 .iter()
707 .copied()
708 .map(ShuffleSource::TrackId)
709 .collect(),
710 });
711 }
712
713 (schedule, longest_duration)
714}
715
716fn build_paths_shuffle_schedule(
717 tracks: &[PathsTrack],
718 info: &Info,
719 dictionary: &[String],
720) -> (Vec<ShuffleScheduleEntry>, f64) {
721 let mut shuffle_timestamps = BTreeSet::new();
722 let mut slot_candidates: Vec<Vec<String>> = Vec::new();
723 let mut slot_points: Vec<HashSet<u64>> = Vec::new();
724 let mut current_paths: Vec<String> = Vec::new();
725 let mut longest_duration = 0.0_f64;
726 let dictionary_lookup: HashMap<&str, u32> = dictionary
727 .iter()
728 .enumerate()
729 .map(|(index, path)| (path.as_str(), index as u32))
730 .collect();
731 shuffle_timestamps.insert(0);
732
733 for track in tracks {
734 if track.file_paths.is_empty() {
735 continue;
736 }
737 let selections = track.selections_count as usize;
738 if selections == 0 {
739 continue;
740 }
741 let points = parse_shuffle_points(&track.shuffle_points);
742 for point in &points {
743 shuffle_timestamps.insert(*point);
744 }
745 let point_set: HashSet<u64> = points.into_iter().collect();
746 for _ in 0..selections {
747 slot_candidates.push(track.file_paths.clone());
748 slot_points.push(point_set.clone());
749 let choice = random_path(&track.file_paths);
750 if let Some(index) = dictionary_lookup.get(choice.as_str()).copied() {
751 if let Some(duration) = info.get_duration(index) {
752 longest_duration = longest_duration.max(duration);
753 }
754 }
755 current_paths.push(choice);
756 }
757 }
758
759 let mut schedule = Vec::new();
760 if current_paths.is_empty() {
761 return (schedule, longest_duration);
762 }
763
764 schedule.push(ShuffleScheduleEntry {
765 at_ms: 0,
766 sources: current_paths
767 .iter()
768 .cloned()
769 .map(ShuffleSource::FilePath)
770 .collect(),
771 });
772
773 for timestamp in shuffle_timestamps.into_iter().filter(|point| *point > 0) {
774 for slot_index in 0..current_paths.len() {
775 if slot_points[slot_index].contains(×tamp) {
776 current_paths[slot_index] = random_path(&slot_candidates[slot_index]);
777 if let Some(index) = dictionary_lookup
778 .get(current_paths[slot_index].as_str())
779 .copied()
780 {
781 if let Some(duration) = info.get_duration(index) {
782 longest_duration = longest_duration.max(duration);
783 }
784 }
785 }
786 }
787 schedule.push(ShuffleScheduleEntry {
788 at_ms: timestamp,
789 sources: current_paths
790 .iter()
791 .cloned()
792 .map(ShuffleSource::FilePath)
793 .collect(),
794 });
795 }
796
797 (schedule, longest_duration)
798}
799
800fn parse_shuffle_points(points: &[String]) -> Vec<u64> {
801 let mut parsed = Vec::new();
802 for point in points {
803 match parse_timestamp_ms(point) {
804 Some(value) => parsed.push(value),
805 None => warn!("Invalid shuffle point timestamp: {}", point),
806 }
807 }
808 parsed.sort_unstable();
809 parsed.dedup();
810 parsed
811}
812
813fn parse_timestamp_ms(value: &str) -> Option<u64> {
814 let parts: Vec<&str> = value.trim().split(':').collect();
815 if parts.is_empty() || parts.len() > 3 {
816 return None;
817 }
818
819 let seconds_component = parts.last()?.parse::<f64>().ok()?;
820 if seconds_component.is_sign_negative() {
821 return None;
822 }
823
824 let minutes = if parts.len() >= 2 {
825 parts[parts.len() - 2].parse::<u64>().ok()?
826 } else {
827 0
828 };
829 let hours = if parts.len() == 3 {
830 parts[0].parse::<u64>().ok()?
831 } else {
832 0
833 };
834
835 let total_seconds = (hours as f64 * 3600.0) + (minutes as f64 * 60.0) + seconds_component;
836 if total_seconds.is_sign_negative() || !total_seconds.is_finite() {
837 return None;
838 }
839 Some((total_seconds * 1000.0).round() as u64)
840}
841
842fn seconds_to_ms(seconds: f64) -> u64 {
843 if !seconds.is_finite() || seconds <= 0.0 {
844 return 0;
845 }
846 (seconds * 1000.0).round() as u64
847}
848
849fn random_id(ids: &[u32]) -> u32 {
850 let random_index = rand::thread_rng().gen_range(0..ids.len());
851 ids[random_index]
852}
853
854fn random_path(paths: &[String]) -> String {
855 let random_index = rand::thread_rng().gen_range(0..paths.len());
856 paths[random_index].clone()
857}
858
859fn sanitize_level(level: f32) -> f32 {
860 if level.is_finite() {
861 level.max(0.0)
862 } else {
863 1.0
864 }
865}
866
867fn sanitize_pan(pan: f32) -> f32 {
868 if pan.is_finite() {
869 pan.clamp(-1.0, 1.0)
870 } else {
871 0.0
872 }
873}
874
875fn get_paths_track_for_slot_mut(
876 tracks: &mut [PathsTrack],
877 slot_index: usize,
878) -> Option<&mut PathsTrack> {
879 let mut slot_cursor = 0usize;
880 for track in tracks.iter_mut() {
881 let span = track.selections_count.max(1) as usize;
882 if slot_index < slot_cursor + span {
883 return Some(track);
884 }
885 slot_cursor += span;
886 }
887 None
888}
889
890fn update_settings_track_slot(
891 tracks: &mut [SettingsTrack],
892 slot_index: usize,
893 level: f32,
894 pan: f32,
895) -> bool {
896 let mut slot_cursor = 0usize;
897 for track in tracks.iter_mut() {
898 let span = track.selections_count.max(1) as usize;
899 if slot_index < slot_cursor + span {
900 track.level = level;
901 track.pan = pan;
902 return true;
903 }
904 slot_cursor += span;
905 }
906 false
907}
908
909fn linked_paths_slots(tracks: &[PathsTrack], slot_index: usize) -> Option<Vec<usize>> {
910 let mut slot_cursor = 0usize;
911 for track in tracks {
912 let span = track.selections_count.max(1) as usize;
913 if slot_index < slot_cursor + span {
914 return Some((slot_cursor..(slot_cursor + span)).collect());
915 }
916 slot_cursor += span;
917 }
918 None
919}
920
921fn linked_settings_slots(tracks: &[SettingsTrack], slot_index: usize) -> Option<Vec<usize>> {
922 let mut slot_cursor = 0usize;
923 for track in tracks {
924 let span = track.selections_count.max(1) as usize;
925 if slot_index < slot_cursor + span {
926 return Some((slot_cursor..(slot_cursor + span)).collect());
927 }
928 slot_cursor += span;
929 }
930 None
931}
932
933fn sources_to_track_ids(sources: &[ShuffleSource]) -> Vec<u32> {
934 sources
935 .iter()
936 .filter_map(|source| match source {
937 ShuffleSource::TrackId(track_id) => Some(*track_id),
938 ShuffleSource::FilePath(_) => None,
939 })
940 .collect()
941}
942
943fn sources_to_track_paths(sources: &[ShuffleSource]) -> Vec<String> {
944 sources
945 .iter()
946 .filter_map(|source| match source {
947 ShuffleSource::TrackId(_) => None,
948 ShuffleSource::FilePath(path) => Some(path.clone()),
949 })
950 .collect()
951}
952
953fn collect_legacy_tracks(
954 settings: &PlaySettingsLegacy,
955 track_index_array: &mut Vec<u32>,
956 longest_duration: &mut f64,
957 info: &Info,
958 total_duration: &mut f64,
959) {
960 for track in &settings.tracks {
961 let (Some(starting_index), Some(length)) = (track.starting_index, track.length) else {
962 continue;
963 };
964 let starting_index = starting_index + 1;
965 let index = rand::thread_rng().gen_range(starting_index..(starting_index + length));
966 if let Some(track_duration) = info.get_duration(index) {
967 if track_duration > *longest_duration {
968 *longest_duration = track_duration;
969 *total_duration = *longest_duration;
970 }
971 }
972 track_index_array.push(index);
973 }
974}
975
976#[cfg(test)]
977mod tests {
978 use super::*;
979
980 fn test_info() -> Info {
981 Info {
982 file_paths: Vec::new(),
983 duration_map: HashMap::new(),
984 channels: 2,
985 sample_rate: 48_000,
986 bits_per_sample: 16,
987 }
988 }
989
990 fn settings_track(
991 ids: Vec<u32>,
992 selections_count: u32,
993 shuffle_points: Vec<&str>,
994 ) -> SettingsTrack {
995 SettingsTrack {
996 level: 1.0,
997 pan: 0.0,
998 ids,
999 name: "Track".to_string(),
1000 safe_name: "Track".to_string(),
1001 selections_count,
1002 shuffle_points: shuffle_points.into_iter().map(|v| v.to_string()).collect(),
1003 }
1004 }
1005
1006 #[test]
1007 fn count_settings_combinations_without_shuffle_points() {
1008 let tracks = vec![settings_track(vec![1, 2, 3], 2, vec![])];
1009 assert_eq!(count_settings_track_combinations(&tracks), Some(9));
1010 }
1011
1012 #[test]
1013 fn count_settings_combinations_with_shuffle_points() {
1014 let tracks = vec![settings_track(vec![1, 2, 3], 2, vec!["0:30", "1:00"])];
1015 assert_eq!(count_settings_track_combinations(&tracks), Some(729));
1016 }
1017
1018 #[test]
1019 fn count_settings_combinations_uses_unique_valid_shuffle_points() {
1020 let tracks = vec![settings_track(
1021 vec![1, 2, 3, 4],
1022 1,
1023 vec!["1:00", "bad", "1:00"],
1024 )];
1025 assert_eq!(count_settings_track_combinations(&tracks), Some(16));
1026 }
1027
1028 #[test]
1029 fn count_paths_combinations_with_shuffle_points() {
1030 let tracks = vec![PathsTrack {
1031 file_paths: vec!["a.wav".to_string(), "b.wav".to_string()],
1032 level: 1.0,
1033 pan: 0.0,
1034 selections_count: 1,
1035 shuffle_points: vec!["0:15".to_string(), "0:45".to_string()],
1036 }];
1037 assert_eq!(count_paths_track_combinations(&tracks), Some(8));
1038 }
1039
1040 #[test]
1041 fn get_track_mix_settings_repeats_by_selections_count_for_paths_tracks() {
1042 let prot = Prot {
1043 info: test_info(),
1044 file_path: None,
1045 file_paths: Some(vec![PathsTrack {
1046 file_paths: vec!["a.wav".to_string()],
1047 level: 0.7,
1048 pan: -0.3,
1049 selections_count: 2,
1050 shuffle_points: vec![],
1051 }]),
1052 file_paths_dictionary: Some(vec!["a.wav".to_string()]),
1053 track_ids: None,
1054 track_paths: None,
1055 duration: 0.0,
1056 shuffle_schedule: Vec::new(),
1057 play_settings: None,
1058 impulse_response_spec: None,
1059 impulse_response_tail_db: None,
1060 effects: None,
1061 };
1062
1063 let settings = prot.get_track_mix_settings();
1064 assert_eq!(settings.get(&0), Some(&(0.7, -0.3)));
1065 assert_eq!(settings.get(&1), Some(&(0.7, -0.3)));
1066 }
1067
1068 #[test]
1069 fn set_slot_mix_settings_updates_paths_track() {
1070 let mut prot = Prot {
1071 info: test_info(),
1072 file_path: None,
1073 file_paths: Some(vec![PathsTrack {
1074 file_paths: vec!["a.wav".to_string()],
1075 level: 1.0,
1076 pan: 0.0,
1077 selections_count: 2,
1078 shuffle_points: vec![],
1079 }]),
1080 file_paths_dictionary: Some(vec!["a.wav".to_string()]),
1081 track_ids: None,
1082 track_paths: None,
1083 duration: 0.0,
1084 shuffle_schedule: Vec::new(),
1085 play_settings: None,
1086 impulse_response_spec: None,
1087 impulse_response_tail_db: None,
1088 effects: None,
1089 };
1090
1091 assert!(prot.set_slot_mix_settings(1, 0.4, 0.6));
1092 let settings = prot.get_track_mix_settings();
1093 assert_eq!(settings.get(&0), Some(&(0.4, 0.6)));
1094 assert_eq!(settings.get(&1), Some(&(0.4, 0.6)));
1095 }
1096
1097 #[test]
1098 fn get_track_mix_settings_includes_v3_tracks() {
1099 use crate::container::play_settings::{
1100 PlaySettingsContainer, PlaySettingsV3, PlaySettingsV3File,
1101 };
1102
1103 let play_settings = PlaySettingsFile::V3(PlaySettingsV3File {
1104 settings: PlaySettingsContainer::Flat(PlaySettingsV3 {
1105 effects: Vec::new(),
1106 tracks: vec![SettingsTrack {
1107 level: 0.25,
1108 pan: 0.2,
1109 ids: vec![1],
1110 name: "Track".to_string(),
1111 safe_name: "track".to_string(),
1112 selections_count: 2,
1113 shuffle_points: vec![],
1114 }],
1115 }),
1116 });
1117
1118 let prot = Prot {
1119 info: test_info(),
1120 file_path: Some("dummy.prot".to_string()),
1121 file_paths: None,
1122 file_paths_dictionary: None,
1123 track_ids: Some(vec![1, 1]),
1124 track_paths: None,
1125 duration: 0.0,
1126 shuffle_schedule: Vec::new(),
1127 play_settings: Some(play_settings),
1128 impulse_response_spec: None,
1129 impulse_response_tail_db: None,
1130 effects: None,
1131 };
1132
1133 let settings = prot.get_track_mix_settings();
1134 assert_eq!(settings.get(&0), Some(&(0.25, 0.2)));
1135 assert_eq!(settings.get(&1), Some(&(0.25, 0.2)));
1136 }
1137
1138 #[test]
1139 fn linked_slot_indices_returns_all_slots_for_same_track() {
1140 let prot = Prot {
1141 info: test_info(),
1142 file_path: None,
1143 file_paths: Some(vec![
1144 PathsTrack {
1145 file_paths: vec!["a.wav".to_string()],
1146 level: 1.0,
1147 pan: 0.0,
1148 selections_count: 2,
1149 shuffle_points: vec![],
1150 },
1151 PathsTrack {
1152 file_paths: vec!["b.wav".to_string()],
1153 level: 1.0,
1154 pan: 0.0,
1155 selections_count: 1,
1156 shuffle_points: vec![],
1157 },
1158 ]),
1159 file_paths_dictionary: Some(vec!["a.wav".to_string(), "b.wav".to_string()]),
1160 track_ids: None,
1161 track_paths: None,
1162 duration: 0.0,
1163 shuffle_schedule: Vec::new(),
1164 play_settings: None,
1165 impulse_response_spec: None,
1166 impulse_response_tail_db: None,
1167 effects: None,
1168 };
1169
1170 assert_eq!(prot.linked_slot_indices(0), Some(vec![0, 1]));
1171 assert_eq!(prot.linked_slot_indices(1), Some(vec![0, 1]));
1172 assert_eq!(prot.linked_slot_indices(2), Some(vec![2]));
1173 assert_eq!(prot.linked_slot_indices(3), None);
1174 }
1175}