1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// ============================================================================
// True Peak Detector — 4x oversampled peak detection (ITU-R BS.1770-4 inspired)
// ============================================================================
/// A true peak detector that uses 4x oversampling via Catmull-Rom interpolation
/// to detect inter-sample peaks.
///
/// Standard peak detection misses peaks that occur between samples. This computes
/// 3 sub-sample interpolation points between each pair of consecutive samples
/// to catch inter-sample peaks that exceed the sample values.
#[derive(Debug, Clone)]
pub struct TruePeakDetector {
/// Previous samples: [x[n-2], x[n-1]]
prev: [f32; 2],
/// Current true peak (linear, not dB).
peak: f32,
}
impl TruePeakDetector {
pub fn new() -> Self {
Self {
prev: [0.0; 2],
peak: 0.0,
}
}
/// Process one input sample. Returns the true peak level in dB.
///
/// Computes 3 sub-sample interpolation points between the previous sample
/// and the current sample, plus the current sample itself, and returns
/// the maximum absolute value in dB.
#[inline]
/// Process one sample and return the true peak level in dB.
/// Note: `process()` and `process_linear()` share state — call only one per sample.
pub fn process(&mut self, sample: f32) -> f32 {
let lin = self.process_linear(sample);
if lin < 1e-12 {
-120.0
} else {
20.0 * lin.log10()
}
}
/// Process one sample and return the true peak as linear amplitude.
/// This is the primary processing method that advances internal state.
#[inline]
pub fn process_linear(&mut self, sample: f32) -> f32 {
// Interpolate between prev[1] (x[n-1]) and sample (x[n])
// using Catmull-Rom with points: prev[0] (x[n-2]), prev[1] (x[n-1]),
// sample (x[n]), and sample again for the future boundary (hold).
let p0 = self.prev[0];
let p1 = self.prev[1];
let p2 = sample;
let p3 = sample; // Hold boundary for causal operation
let mut max_abs = sample.abs();
max_abs = max_abs.max(catmull_rom(0.25, p0, p1, p2, p3).abs());
max_abs = max_abs.max(catmull_rom(0.5, p0, p1, p2, p3).abs());
max_abs = max_abs.max(catmull_rom(0.75, p0, p1, p2, p3).abs());
max_abs = max_abs.max(p1.abs());
self.prev[0] = self.prev[1];
self.prev[1] = sample;
self.peak = max_abs;
max_abs
}
/// Get the last computed true peak in dB.
#[inline]
pub fn peak_db(&self) -> f32 {
if self.peak < 1e-12 {
-120.0
} else {
20.0 * self.peak.log10()
}
}
/// Get the last computed true peak as linear amplitude.
#[inline]
pub fn peak_linear(&self) -> f32 {
self.peak
}
pub fn reset(&mut self) {
self.prev = [0.0; 2];
self.peak = 0.0;
}
}
/// Catmull-Rom spline interpolation between p1 and p2 at parameter t ∈ [0, 1].
#[inline]
fn catmull_rom(t: f32, p0: f32, p1: f32, p2: f32, p3: f32) -> f32 {
let t2 = t * t;
let t3 = t2 * t;
0.5 * ((-t3 + 2.0 * t2 - t) * p0
+ (3.0 * t3 - 5.0 * t2 + 2.0) * p1
+ (-3.0 * t3 + 4.0 * t2 + t) * p2
+ (t3 - t2) * p3)
}
impl Default for TruePeakDetector {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_silence() {
let mut tp = TruePeakDetector::new();
let db = tp.process(0.0);
assert!(db <= -120.0);
}
#[test]
fn test_unity_peak() {
let mut tp = TruePeakDetector::new();
let db = tp.process(1.0);
// Current sample is 1.0 → should be 0 dBFS (or higher due to interpolation overshoot)
assert!(db >= -0.1, "peak db: {}", db);
}
#[test]
fn test_inter_sample_peak() {
let mut tp = TruePeakDetector::new();
// Alternating +0.9 / -0.9 at Nyquist creates large inter-sample peaks
// First establish history
tp.process(0.0);
tp.process(0.9);
let _db = tp.process(-0.9);
// The Catmull-Rom interpolation between 0.9 and -0.9 should show
// peaks larger than 0.9 due to the overshoot
assert!(tp.peak_linear() >= 0.9, "peak: {}", tp.peak_linear());
}
#[test]
fn test_detects_overshoot() {
let mut tp = TruePeakDetector::new();
// Create a signal that overshoots between samples:
// 0.0, 0.8, 0.0 — the interpolation should show a peak >= 0.8
tp.process(0.0);
tp.process(0.8);
tp.process(0.0);
assert!(tp.peak_linear() >= 0.79, "peak: {}", tp.peak_linear());
}
#[test]
fn test_reset() {
let mut tp = TruePeakDetector::new();
tp.process(1.0);
tp.reset();
assert_eq!(tp.peak_linear(), 0.0);
assert_eq!(tp.prev, [0.0; 2]);
}
}