#![allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PpmStandard {
Iec268_10TypeI,
Iec268_10TypeII,
NordicNrk,
Bbc,
Ebu,
}
#[derive(Clone, Debug)]
pub struct PpmConfig {
pub standard: PpmStandard,
pub attack_ms: f64,
pub release_ms: f64,
pub integration_time_ms: f64,
pub peak_hold_ms: f64,
}
impl PpmConfig {
#[must_use]
pub fn iec_type_i() -> Self {
Self {
standard: PpmStandard::Iec268_10TypeI,
attack_ms: 10.0,
release_ms: 1500.0,
integration_time_ms: 5.0,
peak_hold_ms: 1000.0,
}
}
#[must_use]
pub fn iec_type_ii() -> Self {
Self {
standard: PpmStandard::Iec268_10TypeII,
attack_ms: 5.0,
release_ms: 1500.0,
integration_time_ms: 10.0,
peak_hold_ms: 2000.0,
}
}
#[must_use]
pub fn ebu() -> Self {
Self {
standard: PpmStandard::Ebu,
attack_ms: 10.0,
release_ms: 1700.0,
integration_time_ms: 5.0,
peak_hold_ms: 2000.0,
}
}
#[must_use]
pub fn bbc() -> Self {
Self {
standard: PpmStandard::Bbc,
attack_ms: 10.0,
release_ms: 2800.0,
integration_time_ms: 5.0,
peak_hold_ms: 0.0,
}
}
#[must_use]
pub fn nordic_nrk() -> Self {
Self {
standard: PpmStandard::NordicNrk,
attack_ms: 10.0,
release_ms: 1500.0,
integration_time_ms: 5.0,
peak_hold_ms: 1000.0,
}
}
}
pub struct PpmMeter {
pub config: PpmConfig,
pub envelope: f64,
pub peak: f64,
pub peak_hold_timer: u64,
pub sample_rate: f64,
attack_coeff: f64,
release_coeff: f64,
}
impl PpmMeter {
#[must_use]
pub fn new(sample_rate: f64, config: PpmConfig) -> Self {
let attack_coeff = compute_attack_coeff(config.attack_ms, sample_rate);
let release_coeff = compute_release_coeff(config.release_ms, sample_rate);
Self {
config,
envelope: 0.0,
peak: 0.0,
peak_hold_timer: 0,
sample_rate,
attack_coeff,
release_coeff,
}
}
pub fn process_sample(&mut self, sample: f64, time_ms: u64) {
let abs_sample = sample.abs();
if abs_sample > self.envelope {
self.envelope =
self.attack_coeff * self.envelope + (1.0 - self.attack_coeff) * abs_sample;
} else {
self.envelope *= self.release_coeff;
}
if self.envelope >= self.peak {
self.peak = self.envelope;
self.peak_hold_timer = time_ms + self.config.peak_hold_ms as u64;
} else if time_ms > self.peak_hold_timer {
self.peak *= self.release_coeff;
}
}
#[must_use]
pub fn peak_db(&self) -> f64 {
linear_to_db(self.peak)
}
#[must_use]
pub fn envelope_db(&self) -> f64 {
linear_to_db(self.envelope)
}
pub fn reset_peak(&mut self) {
self.peak = 0.0;
self.envelope = 0.0;
self.peak_hold_timer = 0;
}
pub fn process_block(&mut self, samples: &[f64], start_time_ms: u64) {
let samples_per_ms = self.sample_rate / 1000.0;
for (i, &s) in samples.iter().enumerate() {
let time_ms = start_time_ms + (i as f64 / samples_per_ms) as u64;
self.process_sample(s, time_ms);
}
}
}
#[must_use]
pub fn db_to_linear(db: f64) -> f64 {
10.0_f64.powf(db / 20.0)
}
#[must_use]
pub fn linear_to_db(linear: f64) -> f64 {
if linear <= 0.0 {
f64::NEG_INFINITY
} else {
20.0 * linear.log10()
}
}
fn compute_attack_coeff(attack_ms: f64, sample_rate: f64) -> f64 {
if attack_ms <= 0.0 || sample_rate <= 0.0 {
return 0.0;
}
let attack_samples = attack_ms * sample_rate / 1000.0;
(-1.0 / attack_samples).exp()
}
fn compute_release_coeff(release_ms: f64, sample_rate: f64) -> f64 {
if release_ms <= 0.0 || sample_rate <= 0.0 {
return 0.0;
}
let release_samples = release_ms * sample_rate / 1000.0;
(-1.0 / release_samples).exp()
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_RATE: f64 = 48000.0;
#[test]
fn test_db_to_linear_zero_db() {
assert!((db_to_linear(0.0) - 1.0).abs() < 1e-10);
}
#[test]
fn test_db_to_linear_minus_6() {
let linear = db_to_linear(-6.0);
assert!((linear - 0.501187).abs() < 1e-5);
}
#[test]
fn test_db_to_linear_minus_20() {
assert!((db_to_linear(-20.0) - 0.1).abs() < 1e-10);
}
#[test]
fn test_linear_to_db_roundtrip() {
let db = -12.0;
let linear = db_to_linear(db);
let back = linear_to_db(linear);
assert!((back - db).abs() < 1e-10);
}
#[test]
fn test_linear_to_db_zero_is_neg_infinity() {
assert!(linear_to_db(0.0).is_infinite());
assert!(linear_to_db(0.0) < 0.0);
}
#[test]
fn test_ppm_config_iec_type_i() {
let cfg = PpmConfig::iec_type_i();
assert_eq!(cfg.standard, PpmStandard::Iec268_10TypeI);
assert!((cfg.attack_ms - 10.0).abs() < 1e-10);
assert!((cfg.release_ms - 1500.0).abs() < 1e-10);
}
#[test]
fn test_ppm_config_iec_type_ii() {
let cfg = PpmConfig::iec_type_ii();
assert_eq!(cfg.standard, PpmStandard::Iec268_10TypeII);
assert!((cfg.attack_ms - 5.0).abs() < 1e-10);
}
#[test]
fn test_ppm_config_ebu() {
let cfg = PpmConfig::ebu();
assert_eq!(cfg.standard, PpmStandard::Ebu);
}
#[test]
fn test_ppm_meter_new() {
let meter = PpmMeter::new(SAMPLE_RATE, PpmConfig::iec_type_i());
assert!((meter.envelope - 0.0).abs() < 1e-10);
assert!((meter.peak - 0.0).abs() < 1e-10);
}
#[test]
fn test_ppm_meter_attack_on_impulse() {
let mut meter = PpmMeter::new(SAMPLE_RATE, PpmConfig::iec_type_i());
meter.process_sample(1.0, 0);
assert!(
meter.envelope > 0.0,
"envelope should increase after impulse"
);
}
#[test]
fn test_ppm_meter_release_decays() {
let mut meter = PpmMeter::new(SAMPLE_RATE, PpmConfig::iec_type_i());
for _ in 0..1000 {
meter.process_sample(1.0, 0);
}
let after_attack = meter.envelope;
for i in 0..10000 {
meter.process_sample(0.0, i as u64 / 48);
}
assert!(meter.envelope < after_attack, "envelope should decay");
}
#[test]
fn test_ppm_meter_peak_holds() {
let mut meter = PpmMeter::new(SAMPLE_RATE, PpmConfig::iec_type_i());
for _ in 0..500 {
meter.process_sample(0.8, 0);
}
let peak_after_attack = meter.peak;
for i in 0..100 {
meter.process_sample(0.0, (i as u64) / 48);
}
assert!(meter.peak <= peak_after_attack + 0.01);
}
#[test]
fn test_ppm_meter_reset_peak() {
let mut meter = PpmMeter::new(SAMPLE_RATE, PpmConfig::iec_type_i());
meter.process_sample(1.0, 0);
meter.reset_peak();
assert!((meter.peak - 0.0).abs() < 1e-10);
assert!((meter.envelope - 0.0).abs() < 1e-10);
}
#[test]
fn test_ppm_meter_peak_db_silence() {
let meter = PpmMeter::new(SAMPLE_RATE, PpmConfig::iec_type_i());
let db = meter.peak_db();
assert!(db.is_infinite() && db < 0.0);
}
#[test]
fn test_ppm_meter_process_block() {
let mut meter = PpmMeter::new(SAMPLE_RATE, PpmConfig::iec_type_ii());
let block: Vec<f64> = (0..480).map(|_| 0.5).collect();
meter.process_block(&block, 0);
assert!(meter.envelope > 0.0);
}
}