use crate::{
audio::AudioBuffer,
error::Result,
plugins::{AudioEffect, ParameterDefinition, ParameterType, ParameterValue, VoirsPlugin},
VoirsError,
};
use async_trait::async_trait;
use std::{collections::HashMap, sync::RwLock};
pub struct CompressorEffect {
pub threshold: RwLock<f32>,
pub ratio: RwLock<f32>,
pub attack: RwLock<f32>,
pub release: RwLock<f32>,
pub makeup_gain: RwLock<f32>,
envelope: RwLock<f32>,
sample_rate: RwLock<Option<u32>>,
}
impl CompressorEffect {
pub fn new() -> Self {
Self {
threshold: RwLock::new(-20.0),
ratio: RwLock::new(4.0),
attack: RwLock::new(5.0),
release: RwLock::new(50.0),
makeup_gain: RwLock::new(0.0),
envelope: RwLock::new(0.0),
sample_rate: RwLock::new(None),
}
}
fn calculate_gain_reduction(&self, input_level_db: f32) -> f32 {
let threshold = *self.threshold.read().expect("lock should not be poisoned");
let ratio = *self.ratio.read().expect("lock should not be poisoned");
if input_level_db <= threshold {
0.0 } else {
let excess_db = input_level_db - threshold;
excess_db - (excess_db / ratio)
}
}
}
impl Default for CompressorEffect {
fn default() -> Self {
Self::new()
}
}
impl VoirsPlugin for CompressorEffect {
fn name(&self) -> &str {
"Compressor"
}
fn version(&self) -> &str {
"1.0.0"
}
fn description(&self) -> &str {
"Dynamic range compressor for controlling audio dynamics"
}
fn author(&self) -> &str {
"VoiRS Team"
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[async_trait]
impl AudioEffect for CompressorEffect {
async fn process_audio(&self, audio: &AudioBuffer) -> Result<AudioBuffer> {
let sample_rate = audio.sample_rate();
let current_sample_rate = *self
.sample_rate
.read()
.expect("lock should not be poisoned");
if current_sample_rate.is_none()
|| current_sample_rate.expect("value should be present") != sample_rate
{
*self
.sample_rate
.write()
.expect("lock should not be poisoned") = Some(sample_rate);
}
let mut processed = audio.clone();
let samples = processed.samples_mut();
let attack = *self.attack.read().expect("lock should not be poisoned");
let release = *self.release.read().expect("lock should not be poisoned");
let makeup_gain = *self
.makeup_gain
.read()
.expect("lock should not be poisoned");
let attack_coeff = (-1.0 / (sample_rate as f32 * attack / 1000.0)).exp();
let release_coeff = (-1.0 / (sample_rate as f32 * release / 1000.0)).exp();
let mut envelope = *self.envelope.read().expect("lock should not be poisoned");
let makeup_gain_linear = 10_f32.powf(makeup_gain / 20.0);
for sample in samples.iter_mut() {
let input = *sample;
let input_level_db = 20.0 * (input.abs().max(0.00001)).log10();
let gain_reduction_db = self.calculate_gain_reduction(input_level_db);
let target_gain = 10_f32.powf(-gain_reduction_db / 20.0);
let coeff = if target_gain < envelope {
attack_coeff
} else {
release_coeff
};
envelope = target_gain + coeff * (envelope - target_gain);
*sample = input * envelope * makeup_gain_linear;
*sample = sample.clamp(-1.0, 1.0);
}
*self.envelope.write().expect("lock should not be poisoned") = envelope;
Ok(processed)
}
fn get_parameters(&self) -> HashMap<String, ParameterValue> {
let mut params = HashMap::new();
params.insert(
"threshold".to_string(),
ParameterValue::Float(*self.threshold.read().expect("lock should not be poisoned")),
);
params.insert(
"ratio".to_string(),
ParameterValue::Float(*self.ratio.read().expect("lock should not be poisoned")),
);
params.insert(
"attack".to_string(),
ParameterValue::Float(*self.attack.read().expect("lock should not be poisoned")),
);
params.insert(
"release".to_string(),
ParameterValue::Float(*self.release.read().expect("lock should not be poisoned")),
);
params.insert(
"makeup_gain".to_string(),
ParameterValue::Float(
*self
.makeup_gain
.read()
.expect("lock should not be poisoned"),
),
);
params
}
fn set_parameter(&self, name: &str, value: ParameterValue) -> Result<()> {
match name {
"threshold" => {
if let Some(v) = value.as_f32() {
*self.threshold.write().expect("lock should not be poisoned") =
v.clamp(-60.0, 0.0);
Ok(())
} else {
Err(VoirsError::internal(
"plugins",
"Invalid threshold parameter type",
))
}
}
"ratio" => {
if let Some(v) = value.as_f32() {
*self.ratio.write().expect("lock should not be poisoned") = v.clamp(1.0, 20.0);
Ok(())
} else {
Err(VoirsError::internal(
"plugins",
"Invalid ratio parameter type",
))
}
}
"attack" => {
if let Some(v) = value.as_f32() {
*self.attack.write().expect("lock should not be poisoned") =
v.clamp(0.1, 100.0);
Ok(())
} else {
Err(VoirsError::internal(
"plugins",
"Invalid attack parameter type",
))
}
}
"release" => {
if let Some(v) = value.as_f32() {
*self.release.write().expect("lock should not be poisoned") =
v.clamp(10.0, 1000.0);
Ok(())
} else {
Err(VoirsError::internal(
"plugins",
"Invalid release parameter type",
))
}
}
"makeup_gain" => {
if let Some(v) = value.as_f32() {
*self
.makeup_gain
.write()
.expect("lock should not be poisoned") = v.clamp(0.0, 30.0);
Ok(())
} else {
Err(VoirsError::internal(
"plugins",
"Invalid makeup_gain parameter type",
))
}
}
_ => Err(VoirsError::internal(
"plugins",
format!("Unknown parameter: {name}"),
)),
}
}
fn get_parameter_definition(&self, name: &str) -> Option<ParameterDefinition> {
match name {
"threshold" => Some(ParameterDefinition {
name: "threshold".to_string(),
description: "Compression threshold in dB".to_string(),
parameter_type: ParameterType::Float,
default_value: ParameterValue::Float(-20.0),
min_value: Some(ParameterValue::Float(-60.0)),
max_value: Some(ParameterValue::Float(0.0)),
step_size: Some(0.1),
realtime_safe: true,
}),
"ratio" => Some(ParameterDefinition {
name: "ratio".to_string(),
description: "Compression ratio".to_string(),
parameter_type: ParameterType::Float,
default_value: ParameterValue::Float(4.0),
min_value: Some(ParameterValue::Float(1.0)),
max_value: Some(ParameterValue::Float(20.0)),
step_size: Some(0.1),
realtime_safe: true,
}),
"attack" => Some(ParameterDefinition {
name: "attack".to_string(),
description: "Attack time in milliseconds".to_string(),
parameter_type: ParameterType::Float,
default_value: ParameterValue::Float(5.0),
min_value: Some(ParameterValue::Float(0.1)),
max_value: Some(ParameterValue::Float(100.0)),
step_size: Some(0.1),
realtime_safe: true,
}),
"release" => Some(ParameterDefinition {
name: "release".to_string(),
description: "Release time in milliseconds".to_string(),
parameter_type: ParameterType::Float,
default_value: ParameterValue::Float(50.0),
min_value: Some(ParameterValue::Float(10.0)),
max_value: Some(ParameterValue::Float(1000.0)),
step_size: Some(1.0),
realtime_safe: true,
}),
"makeup_gain" => Some(ParameterDefinition {
name: "makeup_gain".to_string(),
description: "Makeup gain in dB".to_string(),
parameter_type: ParameterType::Float,
default_value: ParameterValue::Float(0.0),
min_value: Some(ParameterValue::Float(0.0)),
max_value: Some(ParameterValue::Float(30.0)),
step_size: Some(0.1),
realtime_safe: true,
}),
_ => None,
}
}
}