Skip to content

Commit 21cc75b

Browse files
Handle bidirectional AEC reference lag (#5838)
Delay either the speaker reference or mic input based on sync lag, reset AEC state on alignment changes, and cover negative-lag detection in audio sync tests.
1 parent 9ddabd2 commit 21cc75b

5 files changed

Lines changed: 606 additions & 165 deletions

File tree

crates/audio-actual/src/capture/stream.rs

Lines changed: 115 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,25 @@ async fn run_dual_loop(
165165
while let Some((raw_mic, raw_speaker)) = joiner.pop_pair() {
166166
let raw_mic = Arc::<[f32]>::from(raw_mic);
167167
let raw_speaker = Arc::<[f32]>::from(raw_speaker);
168-
let aec_reference_speaker = aec_reference
168+
let aligned = aec_reference
169169
.as_mut()
170170
.map(|aligner| aligner.align(&raw_speaker, &raw_mic))
171-
.unwrap_or_else(|| Arc::clone(&raw_speaker));
171+
.unwrap_or_else(|| AecAlignedPair {
172+
mic: Arc::clone(&raw_mic),
173+
speaker: Arc::clone(&raw_speaker),
174+
alignment_changed: false,
175+
});
176+
if aligned.alignment_changed {
177+
if let Some(processor) = aec.as_mut() {
178+
processor.reset();
179+
}
180+
linear_echo_gain = None;
181+
}
172182
let aec_mic = process_aec(
173183
&mut aec,
174184
&mut linear_echo_gain,
175-
&raw_mic,
176-
&aec_reference_speaker,
185+
&aligned.mic,
186+
&aligned.speaker,
177187
);
178188
if tx
179189
.send(Ok(CaptureFrame {
@@ -199,13 +209,51 @@ async fn run_dual_loop(
199209

200210
struct AecReferenceAligner {
201211
probe: SyncProbe,
202-
delay_line: SampleDelayLine,
203-
last_delay_samples: usize,
212+
mic_delay_line: SampleDelayLine,
213+
speaker_delay_line: SampleDelayLine,
214+
last_alignment: AecAlignment,
204215
last_logged_state: Option<SyncProbeState>,
205216
last_logged_stable_lag_samples: Option<isize>,
206217
log_probe_events: bool,
207218
}
208219

220+
struct AecAlignedPair {
221+
mic: Arc<[f32]>,
222+
speaker: Arc<[f32]>,
223+
alignment_changed: bool,
224+
}
225+
226+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227+
enum AecAlignment {
228+
None,
229+
DelayMic(usize),
230+
DelaySpeaker(usize),
231+
}
232+
233+
impl AecAlignment {
234+
fn from_lag_samples(lag_samples: Option<isize>) -> Self {
235+
match lag_samples {
236+
Some(lag) if lag > 0 => Self::DelaySpeaker(lag as usize),
237+
Some(lag) if lag < 0 => Self::DelayMic(lag.unsigned_abs()),
238+
_ => Self::None,
239+
}
240+
}
241+
242+
fn mic_delay_samples(self) -> usize {
243+
match self {
244+
Self::DelayMic(samples) => samples,
245+
Self::None | Self::DelaySpeaker(_) => 0,
246+
}
247+
}
248+
249+
fn speaker_delay_samples(self) -> usize {
250+
match self {
251+
Self::DelaySpeaker(samples) => samples,
252+
Self::None | Self::DelayMic(_) => 0,
253+
}
254+
}
255+
}
256+
209257
impl AecReferenceAligner {
210258
fn new(sample_rate: u32) -> Self {
211259
let max_lag_samples = ((sample_rate as usize) * (AEC_MAX_REFERENCE_LAG_MS as usize)) / 1000;
@@ -215,15 +263,16 @@ impl AecReferenceAligner {
215263

216264
Self {
217265
probe: SyncProbe::new(config),
218-
delay_line: SampleDelayLine::new(max_delay_samples),
219-
last_delay_samples: 0,
266+
mic_delay_line: SampleDelayLine::new(max_delay_samples),
267+
speaker_delay_line: SampleDelayLine::new(max_delay_samples),
268+
last_alignment: AecAlignment::None,
220269
last_logged_state: None,
221270
last_logged_stable_lag_samples: None,
222271
log_probe_events: std::env::var(AUDIO_SYNC_PROBE_ENV).ok().as_deref() == Some("1"),
223272
}
224273
}
225274

226-
fn align(&mut self, raw_speaker: &[f32], raw_mic: &[f32]) -> Arc<[f32]> {
275+
fn align(&mut self, raw_speaker: &[f32], raw_mic: &[f32]) -> AecAlignedPair {
227276
let observed = catch_unwind(AssertUnwindSafe(|| {
228277
self.probe.observe(raw_speaker, raw_mic)
229278
}));
@@ -235,44 +284,62 @@ impl AecReferenceAligner {
235284
}
236285
};
237286

238-
if let Some(event) = event {
239-
self.update_delay(&event);
287+
let alignment_changed = if let Some(event) = event {
288+
let alignment_changed = self.update_alignment(&event);
240289
if self.log_probe_events {
241290
self.log_probe_event(event);
242291
}
243-
}
292+
alignment_changed
293+
} else {
294+
false
295+
};
244296

245-
Arc::<[f32]>::from(
246-
self.delay_line
247-
.process(raw_speaker, self.last_delay_samples),
248-
)
297+
let aligned_mic = self
298+
.mic_delay_line
299+
.process(raw_mic, self.last_alignment.mic_delay_samples());
300+
let aligned_speaker = self
301+
.speaker_delay_line
302+
.process(raw_speaker, self.last_alignment.speaker_delay_samples());
303+
304+
AecAlignedPair {
305+
mic: Arc::from(aligned_mic),
306+
speaker: Arc::from(aligned_speaker),
307+
alignment_changed,
308+
}
249309
}
250310

251-
fn update_delay(&mut self, event: &SyncProbeEvent) {
311+
fn update_alignment(&mut self, event: &SyncProbeEvent) -> bool {
252312
let snapshot = event.snapshot();
253-
let next_delay = if matches!(
313+
let next_alignment = if matches!(
254314
snapshot.state,
255315
SyncProbeState::Locked | SyncProbeState::Holdover
256316
) {
257-
snapshot
258-
.stable_lag_samples
259-
.filter(|lag| *lag > 0)
260-
.map(|lag| lag as usize)
261-
.unwrap_or(0)
317+
AecAlignment::from_lag_samples(snapshot.stable_lag_samples)
262318
} else {
263-
0
319+
AecAlignment::None
264320
};
265321

266-
if next_delay != self.last_delay_samples {
322+
if next_alignment != self.last_alignment {
267323
tracing::info!(
268-
previous_delay_samples = self.last_delay_samples,
269-
delay_samples = next_delay,
270-
delay_ms = next_delay as f32 / self.probe.config().sample_rate as f32 * 1000.0,
324+
previous_alignment = ?self.last_alignment,
325+
alignment = ?next_alignment,
326+
mic_delay_samples = next_alignment.mic_delay_samples(),
327+
speaker_delay_samples = next_alignment.speaker_delay_samples(),
328+
mic_delay_ms = next_alignment.mic_delay_samples() as f32
329+
/ self.probe.config().sample_rate as f32
330+
* 1000.0,
331+
speaker_delay_ms = next_alignment.speaker_delay_samples() as f32
332+
/ self.probe.config().sample_rate as f32
333+
* 1000.0,
334+
stable_lag_samples = snapshot.stable_lag_samples,
271335
state = ?snapshot.state,
272-
"aec_reference_delay_changed"
336+
"aec_reference_alignment_changed"
273337
);
274-
self.last_delay_samples = next_delay;
338+
self.last_alignment = next_alignment;
339+
return true;
275340
}
341+
342+
false
276343
}
277344

278345
fn log_probe_event(&mut self, event: SyncProbeEvent) {
@@ -636,4 +703,22 @@ mod tests {
636703
assert_eq!(first, vec![0.0, 0.0, 1.0]);
637704
assert_eq!(second, vec![2.0]);
638705
}
706+
707+
#[test]
708+
fn aec_alignment_delays_speaker_for_positive_lag() {
709+
let alignment = AecAlignment::from_lag_samples(Some(320));
710+
711+
assert_eq!(alignment, AecAlignment::DelaySpeaker(320));
712+
assert_eq!(alignment.mic_delay_samples(), 0);
713+
assert_eq!(alignment.speaker_delay_samples(), 320);
714+
}
715+
716+
#[test]
717+
fn aec_alignment_delays_mic_for_negative_lag() {
718+
let alignment = AecAlignment::from_lag_samples(Some(-320));
719+
720+
assert_eq!(alignment, AecAlignment::DelayMic(320));
721+
assert_eq!(alignment.mic_delay_samples(), 320);
722+
assert_eq!(alignment.speaker_delay_samples(), 0);
723+
}
639724
}

crates/audio-sync/src/estimator.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,14 @@ mod tests {
177177
out
178178
}
179179

180+
fn advance_signal(input: &[f32], advance_samples: usize) -> Vec<f32> {
181+
let mut out = vec![0.0; input.len()];
182+
for idx in 0..input.len().saturating_sub(advance_samples) {
183+
out[idx] = input[idx + advance_samples];
184+
}
185+
out
186+
}
187+
180188
#[test]
181189
fn gcc_phat_estimates_positive_delay() {
182190
let window = 4096;
@@ -190,4 +198,18 @@ mod tests {
190198
assert_eq!(estimate.lag_samples, delay as isize);
191199
assert!(estimate.peak_ratio > 1.0);
192200
}
201+
202+
#[test]
203+
fn gcc_phat_estimates_negative_delay() {
204+
let window = 4096;
205+
let delay = 192;
206+
let reference = excitation(window);
207+
let observed = advance_signal(&reference, delay);
208+
209+
let mut estimator = GccPhatLagEstimator::new(window, 512);
210+
let estimate = estimator.estimate(&reference, &observed).unwrap();
211+
212+
assert_eq!(estimate.lag_samples, -(delay as isize));
213+
assert!(estimate.peak_ratio > 1.0);
214+
}
193215
}

crates/audio-sync/src/probe.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,14 @@ mod tests {
607607
out
608608
}
609609

610+
fn advance_signal(input: &[f32], advance_samples: usize) -> Vec<f32> {
611+
let mut out = vec![0.0; input.len()];
612+
for idx in 0..input.len().saturating_sub(advance_samples) {
613+
out[idx] = input[idx + advance_samples];
614+
}
615+
out
616+
}
617+
610618
fn test_config(window_samples: usize) -> SyncProbeConfig {
611619
SyncProbeConfig {
612620
sample_rate: 16_000,
@@ -650,6 +658,26 @@ mod tests {
650658
assert_eq!(snapshot.accepted_window_count, 3);
651659
}
652660

661+
#[test]
662+
fn sync_probe_can_lock_negative_lag() {
663+
let window = 4096;
664+
let delay = 192;
665+
let reference = excitation(window);
666+
let observed = advance_signal(&reference, delay);
667+
let mut probe = SyncProbe::new(test_config(window));
668+
669+
for _ in 0..2 {
670+
let event = probe.observe(&reference, &observed).unwrap();
671+
assert_eq!(event.snapshot().state, SyncProbeState::Acquiring);
672+
}
673+
674+
let locked = probe.observe(&reference, &observed).unwrap();
675+
let snapshot = locked.snapshot();
676+
677+
assert_eq!(snapshot.state, SyncProbeState::Locked);
678+
assert_eq!(snapshot.stable_lag_samples, Some(-(delay as isize)));
679+
}
680+
653681
#[test]
654682
fn sync_probe_enters_holdover_and_then_lost() {
655683
let window = 4096;

0 commit comments

Comments
 (0)