#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TransientShaperConfig {
pub attack_db: f32,
pub sustain_db: f32,
pub fast_time_ms: f32,
pub slow_time_ms: f32,
pub output_gain_db: f32,
pub sample_rate: f32,
}
impl Default for TransientShaperConfig {
fn default() -> Self {
Self {
attack_db: 0.0,
sustain_db: 0.0,
fast_time_ms: 1.0,
slow_time_ms: 20.0,
output_gain_db: 0.0,
sample_rate: 48000.0,
}
}
}
impl TransientShaperConfig {
#[must_use]
pub fn new(sample_rate: f32) -> Self {
Self {
sample_rate,
..Default::default()
}
}
#[must_use]
pub fn with_attack(mut self, db: f32) -> Self {
self.attack_db = db.clamp(-24.0, 24.0);
self
}
#[must_use]
pub fn with_sustain(mut self, db: f32) -> Self {
self.sustain_db = db.clamp(-24.0, 24.0);
self
}
#[must_use]
pub fn with_output_gain(mut self, db: f32) -> Self {
self.output_gain_db = db.clamp(-24.0, 24.0);
self
}
#[must_use]
pub fn with_fast_time(mut self, ms: f32) -> Self {
self.fast_time_ms = ms.clamp(0.1, 10.0);
self
}
#[must_use]
pub fn with_slow_time(mut self, ms: f32) -> Self {
self.slow_time_ms = ms.clamp(5.0, 200.0);
self
}
}
#[allow(clippy::cast_precision_loss)]
fn db_to_lin(db: f32) -> f32 {
10.0f32.powf(db / 20.0)
}
#[allow(clippy::cast_precision_loss)]
fn coeff_from_ms(ms: f32, sr: f32) -> f32 {
if ms <= 0.0 || sr <= 0.0 {
return 0.0;
}
let n = ms * 0.001 * sr;
(-1.0f32 / n).exp()
}
#[derive(Debug, Clone)]
pub struct EnvelopeFollower {
value: f32,
attack_coeff: f32,
release_coeff: f32,
}
impl EnvelopeFollower {
#[must_use]
pub fn new(time_ms: f32, sample_rate: f32) -> Self {
let c = coeff_from_ms(time_ms, sample_rate);
Self {
value: 0.0,
attack_coeff: c,
release_coeff: c,
}
}
pub fn process(&mut self, input_abs: f32) -> f32 {
if input_abs > self.value {
self.value = self.attack_coeff * self.value + (1.0 - self.attack_coeff) * input_abs;
} else {
self.value = self.release_coeff * self.value + (1.0 - self.release_coeff) * input_abs;
}
self.value
}
pub fn reset(&mut self) {
self.value = 0.0;
}
#[must_use]
pub fn value(&self) -> f32 {
self.value
}
}
#[derive(Debug)]
pub struct TransientShaper {
config: TransientShaperConfig,
fast_env: EnvelopeFollower,
slow_env: EnvelopeFollower,
attack_gain: f32,
sustain_gain: f32,
output_gain: f32,
}
impl TransientShaper {
#[must_use]
pub fn new(config: TransientShaperConfig) -> Self {
let fast_env = EnvelopeFollower::new(config.fast_time_ms, config.sample_rate);
let slow_env = EnvelopeFollower::new(config.slow_time_ms, config.sample_rate);
let attack_gain = db_to_lin(config.attack_db);
let sustain_gain = db_to_lin(config.sustain_db);
let output_gain = db_to_lin(config.output_gain_db);
Self {
config,
fast_env,
slow_env,
attack_gain,
sustain_gain,
output_gain,
}
}
pub fn process_sample(&mut self, input: f32) -> f32 {
let abs_in = input.abs();
let fast = self.fast_env.process(abs_in);
let slow = self.slow_env.process(abs_in);
let transient_diff = (fast - slow).max(0.0);
let sustain_level = slow;
let transient_boost = transient_diff * (self.attack_gain - 1.0);
let sustain_boost = sustain_level * (self.sustain_gain - 1.0);
let sign = if input >= 0.0 { 1.0 } else { -1.0 };
let output = input + sign * (transient_boost + sustain_boost);
output * self.output_gain
}
pub fn process(&mut self, buffer: &mut [f32]) {
for sample in buffer.iter_mut() {
*sample = self.process_sample(*sample);
}
}
pub fn process_stereo(&mut self, left: &mut [f32], right: &mut [f32]) {
let len = left.len().min(right.len());
for i in 0..len {
left[i] = self.process_sample(left[i]);
right[i] = self.process_sample(right[i]);
}
}
pub fn reset(&mut self) {
self.fast_env.reset();
self.slow_env.reset();
}
pub fn set_attack_db(&mut self, db: f32) {
self.config.attack_db = db.clamp(-24.0, 24.0);
self.attack_gain = db_to_lin(self.config.attack_db);
}
pub fn set_sustain_db(&mut self, db: f32) {
self.config.sustain_db = db.clamp(-24.0, 24.0);
self.sustain_gain = db_to_lin(self.config.sustain_db);
}
#[must_use]
pub fn fast_envelope(&self) -> f32 {
self.fast_env.value()
}
#[must_use]
pub fn slow_envelope(&self) -> f32 {
self.slow_env.value()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_db_to_lin_zero() {
assert!((db_to_lin(0.0) - 1.0).abs() < 1e-5);
}
#[test]
fn test_db_to_lin_positive() {
let v = db_to_lin(6.0);
assert!(v > 1.5 && v < 2.1);
}
#[test]
fn test_db_to_lin_negative() {
let v = db_to_lin(-6.0);
assert!(v > 0.4 && v < 0.6);
}
#[test]
fn test_coeff_from_ms() {
let c = coeff_from_ms(10.0, 48000.0);
assert!(c > 0.0 && c < 1.0);
}
#[test]
fn test_coeff_from_ms_zero() {
let c = coeff_from_ms(0.0, 48000.0);
assert!((c - 0.0).abs() < 1e-5);
}
#[test]
fn test_envelope_follower_tracks_up() {
let mut env = EnvelopeFollower::new(1.0, 48000.0);
for _ in 0..480 {
env.process(1.0);
}
assert!(env.value() > 0.9);
}
#[test]
fn test_envelope_follower_tracks_down() {
let mut env = EnvelopeFollower::new(1.0, 48000.0);
for _ in 0..480 {
env.process(1.0);
}
for _ in 0..4800 {
env.process(0.0);
}
assert!(env.value() < 0.1);
}
#[test]
fn test_envelope_follower_reset() {
let mut env = EnvelopeFollower::new(1.0, 48000.0);
env.process(1.0);
env.reset();
assert!((env.value() - 0.0).abs() < 1e-10);
}
#[test]
fn test_shaper_passthrough() {
let config = TransientShaperConfig {
attack_db: 0.0,
sustain_db: 0.0,
output_gain_db: 0.0,
..TransientShaperConfig::new(48000.0)
};
let mut shaper = TransientShaper::new(config);
let mut buf = vec![0.5f32; 4800];
shaper.process(&mut buf);
let last = buf[buf.len() - 1];
assert!((last - 0.5).abs() < 0.15, "got {last}");
}
#[test]
fn test_shaper_attack_boost() {
let config = TransientShaperConfig::new(48000.0).with_attack(12.0);
let mut shaper = TransientShaper::new(config);
let silence = vec![0.0f32; 480];
let mut impulse = vec![0.0f32; 480];
impulse[0] = 1.0;
for &s in &silence {
shaper.process_sample(s);
}
let out = shaper.process_sample(impulse[0]);
assert!(
out.abs() >= 1.0,
"boosted transient should be >= 1.0, got {out}"
);
}
#[test]
fn test_shaper_reset() {
let mut shaper = TransientShaper::new(TransientShaperConfig::default());
shaper.process_sample(1.0);
shaper.reset();
assert!((shaper.fast_envelope() - 0.0).abs() < 1e-10);
assert!((shaper.slow_envelope() - 0.0).abs() < 1e-10);
}
#[test]
fn test_set_attack_db() {
let mut shaper = TransientShaper::new(TransientShaperConfig::default());
shaper.set_attack_db(6.0);
assert!((shaper.config.attack_db - 6.0).abs() < 1e-5);
}
#[test]
fn test_set_sustain_db() {
let mut shaper = TransientShaper::new(TransientShaperConfig::default());
shaper.set_sustain_db(-3.0);
assert!((shaper.config.sustain_db - (-3.0)).abs() < 1e-5);
}
#[test]
fn test_config_builder() {
let cfg = TransientShaperConfig::new(44100.0)
.with_attack(6.0)
.with_sustain(-3.0)
.with_output_gain(1.0)
.with_fast_time(2.0)
.with_slow_time(30.0);
assert!((cfg.attack_db - 6.0).abs() < 1e-5);
assert!((cfg.sustain_db - (-3.0)).abs() < 1e-5);
assert!((cfg.output_gain_db - 1.0).abs() < 1e-5);
assert!((cfg.fast_time_ms - 2.0).abs() < 1e-5);
assert!((cfg.slow_time_ms - 30.0).abs() < 1e-5);
}
#[test]
fn test_process_stereo() {
let mut shaper = TransientShaper::new(TransientShaperConfig::default());
let mut left = vec![0.3f32; 100];
let mut right = vec![0.3f32; 100];
shaper.process_stereo(&mut left, &mut right);
for &s in &left {
assert!(s.is_finite());
}
for &s in &right {
assert!(s.is_finite());
}
}
}