audio_engine_core/processor/loudness/
ramp.rs1pub struct GainRamp {
11 from: f64,
13 to: f64,
15 current: f64,
17 step: f64,
19 total_samples: usize,
21 remaining: usize,
23}
24
25impl GainRamp {
26 pub fn new(from: f64, to: f64, sample_rate: u32, ramp_ms: u32) -> Self {
34 let total_samples = (sample_rate as u64 * ramp_ms as u64 / 1000) as usize;
35 let total_samples = total_samples.max(1);
36
37 Self {
38 from,
39 to,
40 current: from,
41 step: (to - from) / total_samples as f64,
42 total_samples,
43 remaining: total_samples,
44 }
45 }
46
47 pub fn fade_in(target: f64, sample_rate: u32, ramp_ms: u32) -> Self {
49 Self::new(0.0, target, sample_rate, ramp_ms)
50 }
51
52 pub fn fade_out(from: f64, sample_rate: u32, ramp_ms: u32) -> Self {
54 Self::new(from, 0.0, sample_rate, ramp_ms)
55 }
56
57 #[inline(always)]
60 pub fn next_gain(&mut self) -> f64 {
61 if self.remaining > 0 {
62 let gain = self.current;
63 self.remaining -= 1;
64 if self.remaining == 0 {
65 self.current = self.to;
66 } else {
67 self.current += self.step;
68 }
69 gain
70 } else {
71 self.to
72 }
73 }
74
75 pub fn apply(&mut self, samples: &mut [f64]) {
77 if samples.is_empty() {
78 return;
79 }
80
81 let ramp_samples = samples.len().min(self.remaining);
82
83 if ramp_samples > 0 {
84 let mut gain = self.current;
85 for sample in &mut samples[..ramp_samples] {
86 *sample *= gain;
87 gain += self.step;
88 }
89
90 self.remaining -= ramp_samples;
91 if self.remaining == 0 {
92 self.current = self.to;
93 } else {
94 self.current = gain;
95 }
96 }
97
98 if ramp_samples < samples.len() && self.to != 1.0 {
99 for sample in &mut samples[ramp_samples..] {
100 *sample *= self.to;
101 }
102 }
103 }
104
105 pub fn is_done(&self) -> bool {
107 self.remaining == 0
108 }
109
110 pub fn remaining_samples(&self) -> usize {
112 self.remaining
113 }
114
115 pub fn current(&self) -> f64 {
117 self.current
118 }
119
120 pub fn target(&self) -> f64 {
122 self.to
123 }
124
125 pub fn retarget(&mut self, new_target: f64, sample_rate: u32, ramp_ms: u32) {
127 let current = self.current();
128 self.from = current;
129 self.to = new_target;
130 let total_samples = (sample_rate as u64 * ramp_ms as u64 / 1000) as usize;
131 self.total_samples = total_samples.max(1);
132 self.remaining = self.total_samples;
133 self.current = current;
134 self.step = (self.to - self.from) / self.total_samples as f64;
135 }
136
137 pub fn jump(&mut self, target: f64) {
139 self.from = target;
140 self.to = target;
141 self.current = target;
142 self.step = 0.0;
143 self.total_samples = 1;
144 self.remaining = 0;
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::GainRamp;
151
152 #[test]
153 fn apply_matches_next_gain_loop_across_ramp_and_tail() {
154 let mut apply_ramp = GainRamp::new(0.05, 0.95, 48_000, 10);
155 let mut loop_ramp = GainRamp::new(0.05, 0.95, 48_000, 10);
156 let mut apply_samples = synthetic_samples(2_048);
157 let mut loop_samples = apply_samples.clone();
158
159 for chunk in apply_samples.chunks_mut(127) {
160 apply_ramp.apply(chunk);
161 }
162 for sample in &mut loop_samples {
163 *sample *= loop_ramp.next_gain();
164 }
165
166 assert_eq!(
167 apply_ramp.remaining_samples(),
168 loop_ramp.remaining_samples()
169 );
170 assert_eq!(
171 apply_ramp.current().to_bits(),
172 loop_ramp.current().to_bits()
173 );
174 assert_samples_match(&apply_samples, &loop_samples);
175 }
176
177 #[test]
178 fn apply_uses_target_gain_after_ramp_is_done() {
179 let mut ramp = GainRamp::new(0.0, 0.5, 1_000, 1);
180 let mut samples = vec![2.0; 5];
181
182 ramp.apply(&mut samples);
183
184 assert_eq!(samples, vec![0.0, 1.0, 1.0, 1.0, 1.0]);
185 assert!(ramp.is_done());
186 assert_eq!(ramp.current(), 0.5);
187 }
188
189 #[test]
190 fn apply_does_not_change_empty_buffer_state() {
191 let mut ramp = GainRamp::new(0.0, 1.0, 48_000, 100);
192 let current = ramp.current();
193 let remaining = ramp.remaining_samples();
194
195 ramp.apply(&mut []);
196
197 assert_eq!(ramp.current(), current);
198 assert_eq!(ramp.remaining_samples(), remaining);
199 }
200
201 fn synthetic_samples(len: usize) -> Vec<f64> {
202 (0..len)
203 .map(|idx| {
204 let t = idx as f64 / 48_000.0;
205 (std::f64::consts::TAU * 997.0 * t).sin() * 0.35
206 })
207 .collect()
208 }
209
210 fn assert_samples_match(left: &[f64], right: &[f64]) {
211 assert_eq!(left.len(), right.len());
212 for (idx, (left, right)) in left.iter().zip(right).enumerate() {
213 assert_eq!(
214 left.to_bits(),
215 right.to_bits(),
216 "sample mismatch at {idx}: {left} != {right}"
217 );
218 }
219 }
220}