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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
use alloc::vec;
use alloc::vec::Vec;
/// Number of critical bands (Bark scale, 0-24 Bark for audio up to ~20kHz)
pub const NUM_BARK_BANDS: usize = 25;
/// Critical band edges in Hz (Bark scale)
pub const BARK_BAND_EDGES: [f32; 26] = [
0.0, 100.0, 200.0, 300.0, 400.0, 510.0, 630.0, 770.0, 920.0, 1080.0, 1270.0, 1480.0, 1720.0,
2000.0, 2320.0, 2700.0, 3150.0, 3700.0, 4400.0, 5300.0, 6400.0, 7700.0, 9500.0, 12000.0,
15500.0, 20500.0,
];
/// Psychoacoustic model parameters
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct PsychoacousticModel {
/// Sample rate
pub(crate) sample_rate: u32,
/// FFT/MDCT size (2N for MDCT)
pub(crate) fft_size: usize,
/// Number of MDCT coefficients (N)
pub(crate) num_coeffs: usize,
/// Frequency resolution (Hz per bin)
pub(crate) freq_resolution: f32,
/// Absolute threshold of hearing per MDCT bin (dB SPL)
pub(crate) ath: Vec<f32>,
/// Bark band index for each MDCT coefficient
pub(crate) bark_band: Vec<usize>,
/// Spreading function matrix (for masking)
pub(crate) spreading: Vec<Vec<f32>>,
/// Previous frame's masking threshold per Bark band (dB), for temporal
/// (post-)masking
pub(crate) prev_threshold_db: Vec<f32>,
}
impl PsychoacousticModel {
/// Create a new psychoacoustic model
pub fn new(sample_rate: u32, fft_size: usize) -> Self {
let num_coeffs = fft_size / 2;
let freq_resolution = sample_rate as f32 / fft_size as f32;
// Calculate ATH for each bin
let ath: Vec<f32> = (0..num_coeffs)
.map(|k| {
let freq = (k as f32 + 0.5) * freq_resolution;
Self::absolute_threshold_of_hearing(freq)
})
.collect();
// Map each coefficient to its Bark band
let bark_band: Vec<usize> = (0..num_coeffs)
.map(|k| {
let freq = (k as f32 + 0.5) * freq_resolution;
Self::freq_to_bark_band(freq)
})
.collect();
// Pre-compute spreading function
let spreading = Self::compute_spreading_function();
// Initialize previous threshold to silence
let prev_threshold_db = vec![-100.0f32; NUM_BARK_BANDS];
Self {
sample_rate,
fft_size,
num_coeffs,
freq_resolution,
ath,
bark_band,
spreading,
prev_threshold_db,
}
}
/// Get the Bark band for a specific coefficient index
pub fn get_bark_band(&self, coeff_idx: usize) -> usize {
self.bark_band.get(coeff_idx).copied().unwrap_or(0)
}
/// Get number of MDCT coefficients
pub fn num_coefficients(&self) -> usize {
self.num_coeffs
}
/// Get frequency resolution (Hz per bin)
pub fn frequency_resolution(&self) -> f32 {
self.freq_resolution
}
/// Absolute Threshold of Hearing (ATH) in dB SPL
/// Based on ISO 226 / Terhardt formula
pub fn absolute_threshold_of_hearing(freq: f32) -> f32 {
if !(20.0..=20000.0).contains(&freq) {
return 96.0; // Essentially inaudible
}
let f_khz = freq / 1000.0;
// Terhardt's formula (simplified)
// ATH(f) = 3.64 * (f/1000)^-0.8 - 6.5 * exp(-0.6 * (f/1000 - 3.3)^2) + 10^-3 * (f/1000)^4
let term1 = 3.64 * libm::powf(f_khz, -0.8);
let term2 = 6.5 * libm::expf(-0.6 * (f_khz - 3.3) * (f_khz - 3.3));
let term3 = 0.001 * libm::powf(f_khz, 4.0);
(term1 - term2 + term3).clamp(-10.0, 96.0)
}
/// Convert frequency to Bark scale
pub fn freq_to_bark(freq: f32) -> f32 {
// Traunmüller's formula
let bark = ((26.81 * freq) / (1960.0 + freq)) - 0.53;
bark.clamp(0.0, 24.0)
}
/// Get the Bark band index for a frequency
pub fn freq_to_bark_band(freq: f32) -> usize {
for (i, &edge) in BARK_BAND_EDGES.iter().enumerate().skip(1) {
if freq < edge {
return i - 1;
}
}
NUM_BARK_BANDS - 1
}
/// Compute the spreading function between Bark bands
/// This models how a masker in one band affects neighboring bands
fn compute_spreading_function() -> Vec<Vec<f32>> {
let mut spreading = vec![vec![0.0f32; NUM_BARK_BANDS]; NUM_BARK_BANDS];
for i in 0..NUM_BARK_BANDS {
for j in 0..NUM_BARK_BANDS {
let delta_bark = j as f32 - i as f32;
// Spreading function (simplified from MPEG psychoacoustic model)
let spread = if delta_bark >= 0.0 {
// Upper slope (masking above the masker)
-25.0 * delta_bark
} else {
// Lower slope (masking below the masker)
-10.0 * delta_bark
};
// Convert dB to linear and clamp
spreading[i][j] = libm::powf(10.0f32, spread / 10.0).min(1.0);
}
}
spreading
}
/// Calculate the masking threshold for MDCT coefficients
/// Returns threshold in dB for each coefficient
pub fn calculate_masking_threshold(&mut self, coeffs: &[f32]) -> Vec<f32> {
let mut thresholds = vec![0.0f32; self.num_coeffs];
// Step 1: Calculate energy per Bark band
let mut band_energy = [0.0f32; NUM_BARK_BANDS];
let mut band_count = [0usize; NUM_BARK_BANDS];
for (k, &coeff) in coeffs.iter().enumerate() {
let band = self.bark_band[k];
let energy = coeff * coeff;
band_energy[band] += energy;
band_count[band] += 1;
}
// Convert to dB (average energy per band)
let band_db: Vec<f32> = band_energy
.iter()
.zip(band_count.iter())
.map(|(&e, &c)| {
if c > 0 && e > 1e-10 {
10.0 * libm::log10f(e / c as f32)
} else {
-100.0
}
})
.collect();
// Step 2: Apply spreading function (simultaneous masking)
let mut spread_threshold = vec![-100.0f32; NUM_BARK_BANDS];
for i in 0..NUM_BARK_BANDS {
for j in 0..NUM_BARK_BANDS {
// Masking from band j to band i
let masking = band_db[j] + 10.0 * libm::log10f(self.spreading[j][i]);
spread_threshold[i] = spread_threshold[i].max(masking);
}
}
// Step 3: Apply masking offset (tone masking noise vs noise masking tone)
// Simplified: use a single offset (real codecs distinguish tone/noise)
let masking_offset = -6.0; // dB below masker
for t in &mut spread_threshold {
*t += masking_offset;
}
// Step 4: Temporal masking (post-masking)
let temporal_decay = 0.5; // linear energy decay factor per frame (~3 dB/frame)
for i in 0..NUM_BARK_BANDS {
let prev_linear = libm::powf(10.0f32, self.prev_threshold_db[i] / 10.0);
let temporal_mask_db = if prev_linear > 1e-10 {
10.0 * libm::log10f(prev_linear * temporal_decay)
} else {
-100.0
};
spread_threshold[i] = spread_threshold[i].max(temporal_mask_db);
self.prev_threshold_db[i] = spread_threshold[i];
}
// Step 5: Combine with ATH and map back to coefficients
for (k, threshold) in thresholds.iter_mut().enumerate() {
let band = self.bark_band[k];
// Final threshold is max of masking threshold and ATH
// Subtract some headroom for safety
*threshold = spread_threshold[band].max(self.ath[k]) - 10.0;
}
thresholds
}
/// Calculate Signal-to-Mask Ratio (SMR) for each coefficient
/// Higher SMR = more important, needs more bits
pub fn calculate_smr(&mut self, coeffs: &[f32]) -> Vec<f32> {
let thresholds = self.calculate_masking_threshold(coeffs);
coeffs
.iter()
.zip(thresholds.iter())
.map(|(&c, &t)| {
let signal_db = if c.abs() > 1e-10 {
20.0 * libm::log10f(c.abs())
} else {
-100.0
};
// SMR = signal level - masking threshold
// Positive = audible, negative = masked
signal_db - t
})
.collect()
}
/// Calculate bits needed per band based on SMR
/// Higher SMR needs more bits to avoid audible quantization noise
pub fn allocate_bits(&self, smr: &[f32], total_bits: usize) -> Vec<u8> {
let mut band_smr = [0.0f32; NUM_BARK_BANDS];
let mut band_count = [0usize; NUM_BARK_BANDS];
for (k, &s) in smr.iter().enumerate() {
let band = self.bark_band[k];
band_smr[band] = band_smr[band].max(s);
band_count[band] += 1;
}
// Allocate bits proportionally to positive SMR
let total_smr: f32 = band_smr.iter().map(|&s| s.max(0.0)).sum();
let mut bits_per_coeff = vec![0u8; self.num_coeffs];
if total_smr > 0.0 {
for (k, bits) in bits_per_coeff.iter_mut().enumerate() {
let band = self.bark_band[k];
let band_bits = if band_smr[band] > 0.0 {
libm::roundf(
(band_smr[band] / total_smr) * total_bits as f32 / band_count[band] as f32,
) as u8
} else {
0
};
*bits = band_bits.clamp(0, 15);
}
}
bits_per_coeff
}
/// Get the frequency for a given MDCT coefficient index
pub fn bin_to_freq(&self, bin: usize) -> f32 {
(bin as f32 + 0.5) * self.freq_resolution
}
/// Reset temporal state (for seeking/discontinuities)
pub fn reset(&mut self) {
self.prev_threshold_db.fill(-100.0);
}
}