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}