#[derive(Debug, Clone)]
pub struct AudioFingerprint {
pub sample_rate: u32,
pub channel_count: u32,
pub length: u32,
pub data: Vec<f32>,
}
#[derive(Debug, Clone, Copy)]
pub enum WaveType {
Sine,
Triangle,
Square,
Sawtooth,
}
#[derive(Debug, Clone)]
pub struct AudioParams {
pub sample_rate: u32,
pub length: u32,
pub frequency: f64,
pub wave_type: WaveType,
pub threshold: f64,
pub knee: f64,
pub ratio: f64,
pub attack: f64,
pub release: f64,
}
impl Default for AudioParams {
fn default() -> Self {
Self {
sample_rate: 44100,
length: 5000,
frequency: 10000.0,
wave_type: WaveType::Triangle,
threshold: -24.0,
knee: 30.0,
ratio: 12.0,
attack: 0.003,
release: 0.25,
}
}
}
impl AudioFingerprint {
pub fn from_seed(seed: u64) -> Self {
Self::from_params(seed, AudioParams::default())
}
pub fn default_fingerprint() -> Self {
Self::from_seed(0)
}
pub fn from_params(seed: u64, params: AudioParams) -> Self {
let sample_rate = params.sample_rate;
let length = params.length as usize;
let seed_f = seed as f32;
let seed_phase_offset =
((seed_f * 0.123_456_79).sin() + (seed_f * 0.987_654_3).cos()) * 1.0e-7;
let seed_gain = 1.0 + ((seed_f * 0.246_813_6).sin()) * 1.0e-6;
let padded_len = length.div_ceil(N_DIVISION_FRAMES) * N_DIVISION_FRAMES;
let mut input = vec![0.0f32; padded_len];
const BLINK_OSCILLATOR_SCALE: f32 = 0.47624;
let wave = crate::canvas::periodic_wave::PeriodicWave::new(
match params.wave_type {
WaveType::Sine => crate::canvas::periodic_wave::StandardWaveType::Sine,
WaveType::Triangle => crate::canvas::periodic_wave::StandardWaveType::Triangle,
WaveType::Square => crate::canvas::periodic_wave::StandardWaveType::Square,
WaveType::Sawtooth => crate::canvas::periodic_wave::StandardWaveType::Sawtooth,
},
sample_rate as f32,
);
let frequency = params.frequency as f32;
let phase_increment = frequency / sample_rate as f32;
let mut phase: f32 = 0.0;
for slot in input.iter_mut().take(length) {
let effective_phase = (phase + seed_phase_offset).rem_euclid(1.0);
*slot = wave.sample(effective_phase, frequency) * BLINK_OSCILLATOR_SCALE * seed_gain;
phase += phase_increment;
if phase >= 1.0 {
phase -= 1.0;
}
}
let mut kernel = DynamicsCompressorKernel::new(sample_rate as f32, 1);
let mut output = vec![0.0f32; padded_len];
kernel.process(
&[&input],
&mut [&mut output],
padded_len,
params.threshold as f32,
params.knee as f32,
params.ratio as f32,
params.attack as f32,
params.release as f32,
0.006,
0.0,
1.0,
0.09,
0.16,
0.42,
0.98,
);
output.truncate(length);
Self {
sample_rate,
channel_count: 1,
length: params.length,
data: output,
}
}
}
const MAX_PRE_DELAY_FRAMES: usize = 1024;
const MAX_PRE_DELAY_FRAMES_MASK: usize = MAX_PRE_DELAY_FRAMES - 1;
const DEFAULT_PRE_DELAY_FRAMES: usize = 256;
const METERING_RELEASE_TIME_CONSTANT: f32 = 0.325;
const N_DIVISION_FRAMES: usize = 32;
const K_SPACING_DB: f32 = 5.0;
const SAT_RELEASE_TIME: f32 = 0.0025;
const UNINITIALIZED: f32 = -1.0;
struct DynamicsCompressorKernel {
sample_rate: f32,
detector_average: f32,
compressor_gain: f32,
metering_release_k: f32,
metering_gain: f32,
pre_delay_buffers: Vec<Vec<f32>>,
pre_delay_read_index: usize,
pre_delay_write_index: usize,
last_pre_delay_frames: usize,
max_attack_compression_diff_db: f32,
ratio: f32,
slope: f32,
linear_threshold: f32,
db_threshold: f32,
db_knee: f32,
knee_threshold: f32,
knee_threshold_db: f32,
ykneee_threshold_db: f32,
k_cached: f32,
}
#[inline]
fn linear_to_decibels(linear: f32) -> f32 {
20.0 * linear.log10()
}
#[inline]
fn decibels_to_linear(db: f32) -> f32 {
10.0f32.powf(0.05 * db)
}
#[inline]
fn discrete_time_constant_for_sample_rate(tc: f32, sample_rate: f32) -> f32 {
1.0 - (-1.0 / (sample_rate * tc)).exp()
}
#[inline]
fn flush_denormal(x: f32) -> f32 {
if x.abs() < f32::MIN_POSITIVE { 0.0 } else { x }
}
impl DynamicsCompressorKernel {
fn new(sample_rate: f32, number_of_channels: usize) -> Self {
let metering_release_k =
discrete_time_constant_for_sample_rate(METERING_RELEASE_TIME_CONSTANT, sample_rate);
let mut k = Self {
sample_rate,
detector_average: 0.0,
compressor_gain: 1.0,
metering_release_k,
metering_gain: 1.0,
pre_delay_buffers: (0..number_of_channels)
.map(|_| vec![0.0; MAX_PRE_DELAY_FRAMES])
.collect(),
pre_delay_read_index: 0,
pre_delay_write_index: DEFAULT_PRE_DELAY_FRAMES,
last_pre_delay_frames: DEFAULT_PRE_DELAY_FRAMES,
max_attack_compression_diff_db: -1.0,
ratio: UNINITIALIZED,
slope: UNINITIALIZED,
linear_threshold: UNINITIALIZED,
db_threshold: UNINITIALIZED,
db_knee: UNINITIALIZED,
knee_threshold: UNINITIALIZED,
knee_threshold_db: UNINITIALIZED,
ykneee_threshold_db: UNINITIALIZED,
k_cached: UNINITIALIZED,
};
k.reset();
k
}
fn reset(&mut self) {
self.detector_average = 0.0;
self.compressor_gain = 1.0;
self.metering_gain = 1.0;
for buf in &mut self.pre_delay_buffers {
for s in &mut *buf {
*s = 0.0;
}
}
self.pre_delay_read_index = 0;
self.pre_delay_write_index = DEFAULT_PRE_DELAY_FRAMES;
self.max_attack_compression_diff_db = -1.0;
}
fn set_pre_delay_time(&mut self, pre_delay_time: f32) {
let mut pre_delay_frames = (pre_delay_time * self.sample_rate) as usize;
if pre_delay_frames > MAX_PRE_DELAY_FRAMES - 1 {
pre_delay_frames = MAX_PRE_DELAY_FRAMES - 1;
}
if self.last_pre_delay_frames != pre_delay_frames {
self.last_pre_delay_frames = pre_delay_frames;
for buf in &mut self.pre_delay_buffers {
for s in &mut *buf {
*s = 0.0;
}
}
self.pre_delay_read_index = 0;
self.pre_delay_write_index = pre_delay_frames;
}
}
fn knee_curve(&self, x: f32, k: f32) -> f32 {
if x < self.linear_threshold {
return x;
}
self.linear_threshold + (1.0 - (-k * (x - self.linear_threshold)).exp()) / k
}
fn saturate(&self, x: f32, k: f32) -> f32 {
if x < self.knee_threshold {
self.knee_curve(x, k)
} else {
let x_db = linear_to_decibels(x);
let y_db = self.ykneee_threshold_db + self.slope * (x_db - self.knee_threshold_db);
decibels_to_linear(y_db)
}
}
fn slope_at(&self, x: f32, k: f32) -> f32 {
if x < self.linear_threshold {
return 1.0;
}
let x2 = x * 1.001;
let x_db = linear_to_decibels(x);
let x2_db = linear_to_decibels(x2);
let y_db = linear_to_decibels(self.knee_curve(x, k));
let y2_db = linear_to_decibels(self.knee_curve(x2, k));
(y2_db - y_db) / (x2_db - x_db)
}
fn k_at_slope(&self, desired_slope: f32) -> f32 {
let x_db = self.db_threshold + self.db_knee;
let x = decibels_to_linear(x_db);
let mut min_k = 0.1_f32;
let mut max_k = 10_000.0_f32;
let mut k = 5.0_f32;
for _ in 0..15 {
let slope = self.slope_at(x, k);
if slope < desired_slope {
max_k = k;
} else {
min_k = k;
}
k = (min_k * max_k).sqrt();
}
k
}
fn update_static_curve_parameters(
&mut self,
db_threshold: f32,
db_knee: f32,
ratio: f32,
) -> f32 {
if db_threshold != self.db_threshold || db_knee != self.db_knee || ratio != self.ratio {
self.db_threshold = db_threshold;
self.linear_threshold = decibels_to_linear(db_threshold);
self.db_knee = db_knee;
self.ratio = ratio;
self.slope = 1.0 / ratio;
let k = self.k_at_slope(1.0 / ratio);
self.knee_threshold_db = db_threshold + db_knee;
self.knee_threshold = decibels_to_linear(self.knee_threshold_db);
self.ykneee_threshold_db = linear_to_decibels(self.knee_curve(self.knee_threshold, k));
self.k_cached = k;
}
self.k_cached
}
#[allow(clippy::needless_range_loop)]
fn process(
&mut self,
source_channels: &[&[f32]],
destination_channels: &mut [&mut [f32]],
frames_to_process: usize,
db_threshold: f32,
db_knee: f32,
ratio: f32,
attack_time: f32,
release_time: f32,
pre_delay_time: f32,
db_post_gain: f32,
effect_blend: f32,
release_zone1: f32,
release_zone2: f32,
release_zone3: f32,
release_zone4: f32,
) {
let sample_rate = self.sample_rate;
let dry_mix = 1.0 - effect_blend;
let wet_mix = effect_blend;
let k = self.update_static_curve_parameters(db_threshold, db_knee, ratio);
let full_range_gain = self.saturate(1.0, k);
let mut full_range_makeup_gain = 1.0 / full_range_gain;
let exponent = if db_threshold <= -24.0 {
0.6 + 0.0739 * ((-db_threshold - 24.0) / 26.0)
} else {
0.6
};
full_range_makeup_gain = full_range_makeup_gain.powf(exponent);
let master_linear_gain = decibels_to_linear(db_post_gain) * full_range_makeup_gain;
let attack_time = attack_time.max(0.001);
let attack_frames = attack_time * sample_rate;
let release_frames = sample_rate * release_time;
let sat_release_frames = SAT_RELEASE_TIME * sample_rate;
let y1 = release_frames * release_zone1;
let y2 = release_frames * release_zone2;
let y3 = release_frames * release_zone3;
let y4 = release_frames * release_zone4;
let ka = 0.999_999_999_999_999_8 * y1 + 1.843_221_9e-16 * y2 - 1.937_339_4e-16 * y3
+ 8.824_516e-18 * y4;
let kb = -1.578_832 * y1 + 2.330_583_8 * y2 - 0.914_119_4 * y3 + 0.162_367_75 * y4;
let kc = 0.533_414_3 * y1 - 1.272_736_8 * y2 + 0.925_885_6 * y3 - 0.186_563_1 * y4;
let kd = 0.087_834_634 * y1 - 0.169_416_3 * y2 + 0.085_880_58 * y3 - 0.004_298_914 * y4;
let ke = -0.042_416_88 * y1 + 0.111_569_38 * y2 - 0.097_646_765 * y3 + 0.028_494_263 * y4;
self.set_pre_delay_time(pre_delay_time);
let n_divisions = frames_to_process / N_DIVISION_FRAMES;
let mut frame_index: usize = 0;
for _ in 0..n_divisions {
if self.detector_average.is_nan() {
self.detector_average = 1.0;
}
if self.detector_average.is_infinite() {
self.detector_average = 1.0;
}
let desired_gain = self.detector_average;
let scaled_desired_gain = desired_gain.asin() / (0.5 * std::f32::consts::PI);
let is_releasing = scaled_desired_gain > self.compressor_gain;
let mut compression_diff_db =
linear_to_decibels(self.compressor_gain / scaled_desired_gain);
let envelope_rate;
if is_releasing {
self.max_attack_compression_diff_db = -1.0;
if compression_diff_db.is_nan() {
compression_diff_db = -1.0;
}
if compression_diff_db.is_infinite() {
compression_diff_db = -1.0;
}
let mut x = compression_diff_db;
x = x.max(-12.0);
x = x.min(0.0);
x = 0.25 * (x + 12.0);
let x2 = x * x;
let x3 = x2 * x;
let x4 = x2 * x2;
let release_frames_adaptive = ka + kb * x + kc * x2 + kd * x3 + ke * x4;
let db_per_frame = K_SPACING_DB / release_frames_adaptive;
envelope_rate = decibels_to_linear(db_per_frame);
} else {
if compression_diff_db.is_nan() {
compression_diff_db = 1.0;
}
if compression_diff_db.is_infinite() {
compression_diff_db = 1.0;
}
if self.max_attack_compression_diff_db == -1.0
|| self.max_attack_compression_diff_db < compression_diff_db
{
self.max_attack_compression_diff_db = compression_diff_db;
}
let eff_atten_diff_db = self.max_attack_compression_diff_db.max(0.5);
let x = 0.25 / eff_atten_diff_db;
envelope_rate = 1.0 - x.powf(1.0 / attack_frames);
}
let mut pre_delay_read_index = self.pre_delay_read_index;
let mut pre_delay_write_index = self.pre_delay_write_index;
let mut detector_average = self.detector_average;
let mut compressor_gain = self.compressor_gain;
for _ in 0..N_DIVISION_FRAMES {
let mut compressor_input: f32 = 0.0;
for ch in 0..source_channels.len() {
let undelayed_source = source_channels[ch][frame_index];
self.pre_delay_buffers[ch][pre_delay_write_index] = undelayed_source;
let abs_undelayed = undelayed_source.abs();
if compressor_input < abs_undelayed {
compressor_input = abs_undelayed;
}
}
let scaled_input = compressor_input;
let abs_input = scaled_input.abs();
let shaped_input = self.saturate(abs_input, k);
let attenuation = if abs_input <= 0.0001 {
1.0
} else {
shaped_input / abs_input
};
let mut attenuation_db = -linear_to_decibels(attenuation);
attenuation_db = attenuation_db.max(2.0);
let db_per_frame_inner = attenuation_db / sat_release_frames;
let sat_release_rate = decibels_to_linear(db_per_frame_inner) - 1.0;
let is_release = attenuation > detector_average;
let rate = if is_release { sat_release_rate } else { 1.0 };
detector_average += (attenuation - detector_average) * rate;
detector_average = detector_average.min(1.0);
if detector_average.is_nan() {
detector_average = 1.0;
}
if detector_average.is_infinite() {
detector_average = 1.0;
}
if envelope_rate < 1.0 {
compressor_gain += (scaled_desired_gain - compressor_gain) * envelope_rate;
} else {
compressor_gain *= envelope_rate;
compressor_gain = compressor_gain.min(1.0);
}
let post_warp_compressor_gain =
(0.5 * std::f32::consts::PI * compressor_gain).sin();
let total_gain = dry_mix + wet_mix * master_linear_gain * post_warp_compressor_gain;
let db_real_gain = 20.0 * post_warp_compressor_gain.log10();
if db_real_gain < self.metering_gain {
self.metering_gain = db_real_gain;
} else {
self.metering_gain +=
(db_real_gain - self.metering_gain) * self.metering_release_k;
}
for ch in 0..destination_channels.len() {
destination_channels[ch][frame_index] =
self.pre_delay_buffers[ch][pre_delay_read_index] * total_gain;
}
frame_index += 1;
pre_delay_read_index = (pre_delay_read_index + 1) & MAX_PRE_DELAY_FRAMES_MASK;
pre_delay_write_index = (pre_delay_write_index + 1) & MAX_PRE_DELAY_FRAMES_MASK;
}
self.pre_delay_read_index = pre_delay_read_index;
self.pre_delay_write_index = pre_delay_write_index;
self.detector_average = flush_denormal(detector_average);
self.compressor_gain = flush_denormal(compressor_gain);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deterministic_output() {
let a = AudioFingerprint::from_seed(42);
let b = AudioFingerprint::from_seed(42);
assert_eq!(a.data, b.data);
}
#[test]
fn different_seeds_different_output() {
let a = AudioFingerprint::from_seed(0);
let b = AudioFingerprint::from_seed(1);
let diffs = a
.data
.iter()
.zip(b.data.iter())
.filter(|(a, b)| (**a - **b).abs() > 1e-10)
.count();
assert!(diffs > 0);
}
#[test]
fn correct_length() {
let fp = AudioFingerprint::default_fingerprint();
assert_eq!(fp.data.len(), 5000);
assert_eq!(fp.sample_rate, 44100);
assert_eq!(fp.channel_count, 1);
}
#[test]
fn samples_in_range() {
let fp = AudioFingerprint::default_fingerprint();
for &s in &fp.data {
assert!(s.abs() <= 1.5);
}
}
}