1use std::collections::HashMap;
12use std::time::{Duration, Instant};
13
14#[derive(Debug, Clone, PartialEq)]
24pub struct TempoMarking {
25 bpm: f64,
27 feel: f64,
29 label: Option<String>,
31}
32
33impl TempoMarking {
34 pub fn new(bpm: f64, feel: f64) -> Self {
36 Self {
37 bpm: bpm.clamp(1.0, 600.0),
38 feel: feel.clamp(0.0, 1.0),
39 label: None,
40 }
41 }
42
43 pub fn labeled(bpm: f64, feel: f64, label: impl Into<String>) -> Self {
45 Self {
46 bpm: bpm.clamp(1.0, 600.0),
47 feel: feel.clamp(0.0, 1.0),
48 label: Some(label.into()),
49 }
50 }
51
52 pub fn bpm(&self) -> f64 {
54 self.bpm
55 }
56
57 pub fn feel(&self) -> f64 {
59 self.feel
60 }
61
62 pub fn beat_duration(&self) -> Duration {
64 Duration::from_secs_f64(60.0 / self.bpm)
65 }
66
67 pub fn beat_with_feel(&self, variation: f64) -> Duration {
70 let base_secs = 60.0 / self.bpm;
71 let adjusted = base_secs * (1.0 + variation * self.feel * 0.1);
72 Duration::from_secs_f64(adjusted.max(0.001))
73 }
74
75 pub fn label(&self) -> Option<&str> {
77 self.label.as_deref()
78 }
79
80 pub fn largo() -> Self {
82 Self::labeled(50.0, 0.3, "Largo")
83 }
84
85 pub fn andante() -> Self {
87 Self::labeled(80.0, 0.2, "Andante")
88 }
89
90 pub fn allegro() -> Self {
92 Self::labeled(140.0, 0.15, "Allegro")
93 }
94
95 pub fn presto() -> Self {
97 Self::labeled(180.0, 0.1, "Presto")
98 }
99}
100
101#[derive(Debug, Clone, PartialEq)]
111pub struct RubatoProfile {
112 max_compress: f64,
114 max_stretch: f64,
116 transition_rate: f64,
118 name: String,
120}
121
122impl RubatoProfile {
123 pub fn new(name: impl Into<String>, max_compress: f64, max_stretch: f64, transition_rate: f64) -> Self {
125 Self {
126 name: name.into(),
127 max_compress: max_compress.clamp(0.0, 1.0),
128 max_stretch: max_stretch.clamp(0.0, 1.0),
129 transition_rate: transition_rate.clamp(0.0, 10.0),
130 }
131 }
132
133 pub fn strict() -> Self {
135 Self::new("strict", 0.05, 0.05, 0.1)
136 }
137
138 pub fn moderate() -> Self {
140 Self::new("moderate", 0.2, 0.25, 0.5)
141 }
142
143 pub fn expressive() -> Self {
145 Self::new("expressive", 0.4, 0.5, 1.0)
146 }
147
148 pub fn apply(&self, base_bpm: f64, factor: f64) -> f64 {
152 let clamped = factor.clamp(-1.0, 1.0);
153 if clamped >= 0.0 {
154 base_bpm * (1.0 + clamped * self.max_compress)
155 } else {
156 base_bpm * (1.0 - clamped.abs() * self.max_stretch)
157 }
158 }
159
160 pub fn min_bpm(&self, base_bpm: f64) -> f64 {
162 base_bpm * (1.0 - self.max_stretch)
163 }
164
165 pub fn max_bpm(&self, base_bpm: f64) -> f64 {
167 base_bpm * (1.0 + self.max_compress)
168 }
169
170 pub fn is_within_range(&self, base_bpm: f64, actual_bpm: f64) -> bool {
172 actual_bpm >= self.min_bpm(base_bpm) && actual_bpm <= self.max_bpm(base_bpm)
173 }
174
175 pub fn name(&self) -> &str {
176 &self.name
177 }
178 pub fn max_compress(&self) -> f64 {
179 self.max_compress
180 }
181 pub fn max_stretch(&self) -> f64 {
182 self.max_stretch
183 }
184 pub fn transition_rate(&self) -> f64 {
185 self.transition_rate
186 }
187}
188
189#[derive(Debug, Clone, Copy, PartialEq)]
195pub enum CurveShape {
196 Accelerando,
198 Ritardando,
200 Rubato,
202 Flat,
204}
205
206#[derive(Debug, Clone)]
211pub struct TempoCurve {
212 start_bpm: f64,
214 end_bpm: f64,
216 duration_beats: u32,
218 shape: CurveShape,
220 label: Option<String>,
222}
223
224impl TempoCurve {
225 pub fn new(start_bpm: f64, end_bpm: f64, duration_beats: u32) -> Self {
227 let shape = if (start_bpm - end_bpm).abs() < 0.01 {
228 CurveShape::Flat
229 } else if end_bpm > start_bpm {
230 CurveShape::Accelerando
231 } else {
232 CurveShape::Ritardando
233 };
234 Self {
235 start_bpm,
236 end_bpm,
237 duration_beats: duration_beats.max(1),
238 shape,
239 label: None,
240 }
241 }
242
243 pub fn rubato(base_bpm: f64, fluctuation: f64, duration_beats: u32) -> Self {
245 Self {
246 start_bpm: base_bpm,
247 end_bpm: base_bpm + fluctuation,
248 duration_beats: duration_beats.max(1),
249 shape: CurveShape::Rubato,
250 label: None,
251 }
252 }
253
254 pub fn with_label(mut self, label: impl Into<String>) -> Self {
256 self.label = Some(label.into());
257 self
258 }
259
260 pub fn bpm_at_beat(&self, beat: f64) -> f64 {
262 let t = (beat / self.duration_beats as f64).clamp(0.0, 1.0);
263 self.start_bpm + (self.end_bpm - self.start_bpm) * t
264 }
265
266 pub fn bpm_at_position(&self, pos: f64) -> f64 {
269 let t = pos.clamp(0.0, 1.0);
270 let eased = if self.shape == CurveShape::Flat {
272 t
273 } else {
274 3.0 * t * t - 2.0 * t * t * t
275 };
276 self.start_bpm + (self.end_bpm - self.start_bpm) * eased
277 }
278
279 pub fn shape(&self) -> CurveShape {
281 self.shape
282 }
283
284 pub fn start_bpm(&self) -> f64 {
286 self.start_bpm
287 }
288
289 pub fn end_bpm(&self) -> f64 {
291 self.end_bpm
292 }
293
294 pub fn duration_beats(&self) -> u32 {
296 self.duration_beats
297 }
298
299 pub fn label(&self) -> Option<&str> {
301 self.label.as_deref()
302 }
303
304 pub fn delta(&self) -> f64 {
306 self.end_bpm - self.start_bpm
307 }
308
309 pub fn is_accelerando(&self) -> bool {
311 self.shape == CurveShape::Accelerando
312 }
313
314 pub fn is_ritardando(&self) -> bool {
316 self.shape == CurveShape::Ritardando
317 }
318}
319
320#[derive(Debug, Clone)]
329pub struct TempoFollower {
330 leader_id: String,
332 observed_beats: Vec<Instant>,
334 window_size: usize,
336 smoothing: f64,
338 estimated_bpm: Option<f64>,
340}
341
342impl TempoFollower {
343 pub fn new(leader_id: impl Into<String>) -> Self {
345 Self {
346 leader_id: leader_id.into(),
347 observed_beats: Vec::new(),
348 window_size: 16,
349 smoothing: 0.3,
350 estimated_bpm: None,
351 }
352 }
353
354 pub fn with_window_size(mut self, size: usize) -> Self {
356 self.window_size = size.max(2);
357 self
358 }
359
360 pub fn with_smoothing(mut self, smoothing: f64) -> Self {
362 self.smoothing = smoothing.clamp(0.0, 1.0);
363 self
364 }
365
366 pub fn observe_beat(&mut self) {
368 let now = Instant::now();
369 self.observed_beats.push(now);
370 if self.observed_beats.len() > self.window_size {
371 self.observed_beats.remove(0);
372 }
373 self.recalculate();
374 }
375
376 pub fn observe_beat_at(&mut self, time: Instant) {
378 self.observed_beats.push(time);
379 if self.observed_beats.len() > self.window_size {
380 self.observed_beats.remove(0);
381 }
382 self.recalculate();
383 }
384
385 fn recalculate(&mut self) {
386 if self.observed_beats.len() < 2 {
387 return;
388 }
389 let first = self.observed_beats[0];
390 let last = *self.observed_beats.last().unwrap();
391 let elapsed = last.duration_since(first).as_secs_f64();
392 if elapsed <= 0.0 {
393 return;
394 }
395 let intervals = self.observed_beats.len() - 1;
396 let raw_bpm = (intervals as f64 / elapsed) * 60.0;
397 self.estimated_bpm = Some(match self.estimated_bpm {
398 Some(prev) => prev + self.smoothing * (raw_bpm - prev),
399 None => raw_bpm,
400 });
401 }
402
403 pub fn estimated_bpm(&self) -> Option<f64> {
405 self.estimated_bpm
406 }
407
408 pub fn leader_id(&self) -> &str {
410 &self.leader_id
411 }
412
413 pub fn observed_count(&self) -> usize {
415 self.observed_beats.len()
416 }
417
418 pub fn sync_offset(&self, our_bpm: f64) -> Option<f64> {
420 self.estimated_bpm.map(|leader| leader - our_bpm)
421 }
422
423 pub fn reset(&mut self) {
425 self.observed_beats.clear();
426 self.estimated_bpm = None;
427 }
428}
429
430#[derive(Debug, Clone)]
439pub struct TempoLeader {
440 leader_id: String,
442 current: TempoMarking,
444 followers: HashMap<String, f64>,
446 rubato_enabled: bool,
448 active_curve: Option<TempoCurve>,
450 curve_position: f64,
452}
453
454impl TempoLeader {
455 pub fn new(leader_id: impl Into<String>, initial_tempo: TempoMarking) -> Self {
457 Self {
458 leader_id: leader_id.into(),
459 current: initial_tempo,
460 followers: HashMap::new(),
461 rubato_enabled: false,
462 active_curve: None,
463 curve_position: 0.0,
464 }
465 }
466
467 pub fn set_rubato(&mut self, enabled: bool) {
469 self.rubato_enabled = enabled;
470 }
471
472 pub fn add_follower(&mut self, follower_id: impl Into<String>, offset_bpm: f64) {
474 self.followers.insert(follower_id.into(), offset_bpm);
475 }
476
477 pub fn remove_follower(&mut self, follower_id: &str) -> bool {
479 self.followers.remove(follower_id).is_some()
480 }
481
482 pub fn set_tempo(&mut self, tempo: TempoMarking) {
484 self.current = tempo;
485 }
486
487 pub fn start_curve(&mut self, curve: TempoCurve) {
489 self.active_curve = Some(curve);
490 self.curve_position = 0.0;
491 }
492
493 pub fn advance_curve(&mut self, step: f64) -> Option<f64> {
496 if let Some(ref curve) = self.active_curve {
497 self.curve_position = (self.curve_position + step).min(1.0);
498 let bpm = curve.bpm_at_position(self.curve_position);
499 if self.curve_position >= 1.0 {
500 self.current = TempoMarking::new(bpm, self.current.feel);
501 self.active_curve = None;
502 }
503 Some(bpm)
504 } else {
505 None
506 }
507 }
508
509 pub fn effective_bpm(&self) -> f64 {
511 if let Some(ref curve) = self.active_curve {
512 curve.bpm_at_position(self.curve_position)
513 } else {
514 self.current.bpm()
515 }
516 }
517
518 pub fn tempo_for_follower(&self, follower_id: &str) -> f64 {
520 let base = self.effective_bpm();
521 let offset = self.followers.get(follower_id).copied().unwrap_or(0.0);
522 base + offset
523 }
524
525 pub fn leader_id(&self) -> &str {
527 &self.leader_id
528 }
529
530 pub fn follower_count(&self) -> usize {
532 self.followers.len()
533 }
534
535 pub fn current_tempo(&self) -> &TempoMarking {
537 &self.current
538 }
539
540 pub fn rubato_enabled(&self) -> bool {
542 self.rubato_enabled
543 }
544
545 pub fn curve_active(&self) -> bool {
547 self.active_curve.is_some()
548 }
549}
550
551#[derive(Debug, Clone)]
561pub struct TempoMapEntry {
562 start_beat: f64,
564 bpm: f64,
566 label: Option<String>,
568}
569
570#[derive(Debug, Clone)]
572pub struct TempoMap {
573 entries: Vec<TempoMapEntry>,
574 total_beats: f64,
575 default_bpm: f64,
576}
577
578impl TempoMap {
579 pub fn new(default_bpm: f64) -> Self {
581 Self {
582 entries: Vec::new(),
583 total_beats: 0.0,
584 default_bpm,
585 }
586 }
587
588 pub fn add_marking(&mut self, beat: f64, bpm: f64, label: Option<String>) {
590 let entry = TempoMapEntry {
591 start_beat: beat,
592 bpm,
593 label,
594 };
595 self.entries.push(entry);
596 self.entries.sort_by(|a, b| a.start_beat.partial_cmp(&b.start_beat).unwrap());
597 if beat > self.total_beats {
598 self.total_beats = beat;
599 }
600 }
601
602 pub fn bpm_at_beat(&self, beat: f64) -> f64 {
604 if self.entries.is_empty() {
605 return self.default_bpm;
606 }
607 let mut result = self.default_bpm;
609 for entry in &self.entries {
610 if entry.start_beat <= beat {
611 result = entry.bpm;
612 } else {
613 break;
614 }
615 }
616 result
617 }
618
619 pub fn total_beats(&self) -> f64 {
621 self.total_beats
622 }
623
624 pub fn entry_count(&self) -> usize {
626 self.entries.len()
627 }
628
629 pub fn is_empty(&self) -> bool {
631 self.entries.is_empty()
632 }
633
634 pub fn entries(&self) -> &[TempoMapEntry] {
636 &self.entries
637 }
638
639 pub fn total_duration(&self) -> Duration {
641 if self.entries.is_empty() {
642 return Duration::from_secs(0);
643 }
644 let mut total = 0.0_f64;
645 for i in 0..self.entries.len() {
646 let start = self.entries[i].start_beat;
647 let end = if i + 1 < self.entries.len() {
648 self.entries[i + 1].start_beat
649 } else {
650 self.total_beats
651 };
652 let beats = end - start;
653 let secs_per_beat = 60.0 / self.entries[i].bpm;
654 total += beats * secs_per_beat;
655 }
656 Duration::from_secs_f64(total)
657 }
658}
659
660#[cfg(test)]
665mod tests {
666 use super::*;
667
668 #[test]
671 fn test_tempo_marking_creation() {
672 let tm = TempoMarking::new(120.0, 0.5);
673 assert_eq!(tm.bpm(), 120.0);
674 assert_eq!(tm.feel(), 0.5);
675 assert!(tm.label().is_none());
676 }
677
678 #[test]
679 fn test_tempo_marking_labeled() {
680 let tm = TempoMarking::labeled(100.0, 0.2, "Moderato");
681 assert_eq!(tm.label(), Some("Moderato"));
682 }
683
684 #[test]
685 fn test_tempo_marking_clamping() {
686 let tm = TempoMarking::new(0.0, 5.0);
687 assert_eq!(tm.bpm(), 1.0);
688 assert_eq!(tm.feel(), 1.0);
689 }
690
691 #[test]
692 fn test_beat_duration() {
693 let tm = TempoMarking::new(60.0, 0.0);
694 assert_eq!(tm.beat_duration(), Duration::from_secs(1));
695 let tm2 = TempoMarking::new(120.0, 0.0);
696 assert_eq!(tm2.beat_duration(), Duration::from_millis(500));
697 }
698
699 #[test]
700 fn test_beat_with_feel() {
701 let robotic = TempoMarking::new(100.0, 0.0);
702 let base = robotic.beat_duration();
703 let with_zero_feel = robotic.beat_with_feel(1.0);
704 assert_eq!(base, with_zero_feel); let human = TempoMarking::new(100.0, 1.0);
707 let with_feel = human.beat_with_feel(1.0);
708 assert_ne!(base, with_feel);
709 }
710
711 #[test]
712 fn test_well_known_tempos() {
713 assert_eq!(TempoMarking::largo().bpm(), 50.0);
714 assert_eq!(TempoMarking::andante().bpm(), 80.0);
715 assert_eq!(TempoMarking::allegro().bpm(), 140.0);
716 assert_eq!(TempoMarking::presto().bpm(), 180.0);
717 }
718
719 #[test]
722 fn test_rubato_profile_creation() {
723 let p = RubatoProfile::new("test", 0.1, 0.2, 0.5);
724 assert_eq!(p.name(), "test");
725 assert_eq!(p.max_compress(), 0.1);
726 assert_eq!(p.max_stretch(), 0.2);
727 assert_eq!(p.transition_rate(), 0.5);
728 }
729
730 #[test]
731 fn test_rubato_apply_compress() {
732 let p = RubatoProfile::moderate();
733 let result = p.apply(100.0, 1.0); assert!(result > 100.0);
735 assert_eq!(result, 120.0); }
737
738 #[test]
739 fn test_rubato_apply_stretch() {
740 let p = RubatoProfile::moderate();
741 let result = p.apply(100.0, -1.0); assert_eq!(result, 75.0); }
744
745 #[test]
746 fn test_rubato_apply_neutral() {
747 let p = RubatoProfile::moderate();
748 let result = p.apply(100.0, 0.0);
749 assert_eq!(result, 100.0);
750 }
751
752 #[test]
753 fn test_rubato_range() {
754 let p = RubatoProfile::expressive();
755 assert_eq!(p.min_bpm(100.0), 50.0); assert_eq!(p.max_bpm(100.0), 140.0); assert!(p.is_within_range(100.0, 80.0));
758 assert!(p.is_within_range(100.0, 120.0));
759 assert!(!p.is_within_range(100.0, 30.0));
760 }
761
762 #[test]
763 fn test_preset_profiles() {
764 let s = RubatoProfile::strict();
765 assert!(s.max_compress() < 0.1);
766 let m = RubatoProfile::moderate();
767 assert!(m.max_compress() > s.max_compress());
768 let e = RubatoProfile::expressive();
769 assert!(e.max_compress() > m.max_compress());
770 }
771
772 #[test]
775 fn test_accelerando_curve() {
776 let c = TempoCurve::new(80.0, 140.0, 16);
777 assert_eq!(c.shape(), CurveShape::Accelerando);
778 assert!(c.is_accelerando());
779 assert!(!c.is_ritardando());
780 }
781
782 #[test]
783 fn test_ritardando_curve() {
784 let c = TempoCurve::new(140.0, 80.0, 16);
785 assert_eq!(c.shape(), CurveShape::Ritardando);
786 assert!(c.is_ritardando());
787 }
788
789 #[test]
790 fn test_flat_curve() {
791 let c = TempoCurve::new(120.0, 120.0, 8);
792 assert_eq!(c.shape(), CurveShape::Flat);
793 }
794
795 #[test]
796 fn test_curve_bpm_at_beat() {
797 let c = TempoCurve::new(100.0, 200.0, 10);
798 assert_eq!(c.bpm_at_beat(0.0), 100.0);
799 assert_eq!(c.bpm_at_beat(5.0), 150.0);
800 assert_eq!(c.bpm_at_beat(10.0), 200.0);
801 }
802
803 #[test]
804 fn test_curve_bpm_at_position() {
805 let c = TempoCurve::new(100.0, 200.0, 10);
806 assert_eq!(c.bpm_at_position(0.0), 100.0);
807 assert_eq!(c.bpm_at_position(1.0), 200.0);
808 let mid = c.bpm_at_position(0.5);
810 assert_eq!(mid, 150.0); }
812
813 #[test]
814 fn test_curve_with_label() {
815 let c = TempoCurve::new(60.0, 120.0, 8).with_label("crescendo section");
816 assert_eq!(c.label(), Some("crescendo section"));
817 }
818
819 #[test]
820 fn test_curve_delta() {
821 let c = TempoCurve::new(80.0, 140.0, 8);
822 assert_eq!(c.delta(), 60.0);
823 }
824
825 #[test]
826 fn test_rubato_curve() {
827 let c = TempoCurve::rubato(120.0, 10.0, 32);
828 assert_eq!(c.shape(), CurveShape::Rubato);
829 assert_eq!(c.start_bpm(), 120.0);
830 assert_eq!(c.end_bpm(), 130.0);
831 }
832
833 #[test]
836 fn test_follower_creation() {
837 let f = TempoFollower::new("leader-1");
838 assert_eq!(f.leader_id(), "leader-1");
839 assert!(f.estimated_bpm().is_none());
840 assert_eq!(f.observed_count(), 0);
841 }
842
843 #[test]
844 fn test_follower_estimates_bpm() {
845 let mut f = TempoFollower::new("leader");
846 let now = Instant::now();
847 for i in 0..5 {
849 f.observe_beat_at(now + Duration::from_millis(500 * i as u64));
850 }
851 let bpm = f.estimated_bpm().unwrap();
852 assert!(bpm > 110.0 && bpm < 130.0, "Expected ~120 BPM, got {}", bpm);
854 }
855
856 #[test]
857 fn test_follower_sync_offset() {
858 let mut f = TempoFollower::new("leader");
859 let now = Instant::now();
860 for i in 0..5 {
861 f.observe_beat_at(now + Duration::from_millis(500 * i as u64));
862 }
863 let offset = f.sync_offset(100.0).unwrap();
864 assert!(offset > 0.0); }
866
867 #[test]
868 fn test_follower_reset() {
869 let mut f = TempoFollower::new("leader");
870 f.observe_beat();
871 f.observe_beat();
872 f.reset();
873 assert_eq!(f.observed_count(), 0);
874 assert!(f.estimated_bpm().is_none());
875 }
876
877 #[test]
878 fn test_follower_window_size() {
879 let mut f = TempoFollower::new("leader").with_window_size(4);
880 for _ in 0..10 {
881 f.observe_beat();
882 }
883 assert_eq!(f.observed_count(), 4);
884 }
885
886 #[test]
889 fn test_leader_creation() {
890 let l = TempoLeader::new("conductor", TempoMarking::allegro());
891 assert_eq!(l.leader_id(), "conductor");
892 assert_eq!(l.follower_count(), 0);
893 assert!(!l.rubato_enabled());
894 assert!(!l.curve_active());
895 }
896
897 #[test]
898 fn test_leader_followers() {
899 let mut l = TempoLeader::new("conductor", TempoMarking::allegro());
900 l.add_follower("agent-a", 0.0);
901 l.add_follower("agent-b", 10.0);
902 assert_eq!(l.follower_count(), 2);
903 assert_eq!(l.tempo_for_follower("agent-a"), 140.0);
904 assert_eq!(l.tempo_for_follower("agent-b"), 150.0);
905 assert_eq!(l.tempo_for_follower("unknown"), 140.0); }
907
908 #[test]
909 fn test_leader_remove_follower() {
910 let mut l = TempoLeader::new("conductor", TempoMarking::andante());
911 l.add_follower("agent-a", 0.0);
912 assert!(l.remove_follower("agent-a"));
913 assert!(!l.remove_follower("agent-a"));
914 assert_eq!(l.follower_count(), 0);
915 }
916
917 #[test]
918 fn test_leader_set_tempo() {
919 let mut l = TempoLeader::new("conductor", TempoMarking::andante());
920 l.set_tempo(TempoMarking::presto());
921 assert_eq!(l.effective_bpm(), 180.0);
922 }
923
924 #[test]
925 fn test_leader_rubato() {
926 let mut l = TempoLeader::new("conductor", TempoMarking::allegro());
927 l.set_rubato(true);
928 assert!(l.rubato_enabled());
929 }
930
931 #[test]
932 fn test_leader_curve() {
933 let mut l = TempoLeader::new("conductor", TempoMarking::new(80.0, 0.0));
934 let curve = TempoCurve::new(80.0, 140.0, 16);
935 l.start_curve(curve);
936 assert!(l.curve_active());
937
938 let bpm_25 = l.advance_curve(0.25);
940 assert!(bpm_25.is_some());
941 assert!(l.curve_active());
942
943 let bpm_50 = l.advance_curve(0.25);
944 assert!(bpm_50.is_some());
945
946 let bpm_100 = l.advance_curve(0.50);
947 assert!(bpm_100.is_some());
948 assert!(!l.curve_active()); assert_eq!(l.effective_bpm(), bpm_100.unwrap());
950 }
951
952 #[test]
955 fn test_empty_tempo_map() {
956 let map = TempoMap::new(120.0);
957 assert!(map.is_empty());
958 assert_eq!(map.bpm_at_beat(0.0), 120.0);
959 assert_eq!(map.bpm_at_beat(100.0), 120.0);
960 }
961
962 #[test]
963 fn test_tempo_map_entries() {
964 let mut map = TempoMap::new(120.0);
965 map.add_marking(0.0, 80.0, Some("Intro".into()));
966 map.add_marking(16.0, 120.0, Some("Theme".into()));
967 map.add_marking(48.0, 100.0, Some("Bridge".into()));
968 map.add_marking(64.0, 140.0, Some("Finale".into()));
969
970 assert_eq!(map.entry_count(), 4);
971 assert_eq!(map.bpm_at_beat(0.0), 80.0);
972 assert_eq!(map.bpm_at_beat(15.0), 80.0);
973 assert_eq!(map.bpm_at_beat(16.0), 120.0);
974 assert_eq!(map.bpm_at_beat(32.0), 120.0);
975 assert_eq!(map.bpm_at_beat(48.0), 100.0);
976 assert_eq!(map.bpm_at_beat(60.0), 100.0);
977 assert_eq!(map.bpm_at_beat(64.0), 140.0);
978 }
979
980 #[test]
981 fn test_tempo_map_out_of_range() {
982 let mut map = TempoMap::new(120.0);
983 map.add_marking(0.0, 100.0, None);
984 map.add_marking(8.0, 120.0, None);
985 assert_eq!(map.bpm_at_beat(100.0), 120.0);
987 }
988
989 #[test]
990 fn test_tempo_map_default() {
991 let map = TempoMap::new(90.0);
992 assert_eq!(map.bpm_at_beat(0.0), 90.0);
993 }
994
995 #[test]
996 fn test_tempo_map_unordered_insert() {
997 let mut map = TempoMap::new(120.0);
998 map.add_marking(32.0, 140.0, None);
999 map.add_marking(0.0, 80.0, None);
1000 map.add_marking(16.0, 120.0, None);
1001 assert_eq!(map.bpm_at_beat(0.0), 80.0);
1003 assert_eq!(map.bpm_at_beat(16.0), 120.0);
1004 assert_eq!(map.bpm_at_beat(32.0), 140.0);
1005 }
1006
1007 #[test]
1008 fn test_tempo_map_total_beats() {
1009 let mut map = TempoMap::new(120.0);
1010 map.add_marking(0.0, 100.0, None);
1011 map.add_marking(64.0, 120.0, None);
1012 assert_eq!(map.total_beats(), 64.0);
1013 }
1014
1015 #[test]
1016 fn test_tempo_map_duration() {
1017 let mut map = TempoMap::new(120.0);
1018 map.add_marking(0.0, 120.0, None);
1020 map.total_beats = 8.0;
1021 let dur = map.total_duration();
1022 assert_eq!(dur, Duration::from_secs(4));
1023 }
1024
1025 #[test]
1026 fn test_tempo_map_entries_access() {
1027 let mut map = TempoMap::new(120.0);
1028 map.add_marking(0.0, 80.0, Some("A".into()));
1029 map.add_marking(16.0, 120.0, Some("B".into()));
1030 let entries = map.entries();
1031 assert_eq!(entries.len(), 2);
1032 assert_eq!(entries[0].label.as_deref(), Some("A"));
1033 assert_eq!(entries[1].label.as_deref(), Some("B"));
1034 }
1035}