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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
//! Ports the variable cut-off low-pass filter from `silk/LP_variable_cutoff.c`.
//!
//! This module implements an elliptic/Cauer filter with 0.1 dB passband ripple,
//! 80 dB minimum stopband attenuation, and normalized cut-off frequencies ranging
//! from 0.95 down to 0.35. The cut-off frequency is controlled via piece-wise linear
//! interpolation between pre-computed filter coefficient tables during bandwidth
//! transitions.
use crate::silk::biquad_alt::biquad_alt_stride1_inplace;
use crate::silk::tables_other::{
SILK_TRANSITION_LP_A_Q28, SILK_TRANSITION_LP_B_Q28, TRANSITION_INT_NUM, TRANSITION_NA,
TRANSITION_NB,
};
/// Maximum frame length in milliseconds (5ms subframe × 4 subframes).
const MAX_FRAME_LENGTH_MS: i32 = 20;
/// Total transition time in milliseconds.
const TRANSITION_TIME_MS: i32 = 5120;
/// Number of frames over which the transition occurs.
pub(crate) const TRANSITION_FRAMES: i32 = TRANSITION_TIME_MS / MAX_FRAME_LENGTH_MS;
/// Number of interpolation steps between coefficient tables.
const TRANSITION_INT_STEPS: i32 = TRANSITION_FRAMES / (TRANSITION_INT_NUM as i32 - 1);
/// State structure for the variable cut-off low-pass filter.
///
/// This mirrors `silk_LP_state` from the C implementation. The filter can be
/// activated and deactivated dynamically, and the cut-off frequency changes
/// gradually over multiple frames during bandwidth transitions.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct LpState {
/// Two-element Q12 state vector for the biquad filter.
pub in_lp_state: [i32; 2],
/// Frame counter mapping to the current cut-off frequency (0..=TRANSITION_FRAMES).
pub transition_frame_no: i32,
/// Operating mode: <0 = switch down, >0 = switch up, 0 = do nothing.
pub mode: i32,
/// If non-zero, holds the last sampling rate (in kHz) before a bandwidth switching reset.
pub saved_fs_khz: i32,
}
impl LpState {
/// Creates a new `LpState` with all fields initialized to zero.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Low-pass filters a frame with variable cut-off frequency.
///
/// The cut-off frequency is determined by `self.transition_frame_no` and changes
/// gradually over time when `self.mode != 0`. The filter coefficients are interpolated
/// from pre-computed elliptic filter tables.
///
/// # Parameters
/// - `frame`: Mutable slice of Q0 samples to be filtered in-place.
///
/// # Panics
/// Panics if `self.transition_frame_no` is outside the range `[0, TRANSITION_FRAMES]`.
#[allow(
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::arithmetic_side_effects
)]
pub fn lp_variable_cutoff(&mut self, frame: &mut [i16]) {
assert!(
self.transition_frame_no >= 0 && self.transition_frame_no <= TRANSITION_FRAMES,
"transition_frame_no must be in range [0, {}]",
TRANSITION_FRAMES
);
// Run filter only if mode is active
if self.mode == 0 {
return;
}
// Calculate index and interpolation factor for interpolation
let mut fac_q16 = if TRANSITION_INT_STEPS == 64 {
// Optimized shift when TRANSITION_INT_STEPS == 64
(TRANSITION_FRAMES - self.transition_frame_no) << (16 - 6)
} else {
// General division
((TRANSITION_FRAMES - self.transition_frame_no) << 16) / TRANSITION_INT_STEPS
};
let ind = (fac_q16 >> 16) as usize;
fac_q16 -= (ind as i32) << 16;
assert!(
ind < TRANSITION_INT_NUM,
"index must be less than {}",
TRANSITION_INT_NUM
);
// Interpolate filter coefficients
let mut b_q28 = [0i32; TRANSITION_NB];
let mut a_q28 = [0i32; TRANSITION_NA];
lp_interpolate_filter_taps(&mut b_q28, &mut a_q28, ind, fac_q16);
// Update transition frame number for next frame
self.transition_frame_no =
limit(self.transition_frame_no + self.mode, 0, TRANSITION_FRAMES);
// ARMA low-pass filtering
// Note: TRANSITION_NB == 3 and TRANSITION_NA == 2 (hardcoded in tables)
assert_eq!(TRANSITION_NB, 3);
assert_eq!(TRANSITION_NA, 2);
biquad_alt_stride1_inplace(frame, &b_q28, &a_q28, &mut self.in_lp_state);
}
}
/// Interpolates filter taps between pre-computed coefficient tables.
///
/// This helper function performs piece-wise linear interpolation of the numerator (B)
/// and denominator (A) coefficients based on the given index and interpolation factor.
///
/// # Parameters
/// - `b_q28`: Output array for interpolated B (numerator) coefficients in Q28.
/// - `a_q28`: Output array for interpolated A (denominator) coefficients in Q28.
/// - `ind`: Index into the coefficient tables (must be less than
/// `TRANSITION_INT_NUM - 1` for interpolation to occur).
/// - `fac_q16`: Q16 interpolation factor within the current interval.
#[allow(
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::arithmetic_side_effects
)]
fn lp_interpolate_filter_taps(
b_q28: &mut [i32; TRANSITION_NB],
a_q28: &mut [i32; TRANSITION_NA],
ind: usize,
fac_q16: i32,
) {
if ind < TRANSITION_INT_NUM - 1 {
if fac_q16 > 0 {
if fac_q16 < 32768 {
// fac_q16 is in range of a 16-bit int
// Piece-wise linear interpolation of B and A
for nb in 0..TRANSITION_NB {
b_q28[nb] = smlawb(
SILK_TRANSITION_LP_B_Q28[ind][nb],
SILK_TRANSITION_LP_B_Q28[ind + 1][nb] - SILK_TRANSITION_LP_B_Q28[ind][nb],
fac_q16,
);
}
for na in 0..TRANSITION_NA {
a_q28[na] = smlawb(
SILK_TRANSITION_LP_A_Q28[ind][na],
SILK_TRANSITION_LP_A_Q28[ind + 1][na] - SILK_TRANSITION_LP_A_Q28[ind][na],
fac_q16,
);
}
} else {
// (fac_q16 - (1 << 16)) is in range of a 16-bit int
assert_eq!(
fac_q16 - (1 << 16),
sat16(fac_q16 - (1 << 16)),
"fac_q16 - 65536 must be in 16-bit range"
);
// Piece-wise linear interpolation of B and A
for nb in 0..TRANSITION_NB {
b_q28[nb] = smlawb(
SILK_TRANSITION_LP_B_Q28[ind + 1][nb],
SILK_TRANSITION_LP_B_Q28[ind + 1][nb] - SILK_TRANSITION_LP_B_Q28[ind][nb],
fac_q16 - (1 << 16),
);
}
for na in 0..TRANSITION_NA {
a_q28[na] = smlawb(
SILK_TRANSITION_LP_A_Q28[ind + 1][na],
SILK_TRANSITION_LP_A_Q28[ind + 1][na] - SILK_TRANSITION_LP_A_Q28[ind][na],
fac_q16 - (1 << 16),
);
}
}
} else {
// fac_q16 == 0, no interpolation needed
b_q28.copy_from_slice(&SILK_TRANSITION_LP_B_Q28[ind]);
a_q28.copy_from_slice(&SILK_TRANSITION_LP_A_Q28[ind]);
}
} else {
// Use last table entry
b_q28.copy_from_slice(&SILK_TRANSITION_LP_B_Q28[TRANSITION_INT_NUM - 1]);
a_q28.copy_from_slice(&SILK_TRANSITION_LP_A_Q28[TRANSITION_INT_NUM - 1]);
}
}
/// Fixed-point multiply-accumulate: a + ((b * c[15:0]) >> 16).
///
/// This mirrors the `silk_SMLAWB` macro from the C implementation. It multiplies
/// the full 32-bit `b` by the low 16 bits of `c` (sign-extended), shifts the
/// 48-bit product right by 16 bits, and adds the result to `a`.
#[inline]
fn smlawb(a: i32, b: i32, c: i32) -> i32 {
let product = i64::from(b) * i64::from(c as i16);
a.wrapping_add((product >> 16) as i32)
}
/// Saturates a 32-bit value to 16-bit signed range.
#[inline]
fn sat16(value: i32) -> i32 {
if value > i32::from(i16::MAX) {
i32::from(i16::MAX)
} else if value < i32::from(i16::MIN) {
i32::from(i16::MIN)
} else {
value
}
}
/// Clamps a value to the given range.
#[inline]
fn limit(value: i32, min: i32, max: i32) -> i32 {
if value < min {
min
} else if value > max {
max
} else {
value
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lp_state_default_is_zeroed() {
let state = LpState::default();
assert_eq!(state.in_lp_state, [0, 0]);
assert_eq!(state.transition_frame_no, 0);
assert_eq!(state.mode, 0);
assert_eq!(state.saved_fs_khz, 0);
}
#[test]
fn lp_variable_cutoff_does_nothing_when_mode_is_zero() {
let mut state = LpState::new();
state.mode = 0;
let mut frame = [100i16, -200, 300, -400, 500];
let original = frame;
state.lp_variable_cutoff(&mut frame);
assert_eq!(frame, original, "frame should be unchanged when mode is 0");
assert_eq!(
state.transition_frame_no, 0,
"transition_frame_no should not change"
);
}
#[test]
fn lp_variable_cutoff_updates_transition_frame_no() {
let mut state = LpState::new();
state.mode = 1;
state.transition_frame_no = 100;
let mut frame = [0i16; 160];
state.lp_variable_cutoff(&mut frame);
assert_eq!(
state.transition_frame_no, 101,
"transition_frame_no should increment by mode"
);
}
#[test]
fn lp_variable_cutoff_clamps_transition_frame_no() {
let mut state = LpState::new();
state.mode = 10;
state.transition_frame_no = TRANSITION_FRAMES - 5;
let mut frame = [0i16; 160];
state.lp_variable_cutoff(&mut frame);
assert_eq!(
state.transition_frame_no, TRANSITION_FRAMES,
"transition_frame_no should be clamped to TRANSITION_FRAMES"
);
}
#[test]
fn lp_interpolate_uses_last_table_when_ind_at_end() {
let mut b = [0i32; TRANSITION_NB];
let mut a = [0i32; TRANSITION_NA];
lp_interpolate_filter_taps(&mut b, &mut a, TRANSITION_INT_NUM - 1, 12345);
assert_eq!(b, SILK_TRANSITION_LP_B_Q28[TRANSITION_INT_NUM - 1]);
assert_eq!(a, SILK_TRANSITION_LP_A_Q28[TRANSITION_INT_NUM - 1]);
}
#[test]
fn lp_interpolate_copies_when_fac_is_zero() {
let mut b = [0i32; TRANSITION_NB];
let mut a = [0i32; TRANSITION_NA];
lp_interpolate_filter_taps(&mut b, &mut a, 2, 0);
assert_eq!(b, SILK_TRANSITION_LP_B_Q28[2]);
assert_eq!(a, SILK_TRANSITION_LP_A_Q28[2]);
}
#[test]
fn smlawb_matches_reference() {
// smlawb(a, b, c) = a + ((b * c[15:0]) >> 16)
// where c[15:0] is the low 16 bits of c interpreted as signed i16
assert_eq!(smlawb(1000, 0x10000, 0x8000), -31768);
// 1000 + ((65536 * -32768) >> 16) = 1000 + (-32768) = -31768
assert_eq!(smlawb(0, 100_000, -1), -2);
// 0 + ((100000 * -1) >> 16) = 0 + (-2) = -2
assert_eq!(smlawb(500, -200_000, 0x1234), -13722);
// 500 + ((-200000 * 4660) >> 16) = 500 + (-14222) = -13722
}
#[test]
fn limit_clamps_below_min() {
assert_eq!(limit(-10, 0, 100), 0);
}
#[test]
fn limit_clamps_above_max() {
assert_eq!(limit(150, 0, 100), 100);
}
#[test]
fn limit_preserves_in_range() {
assert_eq!(limit(50, 0, 100), 50);
}
}