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 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 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 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 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#[derive(Debug, Clone)]
494pub struct PathsTrack {
495 pub file_paths: Vec<String>,
497 pub level: f32,
499 pub pan: f32,
501 pub selections_count: u32,
503 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 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(×tamp) {
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(×tamp) {
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}