use crate::audio::SOUND_TRIGGER_SAMPLE_INTERVAL_MS;
use crate::audio::fft::{FREQ_BINS, MAX_FREQ};
use ndarray::Array1;
use rustfft::num_traits::Float;
use serde::{Deserialize, Serialize};
use std::ops::Mul;
const MAX_BUFFER_LENGTH: usize = 1001;
#[derive(Debug, Clone, PartialEq, Copy, Serialize, Deserialize)]
pub struct SoundTriggerParams {
center_bin: usize,
bin_radius: usize,
pub attack_duration: f32, pub decay_duration: f32, pub sustain_level: f32, pub release_duration: f32, pub sensitivity: f32,
pub gate_activation_threshold: f32,
pub gate_deactivation_threshold: f32,
pub averaging_samples: usize,
echo_samples: usize,
}
impl SoundTriggerParams {
#[allow(clippy::too_many_arguments)]
pub fn new(
f_center: f32,
f_radius: f32,
attack_duration: f32,
decay_duration: f32,
sustain_level: f32,
release_duration: f32,
sensitivity: f32,
gate_threshold: f32,
) -> Self {
let mut ret = Self {
center_bin: 0,
bin_radius: 0,
attack_duration,
decay_duration,
sustain_level,
release_duration,
sensitivity,
gate_activation_threshold: gate_threshold,
gate_deactivation_threshold: gate_threshold * 0.8,
averaging_samples: 10,
echo_samples: 20,
};
ret.set_filter_tune(f_center, f_radius);
ret
}
pub fn set_filter_tune(&mut self, f_center: f32, f_radius: f32) {
let f_per_bin = MAX_FREQ / FREQ_BINS as f32;
let center_bin = ((f_center / f_per_bin).round() as usize).min(FREQ_BINS - 1);
let bin_radius = (f_radius / f_per_bin).round() as usize;
self.center_bin = center_bin;
self.bin_radius = bin_radius.max(1);
self.gate_deactivation_threshold = self.gate_activation_threshold * 0.9;
}
}
impl Default for SoundTriggerParams {
fn default() -> Self {
SoundTriggerParams::new(100., 50., 0.01, 0.0, 1.0, 0.1, 1., 0.5)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum AdsrPhase {
#[default]
Idle,
Attack,
Decay,
Sustain,
Release,
}
#[derive(Debug, PartialEq, Clone)]
pub struct SoundTrigger {
pub phase: AdsrPhase,
pub gate_active: bool,
pub params: SoundTriggerParams,
pub impulse: f32,
pub delta_time: f32,
pub spectrum: Array1<f32>,
pub current_level: f32,
sample_buffer: PrimitiveRingBuffer<Array1<f32>>,
running_sum: Array1<f32>,
prev_averaging_samples: usize,
echo_buffer: PrimitiveRingBuffer<f32>,
running_impulse_sum: f32,
}
impl Default for SoundTrigger {
fn default() -> Self {
SoundTrigger::new(
SoundTriggerParams::default(),
SOUND_TRIGGER_SAMPLE_INTERVAL_MS as f32 / 1000.,
)
}
}
impl SoundTrigger {
pub fn new(params: SoundTriggerParams, delta_time: f32) -> Self {
Self {
delta_time,
gate_active: false,
spectrum: Array1::zeros(FREQ_BINS),
phase: AdsrPhase::Idle,
params,
current_level: 0.0,
impulse: 0.0,
sample_buffer: PrimitiveRingBuffer::new(Array1::zeros(FREQ_BINS), MAX_BUFFER_LENGTH),
running_sum: Array1::zeros(FREQ_BINS),
prev_averaging_samples: params.averaging_samples,
echo_buffer: PrimitiveRingBuffer::new(0.0, MAX_BUFFER_LENGTH),
running_impulse_sum: 0.0,
}
}
pub fn reset(&mut self) {
self.phase = AdsrPhase::Idle;
self.gate_active = false;
self.current_level = 0.0;
self.impulse = 0.0;
self.sample_buffer = PrimitiveRingBuffer::new(Array1::zeros(FREQ_BINS), MAX_BUFFER_LENGTH);
self.running_sum = Array1::zeros(FREQ_BINS);
self.spectrum = Array1::zeros(FREQ_BINS);
}
#[cfg_attr(feature = "profiling", profiling::function)]
pub fn tick(&mut self, samples: [f32; FREQ_BINS]) {
self.compute_running_average(samples);
self.spectrum.map_inplace(|x| {
*x = x.mul(self.delta_time * 3.0).clamp(0.0, f32::infinity());
});
let max_f = (self.params.center_bin + self.params.bin_radius).clamp(0, FREQ_BINS - 1);
let start = self
.params
.center_bin
.saturating_sub(self.params.bin_radius)
.clamp(1, FREQ_BINS - 1);
let len = max_f.saturating_sub(start);
self.impulse = (self
.spectrum
.iter()
.skip(start)
.take(len)
.map(|x| x.powf(2.))
.sum::<f32>()
/ (2. * self.params.bin_radius as f32))
.sqrt()
.mul(4.0);
self.impulse = self.highpass(self.impulse);
self.impulse = self.impulse.mul(self.params.sensitivity);
self.tick_adsr(self.impulse);
}
pub fn tick_adsr(&mut self, input: f32) -> f32 {
self.gate_active = if self.gate_active {
input > self.params.gate_deactivation_threshold
} else {
input > self.params.gate_activation_threshold
};
match self.phase {
AdsrPhase::Idle => {
if self.gate_active {
self.phase = AdsrPhase::Attack;
}
}
AdsrPhase::Attack => {
if !self.gate_active {
self.phase = AdsrPhase::Release;
} else if self.current_level >= 1.0 {
self.phase = AdsrPhase::Decay;
}
}
AdsrPhase::Decay => {
if !self.gate_active {
self.phase = AdsrPhase::Release;
} else if self.current_level <= self.params.sustain_level {
self.phase = AdsrPhase::Sustain;
}
}
AdsrPhase::Sustain => {
if !self.gate_active {
self.phase = AdsrPhase::Release;
}
}
AdsrPhase::Release => {
if self.gate_active {
self.phase = AdsrPhase::Attack;
} else if self.current_level <= 0.0 {
self.phase = AdsrPhase::Idle;
}
}
}
match self.phase {
AdsrPhase::Idle => {
self.current_level = 0.0;
}
AdsrPhase::Attack => {
let rate = 1.0 / self.params.attack_duration.max(0.001);
self.current_level += rate * self.delta_time;
self.current_level = self.current_level.min(1.0);
}
AdsrPhase::Decay => {
let dist = 1.0 - self.params.sustain_level;
let rate = dist / self.params.decay_duration.max(0.001);
self.current_level -= rate * self.delta_time;
self.current_level = self.current_level.max(self.params.sustain_level);
}
AdsrPhase::Sustain => {
self.current_level = self.params.sustain_level;
}
AdsrPhase::Release => {
let rate = 1.0 / self.params.release_duration.max(0.001);
self.current_level -= rate * self.delta_time;
self.current_level = self.current_level.max(0.0);
}
}
self.current_level = self.current_level.clamp(0.0, 1.0);
self.current_level
}
pub fn compute_running_average(&mut self, current_sample: [f32; FREQ_BINS]) {
if self.prev_averaging_samples != self.params.averaging_samples {
self.running_sum = (0..=self.params.averaging_samples)
.map(|i| self.sample_buffer.get_from_offset(i as i32))
.fold(Array1::zeros(FREQ_BINS), |acc, x| acc + x);
self.prev_averaging_samples = self.params.averaging_samples;
}
let current_rms_sample = Array1::from_vec(current_sample.to_vec());
let remove = self
.sample_buffer
.get_from_offset(-(self.params.averaging_samples as i32));
self.running_sum = &self.running_sum - &remove;
self.sample_buffer.progress();
self.sample_buffer.insert(current_rms_sample.clone());
self.running_sum = &self.running_sum + ¤t_rms_sample;
self.spectrum = &self.running_sum / self.params.averaging_samples as f32;
}
fn highpass(&mut self, x: f32) -> f32 {
let remove = self
.echo_buffer
.get_from_offset(-(self.params.echo_samples as i32));
self.running_impulse_sum -= remove;
self.echo_buffer.progress();
self.echo_buffer.insert(x);
self.running_impulse_sum += x;
let y = self.running_impulse_sum / (2.0 * self.params.echo_samples as f32);
x - y
}
}
#[derive(Clone, Debug, PartialEq)]
struct PrimitiveRingBuffer<T>
where
T: Clone,
{
inner: Vec<T>,
capacity: usize,
front: usize,
}
impl<T> PrimitiveRingBuffer<T>
where
T: Clone,
{
pub fn new(default_elem: T, capacity: usize) -> Self {
Self {
inner: Vec::from_iter(std::iter::repeat_n(default_elem, capacity)),
capacity,
front: capacity.saturating_sub(1),
}
}
pub fn get_from_offset(&self, offset: i32) -> T {
self.inner[(self.front as i32 + offset).rem_euclid(self.capacity as i32) as usize].clone()
}
pub fn insert(&mut self, elem: T) {
self.inner[self.front] = elem;
}
pub fn progress(&mut self) {
self.front = (self.front + 1) % self.capacity;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run_for_with_impulse_at(run_for: usize, at: usize, impulse: f32) -> SoundTrigger {
let mut signal = SoundTrigger::new(SoundTriggerParams::default(), 0.01);
signal.params.averaging_samples = 1;
for i in 0..run_for {
let data = if i == at {
[impulse; FREQ_BINS]
} else {
[0f32; FREQ_BINS]
};
signal.tick(data);
}
signal
}
#[test]
fn test_running_sum_removal() {
let signal = run_for_with_impulse_at(4, 1, 1.0);
assert_eq!(signal.running_sum, Array1::from_vec(vec![0f32; FREQ_BINS]));
}
#[test]
fn test_wraparound() {
let mut signal = SoundTrigger::new(SoundTriggerParams::default(), 0.01);
signal.params.averaging_samples = 1;
for i in 0..(MAX_BUFFER_LENGTH + 10) {
signal.tick([1f32; FREQ_BINS]);
if i >= 2 {
assert_eq!(signal.running_sum, Array1::from_vec(vec![2f32; FREQ_BINS]))
}
}
}
}