1use crate::clip::Clip;
2use crate::synth::sine;
3
4#[derive(Clone, Debug)]
5pub struct MusicTrack {
6 pub clip: Clip,
7 pub looped: bool,
8 pub loop_start: u64,
9 pub loop_end: u64,
10}
11
12impl MusicTrack {
13 pub fn once(clip: Clip) -> Self {
14 let loop_end = clip.frames();
15 Self {
16 clip,
17 looped: false,
18 loop_start: 0,
19 loop_end,
20 }
21 }
22
23 pub fn looping(clip: Clip) -> Self {
24 Self {
25 looped: true,
26 ..Self::once(clip)
27 }
28 }
29
30 pub fn with_loop(clip: Clip, loop_start: u64, loop_end: u64) -> Self {
31 Self {
32 looped: true,
33 loop_start,
34 loop_end,
35 ..Self::once(clip)
36 }
37 }
38
39 pub(crate) fn loop_frames(&self, rate: u32) -> Option<(u64, u64)> {
40 if !self.looped {
41 return None;
42 }
43 scale_loop(
44 self.loop_start,
45 self.loop_end,
46 self.clip.frames(),
47 self.clip.rate,
48 rate,
49 )
50 }
51}
52
53pub(crate) fn scale_loop(
54 start: u64,
55 end: u64,
56 frames: u64,
57 from_rate: u32,
58 to_rate: u32,
59) -> Option<(u64, u64)> {
60 if from_rate == 0 {
61 return None;
62 }
63 let scale = |value: u64| -> u64 {
64 let scaled = u128::from(value) * u128::from(to_rate) / u128::from(from_rate);
65 u64::try_from(scaled).unwrap_or(u64::MAX)
66 };
67 let total = scale(frames);
68 let end = scale(end.min(frames)).min(total);
69 let start = scale(start);
70 if end == 0 || start >= end {
71 return (total > 0).then_some((0, total));
72 }
73 Some((start, end))
74}
75
76#[derive(Clone, Copy, Debug, PartialEq)]
77pub(crate) enum Direction {
78 In,
79 Out,
80}
81
82#[derive(Clone, Copy, Debug, PartialEq)]
83pub(crate) struct Curve {
84 pub(crate) start: u64,
85 pub(crate) len: u64,
86 pub(crate) direction: Direction,
87}
88
89impl Curve {
90 pub(crate) fn gain(&self, at: u64) -> f32 {
91 if at < self.start {
92 return match self.direction {
93 Direction::In => 0.0,
94 Direction::Out => 1.0,
95 };
96 }
97 let past = at - self.start;
98 if past >= self.len {
99 return match self.direction {
100 Direction::In => 1.0,
101 Direction::Out => 0.0,
102 };
103 }
104 let quarter = past as f64 / self.len as f64 * 0.25;
105 match self.direction {
106 Direction::In => sine(quarter),
107 Direction::Out => sine(quarter + 0.25),
108 }
109 }
110
111 pub(crate) fn spent(&self, clock: u64) -> bool {
112 self.direction == Direction::Out && clock >= self.start.saturating_add(self.len)
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn a_loop_keeps_its_points_across_a_rate_change() {
122 let clip = Clip {
123 rate: 24_000,
124 channels: 1,
125 samples: vec![0.0; 1000],
126 };
127 let track = MusicTrack::with_loop(clip.clone(), 100, 900);
128 assert_eq!(track.loop_frames(24_000), Some((100, 900)));
129 assert_eq!(track.loop_frames(48_000), Some((200, 1800)));
130 assert_eq!(MusicTrack::once(clip.clone()).loop_frames(24_000), None);
131 assert_eq!(
132 MusicTrack::looping(clip.clone()).loop_frames(24_000),
133 Some((0, 1000))
134 );
135 let backwards = MusicTrack::with_loop(clip, 900, 100);
136 assert_eq!(backwards.loop_frames(24_000), Some((0, 1000)));
137 }
138
139 #[test]
140 fn crossfade_curves_keep_power_constant() {
141 let fade_in = Curve {
142 start: 10,
143 len: 100,
144 direction: Direction::In,
145 };
146 let fade_out = Curve {
147 direction: Direction::Out,
148 ..fade_in
149 };
150 for at in 0..130 {
151 let sum = fade_in.gain(at).powi(2) + fade_out.gain(at).powi(2);
152 assert!((sum - 1.0).abs() < 1e-5, "frame {at}: {sum}");
153 }
154 assert!(fade_out.spent(110));
155 assert!(!fade_out.spent(109));
156 assert!(!fade_in.spent(1000));
157 }
158}