Skip to main content

pfx_sound/
channel.rs

1use crate::filter::{Filter, Kind};
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
4pub struct ChannelId(u32);
5
6impl ChannelId {
7    pub const MUSIC: ChannelId = ChannelId(0);
8    pub const EFFECTS: ChannelId = ChannelId(1);
9    pub const DIALOG: ChannelId = ChannelId(2);
10
11    const FIRST_CUSTOM: u32 = 3;
12
13    pub const fn custom(index: u32) -> ChannelId {
14        ChannelId(Self::FIRST_CUSTOM.saturating_add(index))
15    }
16}
17
18pub(crate) const BUILT_IN: [ChannelId; 3] =
19    [ChannelId::MUSIC, ChannelId::EFFECTS, ChannelId::DIALOG];
20
21pub const RAMP_SECONDS: f32 = 0.01;
22
23pub(crate) fn ramp_frames(rate: u32) -> u32 {
24    ((rate as f32 * RAMP_SECONDS) as u32).max(1)
25}
26
27#[derive(Clone, Copy, Debug)]
28pub(crate) struct Ramp {
29    current: f32,
30    target: f32,
31    step: f32,
32}
33
34impl Ramp {
35    pub(crate) fn new(value: f32) -> Self {
36        Self {
37            current: value,
38            target: value,
39            step: 0.0,
40        }
41    }
42
43    pub(crate) fn current(&self) -> f32 {
44        self.current
45    }
46
47    pub(crate) fn is_settled(&self) -> bool {
48        self.current == self.target
49    }
50
51    pub(crate) fn snap(&mut self, value: f32) {
52        *self = Self::new(value);
53    }
54
55    pub(crate) fn to(&mut self, target: f32, frames: u32) {
56        self.target = target;
57        self.step = (target - self.current) / frames.max(1) as f32;
58    }
59
60    pub(crate) fn next(&mut self) -> f32 {
61        if self.current != self.target {
62            self.current += self.step;
63            let passed = if self.step > 0.0 {
64                self.current >= self.target
65            } else {
66                self.current <= self.target
67            };
68            if passed || self.step == 0.0 {
69                self.current = self.target;
70            }
71        }
72        self.current
73    }
74
75    pub(crate) fn skip(&mut self, frames: usize) {
76        if self.current == self.target {
77            return;
78        }
79        let moved = self.current + self.step * frames as f32;
80        let passed = if self.step > 0.0 {
81            moved >= self.target
82        } else {
83            moved <= self.target
84        };
85        self.current = if passed || self.step == 0.0 {
86            self.target
87        } else {
88            moved
89        };
90    }
91}
92
93pub(crate) struct Channel {
94    pub(crate) volume: f32,
95    pub(crate) muted: bool,
96    pub(crate) gain: Ramp,
97    pub(crate) pitch: Ramp,
98    pub(crate) pitch_curve: Vec<f32>,
99    pub(crate) pitch_on: bool,
100    pub(crate) lowpass: Filter,
101    pub(crate) highpass: Filter,
102    pub(crate) bus: Vec<f32>,
103    pub(crate) touched: bool,
104}
105
106impl Channel {
107    pub(crate) fn new() -> Self {
108        Self {
109            volume: 1.0,
110            muted: false,
111            gain: Ramp::new(1.0),
112            pitch: Ramp::new(1.0),
113            pitch_curve: Vec::new(),
114            pitch_on: false,
115            lowpass: Filter::new(Kind::LowPass),
116            highpass: Filter::new(Kind::HighPass),
117            bus: Vec::new(),
118            touched: false,
119        }
120    }
121
122    pub(crate) fn prepare_pitch(&mut self, frames: usize) {
123        self.pitch_on = !(self.pitch.is_settled() && self.pitch.current() == 1.0);
124        if self.pitch_on {
125            self.pitch_curve.clear();
126            for _ in 0..frames {
127                self.pitch_curve.push(self.pitch.next());
128            }
129        }
130    }
131
132    pub(crate) fn effective(&self) -> f32 {
133        if self.muted { 0.0 } else { self.volume }
134    }
135}
136
137pub(crate) fn clamp_volume(volume: f32) -> f32 {
138    if volume.is_nan() {
139        0.0
140    } else {
141        volume.clamp(0.0, 1.0)
142    }
143}
144
145#[derive(Clone, Copy, Debug, PartialEq)]
146pub struct Ducking {
147    pub trigger: ChannelId,
148    pub target: ChannelId,
149    pub depth_db: f32,
150    pub attack: f32,
151    pub release: f32,
152}
153
154impl Ducking {
155    pub fn dialog_over_music(depth_db: f32, attack: f32, release: f32) -> Self {
156        Self {
157            trigger: ChannelId::DIALOG,
158            target: ChannelId::MUSIC,
159            depth_db,
160            attack,
161            release,
162        }
163    }
164}
165
166pub(crate) struct DuckState {
167    pub(crate) config: Ducking,
168    pub(crate) reduction_db: f32,
169}
170
171impl DuckState {
172    pub(crate) fn new(config: Ducking) -> Self {
173        Self {
174            config,
175            reduction_db: 0.0,
176        }
177    }
178
179    pub(crate) fn step(&mut self, active: bool, rate: u32) -> f32 {
180        let depth = self.config.depth_db.max(0.0);
181        let (goal, seconds) = if active {
182            (depth, self.config.attack)
183        } else {
184            (0.0, self.config.release)
185        };
186        let frames = seconds.max(0.0) * rate as f32;
187        let travel = if frames < 1.0 { depth } else { depth / frames };
188        if self.reduction_db < goal {
189            self.reduction_db = (self.reduction_db + travel).min(goal);
190        } else if self.reduction_db > goal {
191            self.reduction_db = (self.reduction_db - travel).max(goal);
192        }
193        if self.reduction_db == 0.0 {
194            1.0
195        } else {
196            10f32.powf(-self.reduction_db / 20.0)
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn custom_channels_never_collide_with_the_built_ins() {
207        assert_ne!(ChannelId::custom(0), ChannelId::DIALOG);
208        assert_ne!(ChannelId::custom(0), ChannelId::custom(1));
209        assert_eq!(ChannelId::custom(7), ChannelId::custom(7));
210    }
211
212    #[test]
213    fn a_ramp_lands_exactly_on_its_target() {
214        let mut ramp = Ramp::new(0.0);
215        ramp.to(1.0, 4);
216        let seen: Vec<f32> = (0..6).map(|_| ramp.next()).collect();
217        assert_eq!(seen, vec![0.25, 0.5, 0.75, 1.0, 1.0, 1.0]);
218        ramp.to(0.0, 2);
219        ramp.skip(1);
220        assert_eq!(ramp.current(), 0.5);
221        ramp.skip(5);
222        assert_eq!(ramp.current(), 0.0);
223    }
224}