#![allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ClipThreshold {
DigitalFullScale,
Linear(f64),
Dbfs(f64),
}
impl ClipThreshold {
#[allow(clippy::cast_precision_loss)]
pub fn to_linear(&self) -> f64 {
match self {
Self::DigitalFullScale => 1.0,
Self::Linear(v) => *v,
Self::Dbfs(db) => 10.0_f64.powf(*db / 20.0),
}
}
}
impl Default for ClipThreshold {
fn default() -> Self {
Self::DigitalFullScale
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ClipEvent {
pub channel: usize,
pub start_sample: u64,
pub duration_samples: u64,
pub peak_value: f64,
}
impl ClipEvent {
#[allow(clippy::cast_precision_loss)]
pub fn duration_seconds(&self, sample_rate: f64) -> f64 {
self.duration_samples as f64 / sample_rate
}
}
#[derive(Clone, Debug)]
struct ChannelClipState {
in_clip: bool,
clip_start: u64,
clip_duration: u64,
clip_peak: f64,
total_clipped_samples: u64,
clip_event_count: u64,
samples_processed: u64,
}
impl ChannelClipState {
fn new() -> Self {
Self {
in_clip: false,
clip_start: 0,
clip_duration: 0,
clip_peak: 0.0,
total_clipped_samples: 0,
clip_event_count: 0,
samples_processed: 0,
}
}
fn reset(&mut self) {
*self = Self::new();
}
}
#[derive(Clone, Debug)]
pub struct ClipCounter {
channels: usize,
sample_rate: f64,
threshold_linear: f64,
channel_states: Vec<ChannelClipState>,
events: Vec<ClipEvent>,
log_events: bool,
max_events: usize,
}
impl ClipCounter {
pub fn new(
channels: usize,
sample_rate: f64,
threshold: ClipThreshold,
log_events: bool,
) -> Self {
let channels = channels.max(1);
Self {
channels,
sample_rate,
threshold_linear: threshold.to_linear(),
channel_states: (0..channels).map(|_| ChannelClipState::new()).collect(),
events: Vec::new(),
log_events,
max_events: 10_000,
}
}
pub fn process_interleaved(&mut self, samples: &[f64]) {
let frame_count = samples.len() / self.channels;
for frame in 0..frame_count {
for ch in 0..self.channels {
let sample = samples[frame * self.channels + ch].abs();
let state = &mut self.channel_states[ch];
let clipping = sample >= self.threshold_linear;
if clipping {
state.total_clipped_samples += 1;
if !state.in_clip {
state.in_clip = true;
state.clip_start = state.samples_processed;
state.clip_duration = 1;
state.clip_peak = sample;
state.clip_event_count += 1;
} else {
state.clip_duration += 1;
if sample > state.clip_peak {
state.clip_peak = sample;
}
}
} else if state.in_clip {
if self.log_events && self.events.len() < self.max_events {
self.events.push(ClipEvent {
channel: ch,
start_sample: state.clip_start,
duration_samples: state.clip_duration,
peak_value: state.clip_peak,
});
}
state.in_clip = false;
}
state.samples_processed += 1;
}
}
}
pub fn process_channel(&mut self, channel: usize, samples: &[f64]) {
if channel >= self.channels {
return;
}
for &sample in samples {
let abs_val = sample.abs();
let state = &mut self.channel_states[channel];
let clipping = abs_val >= self.threshold_linear;
if clipping {
state.total_clipped_samples += 1;
if !state.in_clip {
state.in_clip = true;
state.clip_start = state.samples_processed;
state.clip_duration = 1;
state.clip_peak = abs_val;
state.clip_event_count += 1;
} else {
state.clip_duration += 1;
if abs_val > state.clip_peak {
state.clip_peak = abs_val;
}
}
} else if state.in_clip {
if self.log_events && self.events.len() < self.max_events {
self.events.push(ClipEvent {
channel,
start_sample: state.clip_start,
duration_samples: state.clip_duration,
peak_value: state.clip_peak,
});
}
state.in_clip = false;
}
state.samples_processed += 1;
}
}
pub fn total_clip_events(&self) -> u64 {
self.channel_states.iter().map(|s| s.clip_event_count).sum()
}
pub fn channel_clip_events(&self, channel: usize) -> u64 {
self.channel_states
.get(channel)
.map_or(0, |s| s.clip_event_count)
}
pub fn total_clipped_samples(&self) -> u64 {
self.channel_states
.iter()
.map(|s| s.total_clipped_samples)
.sum()
}
#[allow(clippy::cast_precision_loss)]
pub fn clip_percentage(&self) -> f64 {
let total_samples: u64 = self
.channel_states
.iter()
.map(|s| s.samples_processed)
.sum();
if total_samples == 0 {
return 0.0;
}
let clipped: u64 = self.total_clipped_samples();
clipped as f64 / total_samples as f64 * 100.0
}
pub fn has_clipping(&self) -> bool {
self.total_clip_events() > 0
}
pub fn events(&self) -> &[ClipEvent] {
&self.events
}
pub fn severity(&self) -> ClipSeverity {
let pct = self.clip_percentage();
if pct == 0.0 {
ClipSeverity::None
} else if pct < 0.01 {
ClipSeverity::Minor
} else if pct < 0.1 {
ClipSeverity::Moderate
} else {
ClipSeverity::Severe
}
}
pub fn reset(&mut self) {
for state in &mut self.channel_states {
state.reset();
}
self.events.clear();
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ClipSeverity {
None,
Minor,
Moderate,
Severe,
}
impl ClipSeverity {
pub fn description(&self) -> &str {
match self {
Self::None => "No clipping",
Self::Minor => "Minor clipping (barely audible)",
Self::Moderate => "Moderate clipping (audible distortion)",
Self::Severe => "Severe clipping (significant distortion)",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_threshold_digital_full_scale() {
let t = ClipThreshold::DigitalFullScale;
assert!((t.to_linear() - 1.0).abs() < 1e-12);
}
#[test]
fn test_threshold_dbfs() {
let t = ClipThreshold::Dbfs(-6.0);
let linear = t.to_linear();
assert!((linear - 0.5012).abs() < 0.01);
}
#[test]
fn test_threshold_linear() {
let t = ClipThreshold::Linear(0.95);
assert!((t.to_linear() - 0.95).abs() < 1e-12);
}
#[test]
fn test_no_clipping() {
let mut counter = ClipCounter::new(1, 48000.0, ClipThreshold::DigitalFullScale, false);
let samples: Vec<f64> = (0..1000).map(|i| (i as f64 * 0.001).sin() * 0.5).collect();
counter.process_channel(0, &samples);
assert_eq!(counter.total_clip_events(), 0);
assert!(!counter.has_clipping());
assert_eq!(counter.severity(), ClipSeverity::None);
}
#[test]
fn test_single_clip_event() {
let mut counter = ClipCounter::new(1, 48000.0, ClipThreshold::DigitalFullScale, true);
let mut samples = vec![0.5; 100];
for s in samples.iter_mut().take(55).skip(50) {
*s = 1.0;
}
samples.push(0.1);
counter.process_channel(0, &samples);
assert_eq!(counter.total_clip_events(), 1);
assert_eq!(counter.events().len(), 1);
assert_eq!(counter.events()[0].duration_samples, 5);
}
#[test]
fn test_multiple_clip_events() {
let mut counter = ClipCounter::new(1, 48000.0, ClipThreshold::DigitalFullScale, true);
let mut samples = vec![0.5; 200];
samples[10] = 1.0;
samples[11] = 1.0;
samples[100] = 1.0;
samples[101] = 1.0;
samples[102] = 1.0;
counter.process_channel(0, &samples);
assert_eq!(counter.total_clip_events(), 2);
assert_eq!(counter.events().len(), 2);
}
#[test]
fn test_interleaved_stereo() {
let mut counter = ClipCounter::new(2, 48000.0, ClipThreshold::DigitalFullScale, true);
let mut samples = vec![0.5; 20]; samples[6] = 1.0; samples[11] = 1.0; samples.extend_from_slice(&[0.1, 0.1]);
counter.process_interleaved(&samples);
assert_eq!(counter.channel_clip_events(0), 1);
assert_eq!(counter.channel_clip_events(1), 1);
assert_eq!(counter.total_clip_events(), 2);
}
#[test]
fn test_clip_percentage() {
let mut counter = ClipCounter::new(1, 48000.0, ClipThreshold::DigitalFullScale, false);
let mut samples = vec![0.5; 1000];
for s in samples.iter_mut().take(10) {
*s = 1.0;
}
counter.process_channel(0, &samples);
let pct = counter.clip_percentage();
assert!((pct - 1.0).abs() < 0.01);
}
#[test]
fn test_severity_levels() {
assert_eq!(ClipSeverity::None.description(), "No clipping");
assert_eq!(
ClipSeverity::Severe.description(),
"Severe clipping (significant distortion)"
);
}
#[test]
fn test_clip_event_duration_seconds() {
let evt = ClipEvent {
channel: 0,
start_sample: 0,
duration_samples: 48000,
peak_value: 1.0,
};
assert!((evt.duration_seconds(48000.0) - 1.0).abs() < 1e-12);
}
#[test]
fn test_reset() {
let mut counter = ClipCounter::new(1, 48000.0, ClipThreshold::DigitalFullScale, true);
counter.process_channel(0, &[1.0, 1.0, 0.5]);
assert!(counter.has_clipping());
counter.reset();
assert!(!counter.has_clipping());
assert!(counter.events().is_empty());
}
#[test]
fn test_custom_threshold() {
let mut counter = ClipCounter::new(1, 48000.0, ClipThreshold::Linear(0.8), true);
counter.process_channel(0, &[0.9, 0.85, 0.5]);
assert_eq!(counter.total_clip_events(), 1);
assert_eq!(counter.events()[0].duration_samples, 2);
}
#[test]
fn test_invalid_channel_ignored() {
let mut counter = ClipCounter::new(2, 48000.0, ClipThreshold::DigitalFullScale, false);
counter.process_channel(5, &[1.0, 1.0]);
assert_eq!(counter.total_clip_events(), 0);
}
}