Skip to main content

pfx_sound/
music.rs

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}