Skip to main content

max / audiofiles

5.7 KB · 196 lines History Blame Raw
1 //! Basic audio measurements: peak amplitude and RMS level in dBFS.
2
3 /// Compute peak amplitude in dBFS.
4 ///
5 /// Returns -96.0 for silence. The -96 dBFS floor represents the noise floor of
6 /// 16-bit audio (96 dB dynamic range = 16 bits * 6 dB/bit) and serves as a
7 /// practical "silent" value.
8 pub fn peak_db(samples: &[f32]) -> f64 {
9 if samples.is_empty() {
10 return -96.0;
11 }
12 let peak = samples
13 .iter()
14 .fold(0.0f32, |max, &s| max.max(s.abs()));
15 if peak == 0.0 {
16 -96.0
17 } else {
18 20.0 * (peak as f64).log10()
19 }
20 }
21
22 /// Compute RMS level in dBFS.
23 ///
24 /// Returns -96.0 for silence or empty input (same floor convention as `peak_db`).
25 pub fn rms_db(samples: &[f32]) -> f64 {
26 if samples.is_empty() {
27 return -96.0;
28 }
29 let sum_sq: f64 = samples.iter().map(|&s| (s as f64) * (s as f64)).sum();
30 let rms = (sum_sq / samples.len() as f64).sqrt();
31 if rms == 0.0 {
32 -96.0
33 } else {
34 20.0 * rms.log10()
35 }
36 }
37
38 /// Compute crest factor: peak / RMS in linear domain.
39 ///
40 /// High values (>8) indicate sharp transients (impacts, clicks).
41 /// Low values (<3) indicate sustained signals (pads, drones).
42 /// Returns 0.0 for silence or empty input.
43 pub fn crest_factor(samples: &[f32]) -> f64 {
44 if samples.is_empty() {
45 return 0.0;
46 }
47 let peak = samples.iter().fold(0.0f32, |max, &s| max.max(s.abs()));
48 if peak == 0.0 {
49 return 0.0;
50 }
51 let sum_sq: f64 = samples.iter().map(|&s| (s as f64) * (s as f64)).sum();
52 let rms = (sum_sq / samples.len() as f64).sqrt();
53 if rms == 0.0 {
54 0.0
55 } else {
56 peak as f64 / rms
57 }
58 }
59
60 /// Compute attack time in seconds: time to reach 90% of peak amplitude using a 1ms RMS envelope.
61 ///
62 /// Fast attack (<5ms) = percussive hits, impacts.
63 /// Slow attack (>50ms) = pads, ambience, swells.
64 /// Returns 0.0 for silence or very short signals.
65 pub fn attack_time(samples: &[f32], sample_rate: u32) -> f64 {
66 if samples.is_empty() || sample_rate == 0 {
67 return 0.0;
68 }
69
70 // 1ms RMS envelope window
71 let window_len = (sample_rate as usize) / 1000;
72 if window_len == 0 || samples.len() < window_len {
73 return 0.0;
74 }
75
76 // Compute RMS envelope
77 let mut envelope = Vec::with_capacity(samples.len() / window_len);
78 let mut pos = 0;
79 while pos + window_len <= samples.len() {
80 let sum_sq: f64 = samples[pos..pos + window_len]
81 .iter()
82 .map(|&s| (s as f64) * (s as f64))
83 .sum();
84 envelope.push((sum_sq / window_len as f64).sqrt());
85 pos += window_len;
86 }
87
88 if envelope.is_empty() {
89 return 0.0;
90 }
91
92 let peak_env = envelope.iter().cloned().fold(0.0f64, f64::max);
93 if peak_env == 0.0 {
94 return 0.0;
95 }
96
97 let threshold = peak_env * 0.9;
98 for (i, &val) in envelope.iter().enumerate() {
99 if val >= threshold {
100 return (i as f64 * window_len as f64) / sample_rate as f64;
101 }
102 }
103
104 // Never reached threshold (shouldn't happen with 90% of peak)
105 0.0
106 }
107
108 #[cfg(test)]
109 mod tests {
110 use super::*;
111
112 #[test]
113 fn silence_gives_floor() {
114 let silence = vec![0.0f32; 1024];
115 assert_eq!(peak_db(&silence), -96.0);
116 assert_eq!(rms_db(&silence), -96.0);
117 }
118
119 #[test]
120 fn full_scale_sine() {
121 // A full-scale signal at 1.0 peak
122 let samples: Vec<f32> = (0..44100)
123 .map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0).sin())
124 .collect();
125 let peak = peak_db(&samples);
126 assert!((peak - 0.0).abs() < 0.1, "peak should be ~0 dBFS, got {peak}");
127
128 let rms = rms_db(&samples);
129 // RMS of a sine wave is -3.01 dBFS
130 assert!((rms - (-3.01)).abs() < 0.1, "rms should be ~-3 dBFS, got {rms}");
131 }
132
133 #[test]
134 fn dc_offset() {
135 let samples = vec![0.5f32; 1000];
136 let peak = peak_db(&samples);
137 assert!((peak - (-6.02)).abs() < 0.1);
138 }
139
140 #[test]
141 fn empty_gives_floor() {
142 assert_eq!(rms_db(&[]), -96.0);
143 }
144
145 #[test]
146 fn crest_factor_silence() {
147 assert_eq!(crest_factor(&[0.0; 1000]), 0.0);
148 assert_eq!(crest_factor(&[]), 0.0);
149 }
150
151 #[test]
152 fn crest_factor_sine() {
153 // Sine wave crest factor = sqrt(2) ≈ 1.414
154 let samples: Vec<f32> = (0..44100)
155 .map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0).sin())
156 .collect();
157 let cf = crest_factor(&samples);
158 assert!((cf - std::f64::consts::SQRT_2).abs() < 0.05, "sine crest factor should be ~1.414, got {cf}");
159 }
160
161 #[test]
162 fn crest_factor_impulse_is_high() {
163 // Single spike in silence — very high crest factor
164 let mut samples = vec![0.0f32; 44100];
165 samples[100] = 1.0;
166 let cf = crest_factor(&samples);
167 assert!(cf > 100.0, "impulse crest factor should be very high, got {cf}");
168 }
169
170 #[test]
171 fn attack_time_silence() {
172 assert_eq!(attack_time(&[0.0; 1000], 44100), 0.0);
173 assert_eq!(attack_time(&[], 44100), 0.0);
174 }
175
176 #[test]
177 fn attack_time_immediate_onset() {
178 // Full-scale signal from the start — attack time should be ~0
179 let samples = vec![1.0f32; 44100];
180 let at = attack_time(&samples, 44100);
181 assert!(at < 0.002, "constant signal attack should be ~0, got {at}");
182 }
183
184 #[test]
185 fn attack_time_slow_ramp() {
186 // Linear ramp from 0 to 1 over 1 second
187 let sr = 44100u32;
188 let samples: Vec<f32> = (0..sr)
189 .map(|i| i as f32 / sr as f32)
190 .collect();
191 let at = attack_time(&samples, sr);
192 // 90% of peak (1.0) is 0.9, reached at t ≈ 0.9s
193 assert!(at > 0.5, "ramp attack time should be slow, got {at}");
194 }
195 }
196