gled_model/sound/
trigger.rs1use super::trigger_params::FREQ_BINS;
5pub use super::trigger_params::SoundTriggerParams;
6use ndarray::Array1;
7use std::ops::Mul;
8
9pub(crate) const SAMPLE_INTERVAL_MS: u64 = 10;
11
12const MAX_BUFFER_LENGTH: usize = 1001;
13
14#[derive(Debug, Clone, Copy, PartialEq, Default)]
15pub(crate) enum AdsrPhase {
16 #[default]
17 Idle,
18 Attack,
19 Decay,
20 Sustain,
21 Release,
22}
23
24#[derive(Debug, PartialEq, Clone)]
25pub struct SoundTrigger {
26 pub(crate) phase: AdsrPhase,
27 pub(crate) gate_active: bool,
28 pub params: SoundTriggerParams,
29 pub impulse: f32,
30 pub(crate) delta_time: f32,
31 pub spectrum: Array1<f32>,
32 pub current_level: f32,
33 sample_buffer: PrimitiveRingBuffer<Array1<f32>>,
34 running_sum: Array1<f32>,
35 prev_averaging_samples: usize,
36 echo_buffer: PrimitiveRingBuffer<f32>,
37 running_impulse_sum: f32,
38}
39
40impl Default for SoundTrigger {
41 fn default() -> Self {
42 SoundTrigger::new(
43 SoundTriggerParams::default(),
44 SAMPLE_INTERVAL_MS as f32 / 1000.,
45 )
46 }
47}
48
49impl SoundTrigger {
50 pub fn new(params: SoundTriggerParams, delta_time: f32) -> Self {
51 Self {
52 delta_time,
53 gate_active: false,
54 spectrum: Array1::zeros(FREQ_BINS),
55 phase: AdsrPhase::Idle,
56 params,
57 current_level: 0.0,
58 impulse: 0.0,
59 sample_buffer: PrimitiveRingBuffer::new(Array1::zeros(FREQ_BINS), MAX_BUFFER_LENGTH),
60 running_sum: Array1::zeros(FREQ_BINS),
61 prev_averaging_samples: params.averaging_samples,
62 echo_buffer: PrimitiveRingBuffer::new(0.0, MAX_BUFFER_LENGTH),
63 running_impulse_sum: 0.0,
64 }
65 }
66
67 pub fn reset(&mut self) {
68 self.phase = AdsrPhase::Idle;
69 self.gate_active = false;
70 self.current_level = 0.0;
71 self.impulse = 0.0;
72 self.sample_buffer = PrimitiveRingBuffer::new(Array1::zeros(FREQ_BINS), MAX_BUFFER_LENGTH);
73 self.running_sum = Array1::zeros(FREQ_BINS);
74 self.spectrum = Array1::zeros(FREQ_BINS);
75 }
76
77 pub fn tick(&mut self, samples: [f32; FREQ_BINS]) {
78 self.compute_running_average(samples);
79 self.spectrum.map_inplace(|x| {
81 *x = x.mul(self.delta_time * 3.0).clamp(0.0, f32::INFINITY);
82 });
83
84 let max_f = (self.params.center_bin + self.params.bin_radius).clamp(0, FREQ_BINS - 1);
85 let start = self
86 .params
87 .center_bin
88 .saturating_sub(self.params.bin_radius)
89 .clamp(1, FREQ_BINS - 1);
90 let len = max_f.saturating_sub(start);
91 self.impulse = (self
92 .spectrum
93 .iter()
94 .skip(start)
95 .take(len)
96 .map(|x| x.powf(2.))
97 .sum::<f32>()
98 / (2. * self.params.bin_radius as f32))
99 .sqrt()
100 .mul(4.0);
101
102 self.impulse = self.highpass(self.impulse);
103 self.impulse = self.impulse.mul(self.params.sensitivity);
104 self.tick_adsr(self.impulse);
105 }
106
107 pub fn tick_adsr(&mut self, input: f32) -> f32 {
112 self.gate_active = if self.gate_active {
113 input > self.params.gate_deactivation_threshold
114 } else {
115 input > self.params.gate_activation_threshold
116 };
117
118 match self.phase {
120 AdsrPhase::Idle => {
121 if self.gate_active {
122 self.phase = AdsrPhase::Attack;
123 }
124 }
125 AdsrPhase::Attack => {
126 if !self.gate_active {
127 self.phase = AdsrPhase::Release;
128 } else if self.current_level >= 1.0 {
129 self.phase = AdsrPhase::Decay;
130 }
131 }
132 AdsrPhase::Decay => {
133 if !self.gate_active {
134 self.phase = AdsrPhase::Release;
135 } else if self.current_level <= self.params.sustain_level {
136 self.phase = AdsrPhase::Sustain;
137 }
138 }
139 AdsrPhase::Sustain => {
140 if !self.gate_active {
141 self.phase = AdsrPhase::Release;
142 }
143 }
145 AdsrPhase::Release => {
146 if self.gate_active {
147 self.phase = AdsrPhase::Attack;
149 } else if self.current_level <= 0.0 {
150 self.phase = AdsrPhase::Idle;
151 }
152 }
153 }
154
155 match self.phase {
157 AdsrPhase::Idle => {
158 self.current_level = 0.0;
159 }
160 AdsrPhase::Attack => {
161 let rate = 1.0 / self.params.attack_duration.max(0.001);
163 self.current_level += rate * self.delta_time;
164 self.current_level = self.current_level.min(1.0);
165 }
166 AdsrPhase::Decay => {
167 let dist = 1.0 - self.params.sustain_level;
169 let rate = dist / self.params.decay_duration.max(0.001);
170 self.current_level -= rate * self.delta_time;
171 self.current_level = self.current_level.max(self.params.sustain_level);
172 }
173 AdsrPhase::Sustain => {
174 self.current_level = self.params.sustain_level;
176 }
177 AdsrPhase::Release => {
178 let rate = 1.0 / self.params.release_duration.max(0.001);
182 self.current_level -= rate * self.delta_time;
183 self.current_level = self.current_level.max(0.0);
184 }
185 }
186
187 self.current_level = self.current_level.clamp(0.0, 1.0);
189
190 self.current_level
191 }
192
193 pub(crate) fn compute_running_average(&mut self, current_sample: [f32; FREQ_BINS]) {
194 if self.prev_averaging_samples != self.params.averaging_samples {
198 self.running_sum = (0..=self.params.averaging_samples)
218 .map(|i| self.sample_buffer.get_from_offset(i as i32))
219 .fold(Array1::zeros(FREQ_BINS), |acc, x| acc + x);
220
221 self.prev_averaging_samples = self.params.averaging_samples;
222 }
223
224 let current_rms_sample = Array1::from_vec(current_sample.to_vec());
225
226 let remove = self
228 .sample_buffer
229 .get_from_offset(-(self.params.averaging_samples as i32));
230 self.running_sum = &self.running_sum - &remove;
231 self.sample_buffer.progress();
233 self.sample_buffer.insert(current_rms_sample.clone());
235 self.running_sum = &self.running_sum + ¤t_rms_sample;
236
237 self.spectrum = &self.running_sum / self.params.averaging_samples as f32;
240 }
241
242 fn highpass(&mut self, x: f32) -> f32 {
243 let remove = self
244 .echo_buffer
245 .get_from_offset(-(self.params.echo_samples as i32));
246 self.running_impulse_sum -= remove;
247 self.echo_buffer.progress();
249 self.echo_buffer.insert(x);
251 self.running_impulse_sum += x;
252 let y = self.running_impulse_sum / (2.0 * self.params.echo_samples as f32);
253 x - y
254 }
255}
256
257#[derive(Clone, Debug, PartialEq)]
258struct PrimitiveRingBuffer<T>
259where
260 T: Clone,
261{
262 inner: Vec<T>,
263 capacity: usize,
264 front: usize,
265}
266
267impl<T> PrimitiveRingBuffer<T>
268where
269 T: Clone,
270{
271 pub(crate) fn new(default_elem: T, capacity: usize) -> Self {
272 Self {
273 inner: Vec::from_iter(std::iter::repeat_n(default_elem, capacity)),
274 capacity,
275 front: capacity.saturating_sub(1),
276 }
277 }
278 pub(crate) fn get_from_offset(&self, offset: i32) -> T {
279 self.inner[(self.front as i32 + offset).rem_euclid(self.capacity as i32) as usize].clone()
280 }
281 pub(crate) fn insert(&mut self, elem: T) {
282 self.inner[self.front] = elem;
283 }
284
285 pub(crate) fn progress(&mut self) {
286 self.front = (self.front + 1) % self.capacity;
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 fn run_for_with_impulse_at(run_for: usize, at: usize, impulse: f32) -> SoundTrigger {
295 let mut signal = SoundTrigger::new(SoundTriggerParams::default(), 0.01);
296 signal.params.averaging_samples = 1;
297 for i in 0..run_for {
298 let data = if i == at {
299 [impulse; FREQ_BINS]
300 } else {
301 [0f32; FREQ_BINS]
302 };
303 signal.tick(data);
304 }
305 signal
306 }
307
308 #[test]
309 fn test_running_sum_removal() {
310 let signal = run_for_with_impulse_at(4, 1, 1.0);
311 assert_eq!(signal.running_sum, Array1::from_vec(vec![0f32; FREQ_BINS]));
312 }
313
314 #[test]
315 fn test_wraparound() {
316 let mut signal = SoundTrigger::new(SoundTriggerParams::default(), 0.01);
317 signal.params.averaging_samples = 1;
318 for i in 0..(MAX_BUFFER_LENGTH + 10) {
319 signal.tick([1f32; FREQ_BINS]);
320 if i >= 2 {
321 assert_eq!(signal.running_sum, Array1::from_vec(vec![2f32; FREQ_BINS]))
322 }
323 }
324 }
325}