Skip to content

Commit a3d3e5d

Browse files
Handle bidirectional AEC reference lag
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 2e0efc5 commit a3d3e5d

4 files changed

Lines changed: 331 additions & 32 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)