use super::trigger_params::FREQ_BINS;
pub use super::trigger_params::SoundTriggerParams;
use ndarray::Array1;
use std::ops::Mul;
pub(crate) const SAMPLE_INTERVAL_MS: u64 = 10;
const MAX_BUFFER_LENGTH: usize = 1001;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub(crate) enum AdsrPhase {
#[default]
Idle,
Attack,
Decay,
Sustain,
Release,
}
#[derive(Debug, PartialEq, Clone)]
pub struct SoundTrigger {
pub(crate) phase: AdsrPhase,
pub(crate) gate_active: bool,
pub params: SoundTriggerParams,
pub impulse: f32,
pub(crate) 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(),
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);
}
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(crate) 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(crate) 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(crate) 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(crate) fn insert(&mut self, elem: T) {
self.inner[self.front] = elem;
}
pub(crate) 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]))
}
}
}
}