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
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
/*
* Copyright 2025 Security Union LLC
*
* Licensed under either of
*
* * Apache License, Version 2.0
* (http://www.apache.org/licenses/LICENSE-2.0)
* * MIT license
* (http://opensource.org/licenses/MIT)
*
* at your option.
*
* Unless you explicitly state otherwise, any contribution intentionally
* submitted for inclusion in the work by you, as defined in the Apache-2.0
* license, shall be dual licensed as above, without any additional terms or
* conditions.
*/
/// Buffer level filter for smoothing buffer measurements
///
/// This filter prevents oscillating acceleration/deceleration decisions by
/// smoothing the buffer level measurements over time. It accounts for
/// time-stretched samples from previous operations to provide accurate
/// buffering decisions.
///
/// Based on WebRTC's BufferLevelFilter implementation.
#[derive(Debug)]
pub struct BufferLevelFilter {
/// Current filtered buffer level in samples
filtered_level_samples: f64,
/// Filter coefficient (dynamic based on target delay)
level_factor: f64,
/// Sample rate for conversions
sample_rate_hz: u32,
}
impl BufferLevelFilter {
/// Create a new buffer level filter
pub fn new(sample_rate_hz: u32) -> Self {
Self {
filtered_level_samples: 0.0,
level_factor: 253.0 / 256.0, // Default coefficient (α ≈ 0.988)
sample_rate_hz,
}
}
/// Update the filter with new buffer size and time-stretched samples
///
/// Hybrid approach:
/// 1. Use WebRTC's conservative filtering for normal operations
/// 2. Become more responsive for large buffer jumps (late-joining peers, bursts)
/// 3. Apply exponential filter to raw buffer_size_samples
/// 4. Then subtract time_stretched_samples
///
/// # Arguments
/// * `buffer_size_samples` - Current raw buffer size in samples
/// * `time_stretched_samples` - Samples added/removed by time-stretching operations
pub fn update(&mut self, buffer_size_samples: usize, time_stretched_samples: i32) {
// Detect large buffer jumps that need faster response
let buffer_jump = (buffer_size_samples as f64 - self.filtered_level_samples).abs();
// Smart threshold: prevent false triggers on startup but catch real overloads
// - If current level is very low (<100), use higher threshold (startup protection)
// - Otherwise use normal threshold for established operation
let buffer_jump_threshold = if self.filtered_level_samples < 100.0 {
// Startup scenario: need much larger jump to trigger aggressive mode
(buffer_size_samples as f64 * 0.75).max(3000.0) // 75% of target or 3000 samples
} else {
// Normal operation: use responsive threshold
(self.filtered_level_samples * 3.0).max(1500.0)
};
// Choose coefficient based on buffer jump size
let effective_factor = if buffer_jump > buffer_jump_threshold {
// Large jump detected - use much more responsive coefficient temporarily
// WebRTC uses α≈0.988, we use α≈0.7 for large jumps (30% new value vs 1.2%)
0.7
} else {
// Normal WebRTC conservative behavior
self.level_factor
};
// Step 1: Apply exponential smoothing to raw buffer size (WebRTC's order)
// filtered = α × previous_filtered + (1-α) × buffer_size_samples
let filtered_level = effective_factor * self.filtered_level_samples
+ (1.0 - effective_factor) * buffer_size_samples as f64;
// Step 2: Account for time-scale operations and ensure non-negative
// Subtract time_stretched_samples AFTER filtering (WebRTC's approach)
self.filtered_level_samples = (filtered_level - time_stretched_samples as f64).max(0.0);
}
/// Set the filtered buffer level directly (used for buffer flush scenarios)
/// Matches WebRTC's SetFilteredBufferLevel
pub fn set_filtered_buffer_level(&mut self, buffer_size_samples: usize) {
self.filtered_level_samples = buffer_size_samples as f64;
}
/// Get the current filtered buffer level in samples
/// Matches WebRTC's filtered_current_level()
pub fn filtered_current_level(&self) -> usize {
self.filtered_level_samples.max(0.0) as usize
}
/// Get the current filtered buffer level in milliseconds
pub fn filtered_current_level_ms(&self) -> u32 {
if self.sample_rate_hz == 0 {
return 0;
}
let samples = self.filtered_current_level();
(samples as u64 * 1000 / self.sample_rate_hz as u64) as u32
}
/// Set the target buffer level and adjust filter coefficient accordingly
/// Matches WebRTC's SetTargetBufferLevel exactly
pub fn set_target_buffer_level(&mut self, target_level_ms: u32) {
// WebRTC's dynamic coefficient selection based on target delay
self.level_factor = if target_level_ms <= 20 {
251.0 / 256.0 // α ≈ 0.980 - Most responsive for low latency
} else if target_level_ms <= 60 {
252.0 / 256.0 // α ≈ 0.984
} else if target_level_ms <= 140 {
253.0 / 256.0 // α ≈ 0.988 - Default
} else {
254.0 / 256.0 // α ≈ 0.992 - Most smoothing for high latency
};
}
/// Get the target buffer level in samples (for convenience)
pub fn target_level_samples(&self, target_level_ms: u32) -> usize {
if self.sample_rate_hz == 0 {
return 0;
}
(target_level_ms as u64 * self.sample_rate_hz as u64 / 1000) as usize
}
/// Reset the filter state
/// Matches WebRTC's Reset method
pub fn reset(&mut self) {
self.filtered_level_samples = 0.0;
self.level_factor = 253.0 / 256.0; // Reset to default coefficient
}
/// Update sample rate (called when sample rate changes)
pub fn set_sample_rate(&mut self, sample_rate_hz: u32) {
self.sample_rate_hz = sample_rate_hz;
}
/// Get current filter coefficient for debugging/testing
pub fn get_filter_coefficient(&self) -> f64 {
self.level_factor
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_buffer_level_filter_creation() {
let filter = BufferLevelFilter::new(16000);
assert_eq!(filter.filtered_current_level(), 0);
assert_eq!(filter.sample_rate_hz, 16000);
}
#[test]
fn test_buffer_level_filter_update() {
let mut filter = BufferLevelFilter::new(16000);
// WebRTC behavior: first update applies exponential filter from 0
filter.update(1000, 0);
let first_level = filter.filtered_current_level();
// With coefficient 253: (253*0>>8) + (256-253)*1000 = 3000 Q8 = ~12 samples
assert!((10..=15).contains(&first_level));
// Subsequent updates should be smoothed
filter.update(2000, 0);
let filtered_level = filter.filtered_current_level();
println!("First level: {first_level}, Second level: {filtered_level}");
// With hybrid approach: 2000 vs ~12 is a large jump (>3x threshold), triggers aggressive mode
// α=0.7: filtered = 0.7 * 12 + 0.3 * 2000 = 8.4 + 600 = ~608
// Should increase significantly due to large jump detection, but less than raw 2000
assert!(filtered_level > first_level && filtered_level < 1000);
}
#[test]
fn test_time_stretched_samples_compensation() {
let mut filter = BufferLevelFilter::new(16000);
// Update with time-stretched samples
filter.update(1000, 100); // 100 samples were time-stretched
let result = filter.filtered_current_level();
// With WebRTC: filtered = ((253*0>>8) + (256-253)*1000) - 100*256 = 3000 - 25600 = negative -> 0
// WebRTC clamps negative values to 0
assert_eq!(result, 0);
}
#[test]
fn test_set_filtered_buffer_level() {
let mut filter = BufferLevelFilter::new(16000);
filter.set_filtered_buffer_level(500);
assert_eq!(filter.filtered_current_level(), 500);
}
#[test]
fn test_target_level_conversion() {
let mut filter = BufferLevelFilter::new(16000);
filter.set_target_buffer_level(100); // 100ms
assert_eq!(filter.target_level_samples(100), 1600); // 100ms * 16000Hz / 1000 = 1600 samples
}
#[test]
fn test_filtered_level_ms_conversion() {
let mut filter = BufferLevelFilter::new(16000);
filter.set_filtered_buffer_level(1600); // 1600 samples
assert_eq!(filter.filtered_current_level_ms(), 100); // 1600 * 1000 / 16000 = 100ms
}
#[test]
fn test_reset() {
let mut filter = BufferLevelFilter::new(16000);
filter.update(1000, 0);
let before_reset = filter.filtered_current_level();
// WebRTC behavior: first update gives α*0 + (1-α)*1000 = small value
assert!(
before_reset > 0,
"Filter should have non-zero value after update"
);
filter.reset();
assert_eq!(filter.filtered_current_level(), 0);
}
// === NEW TESTS TO DEBUG THE STUCK FILTER ===
#[test]
fn test_stuck_filter_reproduction() {
let mut filter = BufferLevelFilter::new(48000); // 48kHz like in user's logs
filter.set_target_buffer_level(80); // 80ms target
// Simulate the exact scenario from user logs:
// Raw buffer around 11k samples (230ms), filtered stuck at 1920 samples (40ms)
// Initialize with WebRTC-style behavior (starts from 0)
filter.update(1920, 0); // First update with 1920 samples
let initial_level = filter.filtered_current_level();
// WebRTC behavior: starts from 0, so gets small initial value
assert!(
(20..=30).contains(&initial_level),
"Expected ~23, got {initial_level}"
);
// Now try updating with much higher raw buffer values
for i in 1..=10 {
let raw_samples = 11000 + i * 100; // 11k+ samples like in logs
let prev_filtered = filter.filtered_current_level();
filter.update(raw_samples, 0);
let new_filtered = filter.filtered_current_level();
println!(
"Iteration {}: raw={}, prev_filtered={}, new_filtered={}, change={}",
i,
raw_samples,
prev_filtered,
new_filtered,
new_filtered as i32 - prev_filtered as i32
);
// With smoothing_factor 0.6: new = 0.6 * old + 0.4 * current
// Expected: new = 0.6 * 1920 + 0.4 * 11000 = 1152 + 4400 = 5552
if i == 1 {
// First large jump (from ~23 to 11100) should trigger aggressive mode
// α=0.7: filtered = 0.7 * 23 + 0.3 * 11100 = 16.1 + 3330 = ~3346
assert!(
new_filtered > 3000,
"Filter should use aggressive mode for large jump from {prev_filtered} to {raw_samples}, got {new_filtered}"
);
}
}
}
#[test]
fn test_bounds_clamping_bug() {
let mut filter = BufferLevelFilter::new(48000);
filter.set_target_buffer_level(80); // 80ms = 3840 samples at 48kHz
// Test if bounds clamping is causing the stuck behavior
filter.update(1920, 0); // Initialize at 40ms
// Target is 80ms = 3840 samples
// Raw buffer is 240ms = 11520 samples
// This is > 2x target (7680), so bounds kick in
let raw_samples = 11520;
let target_samples = 3840;
filter.update(raw_samples, 0);
let filtered = filter.filtered_current_level();
println!("Raw: {raw_samples}, Target: {target_samples}, Filtered: {filtered}");
println!("Target/2: {}", target_samples / 2);
// Check if it's clamped to target/2 = 1920
if filtered == target_samples / 2 {
println!(
"BUG FOUND: Filter is clamped to target/2 = {} samples",
target_samples / 2
);
}
}
#[test]
fn test_mathematical_correctness() {
let mut filter = BufferLevelFilter::new(48000);
filter.set_target_buffer_level(100); // Set target for coefficient selection
// Test WebRTC-style behavior: starts from 0, applies exponential filter
filter.update(1000, 0);
let first_result = filter.filtered_current_level();
// With α=253/256≈0.988: filtered = 0.988*0 + 0.012*1000 ≈ 12
assert!(
(10..=15).contains(&first_result),
"First update should be ~12, got {first_result}"
);
// Test small increment (conservative behavior)
// Jump from ~12 to 100 should be below threshold (12*3=36, but min is 1000, so need <1000 jump)
filter.update(100, 0);
let small_increment = filter.filtered_current_level();
// Small change, should use conservative coefficient
// α=253/256: filtered = 0.988*12 + 0.012*100 ≈ 11.8 + 1.2 ≈ 13
assert!(
(10..=20).contains(&small_increment),
"Small increment should be ~13, got {small_increment}"
);
// Test large jump (aggressive behavior)
filter.update(5000, 0);
let large_jump = filter.filtered_current_level();
// Large jump detected (5000 vs ~26), should use α=0.7
// filtered = 0.7*26 + 0.3*5000 = 18.2 + 1500 = ~1518
assert!(
(1400..=1600).contains(&large_jump),
"Large jump should trigger aggressive mode ~1518, got {large_jump}"
);
}
#[test]
fn test_time_stretched_samples_bug() {
let mut filter = BufferLevelFilter::new(48000);
filter.set_target_buffer_level(80); // 80ms target
// Initialize filter with WebRTC behavior
filter.update(1920, 0); // Start with 1920 samples
let initial_level = filter.filtered_current_level();
assert!(
(20..=30).contains(&initial_level),
"Expected ~23, got {initial_level}"
);
// Simulate the exact scenario from logs:
// Raw buffer is high (11k+ samples) but time-stretched samples make current_level = 1920
for i in 1..=10 {
let raw_samples = 11000 + i * 100; // High raw buffer
// HYPOTHESIS: Large time_stretched_samples cancel out the raw buffer
// current_level = raw_samples - time_stretched_samples
// If time_stretched_samples = raw_samples - 1920, then current_level = 1920
let time_stretched_samples = (raw_samples - 1920) as i32;
let prev_filtered = filter.filtered_current_level();
filter.update(raw_samples, time_stretched_samples);
let new_filtered = filter.filtered_current_level();
println!("Test iteration {}: raw={}, time_stretched={}, current_level={}, prev_filtered={}, new_filtered={}",
i, raw_samples, time_stretched_samples, raw_samples as i32 - time_stretched_samples, prev_filtered, new_filtered);
// If time_stretched_samples cancels out raw buffer growth, filter should stay low
if time_stretched_samples as usize == raw_samples - initial_level {
assert!(
(20..=30).contains(&new_filtered),
"Filter should be stuck at low value due to time-stretched compensation, got {new_filtered}"
);
}
}
}
#[test]
fn test_reset_loop_bug() {
let mut filter = BufferLevelFilter::new(48000);
filter.set_target_buffer_level(80);
// Test if filter gets reset repeatedly
filter.update(11000, 0); // Set to high value
let high_value = filter.filtered_current_level();
println!("DEBUG: After update(11000, 0), filtered_level = {high_value}");
// Expected: large jump from 0 to 11000 should trigger aggressive mode (11000 > 5000 threshold)
// α=0.7: filtered = 0.7 * 0 + 0.3 * 11000 = 3300
assert!(
high_value > 3000,
"Large jump should result in significant increase, got {high_value}"
);
// Reset and re-initialize at 1920
filter.reset();
filter.update(1920, 0);
let after_reset = filter.filtered_current_level();
println!("DEBUG: After reset and update(1920, 0), filtered_level = {after_reset}");
// After reset, filter starts from 0 again
// Jump 0→1920 is below 5000 threshold, so should use conservative filtering
// α=253/256: filtered = 0.988 * 0 + 0.012 * 1920 = ~23
assert!(
(20..=30).contains(&after_reset),
"After reset should use conservative filtering, got {after_reset}"
);
// If this happens every frame, filter would be stuck at 1920
println!("RESET BUG: Filter reset from {high_value} to 1920");
}
#[test]
fn test_sample_memory_accumulation_bug_fixed() {
let mut filter = BufferLevelFilter::new(48000);
filter.set_target_buffer_level(80);
// Simulate the bug scenario: sample_memory keeps accumulating
filter.update(1920, 0); // Initialize with WebRTC behavior
let initial_level = filter.filtered_current_level();
assert!(
(20..=30).contains(&initial_level),
"Expected ~23, got {initial_level}"
);
// Simulate multiple frames where sample_memory is NOT reset (the old bug)
let mut accumulated_sample_memory = 0i32;
for frame in 1..=10 {
let raw_samples = 11000 + frame * 100;
// Each acceleration operation adds ~1000 samples to memory (larger accumulation)
let samples_removed_this_frame = 1000i32;
accumulated_sample_memory += samples_removed_this_frame;
// OLD BUG: time_stretched_samples keeps growing
let time_stretched_samples = accumulated_sample_memory;
let _prev_filtered = filter.filtered_current_level();
filter.update(raw_samples, time_stretched_samples);
let new_filtered = filter.filtered_current_level();
println!(
"Frame {}: raw={}, time_stretched={}, current_level={}, filtered={}",
frame,
raw_samples,
time_stretched_samples,
raw_samples as i32 - time_stretched_samples,
new_filtered
);
// After a few frames, accumulated time_stretched should make current_level very low
if frame >= 5 {
let current_level = raw_samples as i32 - time_stretched_samples;
assert!(
current_level < 7000,
"Current level should be much lower due to accumulation, got {current_level}"
);
}
}
println!("BUG DEMONSTRATED: Filter gets wrong inputs due to sample_memory accumulation");
// NOW TEST THE FIX: sample_memory resets each frame
filter.update(1920, 0); // Re-initialize
for frame in 1..=5 {
let raw_samples = 11000 + frame * 100;
// FIXED: time_stretched_samples is only from this frame (sample_memory resets)
let samples_removed_this_frame = 500i32; // Only current frame
let time_stretched_samples = samples_removed_this_frame; // Not accumulated!
let _prev_filtered = filter.filtered_current_level();
filter.update(raw_samples, time_stretched_samples);
let new_filtered = filter.filtered_current_level();
println!(
"FIXED Frame {}: raw={}, time_stretched={}, current_level={}, filtered={}",
frame,
raw_samples,
time_stretched_samples,
raw_samples as i32 - time_stretched_samples,
new_filtered
);
// With reset sample_memory, current_level should be reasonable
let current_level = raw_samples as i32 - time_stretched_samples;
assert!(
current_level > 10000,
"Current level should be high with fixed sample_memory"
);
}
println!("FIX VERIFIED: Filter gets correct inputs when sample_memory resets");
}
#[test]
fn test_webrtc_exact_pattern() {
let mut filter = BufferLevelFilter::new(48000);
filter.set_target_buffer_level(80);
// Simulate WebRTC's exact pattern based on neteq_impl.cc
filter.update(1920, 0); // Initialize
// Simulate WebRTC's pattern:
// 1. Set sample_memory to available samples for time-stretching
// 2. Use it for decision
// 3. Subtract consumed samples after successful operation
for frame in 1..=10 {
let raw_samples = 11000 + frame * 100;
let samples_left = 2000; // Assume we have 2000 samples available
// WebRTC pattern: SET sample_memory to available samples (not accumulate)
let mut webrtc_sample_memory = samples_left; // Line 1304: set_sample_memory(samples_left + extracted_samples)
// Use for this frame's decision
let time_stretched_samples = webrtc_sample_memory;
let _prev_filtered = filter.filtered_current_level();
filter.update(raw_samples, time_stretched_samples);
let new_filtered = filter.filtered_current_level();
println!(
"WebRTC Frame {}: raw={}, sample_memory={}, current_level={}, filtered={}",
frame,
raw_samples,
webrtc_sample_memory,
raw_samples as i32 - time_stretched_samples,
new_filtered
);
// After successful time-stretching, WebRTC subtracts consumed samples (line 1084-1085)
// AddSampleMemory(-(samples_left + output_size_samples_))
let output_size_samples = 480; // 10ms at 48kHz
webrtc_sample_memory -= samples_left + output_size_samples;
println!(" After subtract: sample_memory={webrtc_sample_memory}");
}
println!("WebRTC pattern: sample_memory is set to available samples, then adjusted");
}
#[test]
fn test_our_neteq_fix_realistic_scenario() {
let mut filter = BufferLevelFilter::new(48000);
filter.set_target_buffer_level(80);
// Simulate our NetEQ implementation with the fix
filter.update(1920, 0); // Initialize
// Simulate realistic scenario: high raw buffer with reasonable time_stretched_samples
for frame in 1..=10 {
let raw_buffer_samples = 11000 + frame * 100; // High raw buffer (like user's 380ms)
// Our fix: sample_memory is set to available samples for time-stretching
// In our acceleration methods, we use extended_frame which might be ~1.5x normal frame
let frame_samples = 480; // 10ms at 48kHz
let extended_frame_samples = (frame_samples as f32 * 1.5) as usize; // ~720 samples
let available_samples_for_time_stretching = extended_frame_samples;
// This is what our fixed NetEQ should pass to the filter
let time_stretched_samples = available_samples_for_time_stretching as i32;
let prev_filtered = filter.filtered_current_level();
filter.update(raw_buffer_samples, time_stretched_samples);
let new_filtered = filter.filtered_current_level();
println!("FIXED Frame {}: raw_buffer={}ms ({}), time_stretched={}, current_level={}, filtered={}ms ({})",
frame,
raw_buffer_samples * 1000 / 48000, raw_buffer_samples,
time_stretched_samples,
raw_buffer_samples as i32 - time_stretched_samples,
new_filtered * 1000 / 48000, new_filtered);
// Verify the filter is moving and not stuck
if frame > 1 {
assert_ne!(
new_filtered, prev_filtered,
"Filter should not be stuck at same value"
);
}
// Verify current_level is reasonable (should be most of raw_buffer)
let current_level = raw_buffer_samples as i32 - time_stretched_samples;
assert!(
current_level > 10000,
"Current level should be high with reasonable time_stretched_samples, got {current_level}"
);
}
println!(
"✅ Our NetEQ fix produces reasonable time_stretched_samples, filter moves correctly"
);
}
#[test]
fn test_webrtc_compatible_behavior() {
let mut filter = BufferLevelFilter::new(16000);
// Test dynamic coefficient selection (WebRTC behavior)
filter.set_target_buffer_level(15); // <= 20ms
assert!((filter.get_filter_coefficient() - 251.0 / 256.0).abs() < 0.001);
filter.set_target_buffer_level(40); // <= 60ms
assert!((filter.get_filter_coefficient() - 252.0 / 256.0).abs() < 0.001);
filter.set_target_buffer_level(100); // <= 140ms
assert!((filter.get_filter_coefficient() - 253.0 / 256.0).abs() < 0.001);
filter.set_target_buffer_level(200); // > 140ms
assert!((filter.get_filter_coefficient() - 254.0 / 256.0).abs() < 0.001);
// Test that filter is much more conservative than before
filter.reset();
filter.set_target_buffer_level(80); // Default coefficient 253
// Initialize with 1000 samples
filter.update(1000, 0);
let initial_level = filter.filtered_current_level();
println!("Initial level after first update: {initial_level}");
// WebRTC starts from 0, so first update: (253*0>>8) + (256-253)*1000 = 3000 Q8 = 11-12 samples
// This is expected WebRTC behavior - very gradual response from zero state
assert!(
(10..=15).contains(&initial_level),
"Expected 10-15, got {initial_level}"
);
// Large jump to 5000 samples - hybrid behavior test
filter.update(5000, 0);
let after_jump = filter.filtered_current_level();
// With smart threshold: jump from ~11 to 5000 (4989) exceeds startup threshold (3750)
// So uses aggressive mode: α=0.7: new = 0.7*11 + 0.3*5000 = 7.7 + 1500 = ~1508
let expected_aggressive = (0.7 * initial_level as f64 + 0.3 * 5000.0) as usize;
println!("After jump: {after_jump}, expected: {expected_aggressive}");
assert!(
after_jump >= expected_aggressive - 50 && after_jump <= expected_aggressive + 50,
"Expected aggressive mode ~{expected_aggressive}, got {after_jump}"
);
println!("✅ Hybrid filter correctly uses aggressive mode for large jumps: {after_jump}");
}
}