Refresh the speaker format when restarting voice output (#44924)

## Why

Opening a Bluetooth microphone can change the speaker format, leaving the previously cached sample rate unsupported when the speaker stream restarts.

## What changed

Requery and validate the speaker configuration after stopping the old stream. Update playback and render resampling to the current sample rate, and revalidate callback timing. Discard stale echo references while preserving capture history and audio processing state.

## Testing

Extend the speaker reset test to cover 48, 24, and 44.1 kHz output rates, checking that render output matches a fresh converter while capture history and pending samples are preserved.

GitOrigin-RevId: 3a4fba577288a83a7509fc78be86171ac85a3c0a
This commit is contained in:
Benjamin Carlsson
2026-09-11 21:59:10 +00:00
committed by copyberry
parent ce7fbb373b
commit 7ef70f95d5
3 changed files with 71 additions and 45 deletions

View File

@@ -47,8 +47,7 @@ pub(super) struct Devices {
_input: cpal::Stream,
output: Option<cpal::Stream>,
output_device: cpal::Device,
output_config: cpal::SupportedStreamConfig,
output_stream_config: cpal::StreamConfig,
input_frames: u32,
playback: PlaybackPort,
playout: Option<playout::Playout>,
worker: capture_worker::CaptureWorker,
@@ -91,14 +90,6 @@ impl Devices {
let output_config = output
.default_output_config()
.map_err(|_| io::Error::other("speaker configuration unavailable"))?;
for config in [&input_config, &output_config] {
if config.channels() == 0
|| config.channels() > 32
|| !(8_000..=384_000).contains(&config.sample_rate())
{
return Err(io::Error::other("unsupported audio device configuration"));
}
}
let input_stream_config = bounded_stream_config(&input_config)?;
let output_stream_config = bounded_stream_config(&output_config)?;
let buffers = Arc::new(Buffers::new(
@@ -144,8 +135,7 @@ impl Devices {
_input: input,
output: Some(output_stream),
output_device: output,
output_config,
output_stream_config,
input_frames,
playback,
playout: None,
worker: capture_worker::CaptureWorker {
@@ -181,16 +171,35 @@ impl Devices {
buffers.last_dac_ns.store(/*val*/ 0, Ordering::Release);
drop(producer);
while buffers.rendered.pop().is_some() {}
self.worker.processor.reset_render();
// Opening a Bluetooth microphone can change the speaker's format.
// Requery after stopping the old stream rather than restoring its
// pre-microphone rate, which the device may no longer support.
let output_config = self
.output_device
.default_output_config()
.map_err(|_| io::Error::other("speaker configuration unavailable"))?;
let output_stream_config = bounded_stream_config(&output_config)?;
let cpal::BufferSize::Fixed(output_frames) = output_stream_config.buffer_size else {
return Err(io::Error::other("audio callback size unavailable"));
};
self.worker
.processor
.set_render_rate(output_config.sample_rate())
.map_err(io::Error::other)?;
self.worker
.processor
.validate_callback_timing(self.input_frames, output_frames)
.map_err(io::Error::other)?;
self.playback = PlaybackPort::new(buffers.clone(), output_config.sample_rate());
if !controls.speaker_suppressed {
self.playout =
Some(playout::Playout::new(self.playback.writer()).map_err(io::Error::other)?);
}
let output = stream!(
self.output_config.sample_format(),
output_config.sample_format(),
build_output,
&self.output_device,
&self.output_stream_config,
&output_stream_config,
buffers
)
.map_err(|_| io::Error::other("failed to reset speaker"))?;
@@ -234,6 +243,12 @@ impl Drop for Devices {
fn bounded_stream_config(
supported: &cpal::SupportedStreamConfig,
) -> io::Result<cpal::StreamConfig> {
if supported.channels() == 0
|| supported.channels() > 32
|| !(8_000..=384_000).contains(&supported.sample_rate())
{
return Err(io::Error::other("unsupported audio device configuration"));
}
let cpal::SupportedBufferSize::Range { min, max } = *supported.buffer_size() else {
return Err(io::Error::other("audio callback size range unavailable"));
};

View File

@@ -151,6 +151,15 @@ impl Processor {
Ok(())
}
pub(super) fn set_render_rate(&mut self, rate: u32) -> Result<()> {
// Capture keeps its own rate and history across speaker-only changes.
if self.render.rate != rate {
self.render = Converter::new(rate)?;
}
self.reset_render();
Ok(())
}
pub(super) fn reset_render(&mut self) {
// Speaker changes discard old references without resetting capture or APM state.
self.render.resampler.reset();

View File

@@ -156,38 +156,40 @@ fn unmute_reset_rejects_delayed_pre_unmute_audio_and_partial_history() {
}
#[test]
fn speaker_reset_discards_partial_echo_reference_but_keeps_capture_history() {
let mut processor = Processor::new(/*input_rate*/ 48_000, /*output_rate*/ 48_000).unwrap();
let mut fresh = Processor::new(/*input_rate*/ 48_000, /*output_rate*/ 48_000).unwrap();
let start = Instant::now();
let mut frame = Frame {
samples: [0.75; 256],
len: 256,
at: start,
generation: 2,
};
processor.render.push(&frame).unwrap();
processor.render.output.push_back(0.75);
processor.capture.input.extend([0.25; 256]);
processor.pending.extend([0.25; 480]);
processor.render_delay = 123;
processor.reset_render();
assert_eq!(processor.capture.input, VecDeque::from(vec![0.25; 256]));
assert_eq!(processor.pending, vec![0.25; 480]);
assert_eq!(processor.render_delay, 0);
frame.samples.fill(0.0);
let mut actual = Vec::new();
let mut expected = Vec::new();
for index in 0..5 {
frame.at = start + Duration::from_secs_f64(index as f64 * 256.0 / 48_000.0);
fn speaker_rate_change_discards_old_reference_but_keeps_capture_history() {
for rate in [48_000, 24_000, 44_100] {
let mut processor = Processor::new(/*input_rate*/ 48_000, /*output_rate*/ 48_000).unwrap();
let mut fresh = Processor::new(/*input_rate*/ 48_000, rate).unwrap();
let start = Instant::now();
let mut frame = Frame {
samples: [0.75; 256],
len: 256,
at: start,
generation: 2,
};
processor.render.push(&frame).unwrap();
fresh.render.push(&frame).unwrap();
actual.extend(std::iter::from_fn(|| processor.render.next()));
expected.extend(std::iter::from_fn(|| fresh.render.next()));
processor.render.output.push_back(0.75);
processor.capture.input.extend([0.25; 256]);
processor.pending.extend([0.25; 480]);
processor.render_delay = 123;
processor.set_render_rate(rate).unwrap();
assert_eq!(processor.capture.input, VecDeque::from(vec![0.25; 256]));
assert_eq!(processor.pending, vec![0.25; 480]);
assert_eq!(processor.render_delay, 0);
frame.samples.fill(0.0);
let mut actual = Vec::new();
let mut expected = Vec::new();
for index in 0..5 {
frame.at = start + Duration::from_secs_f64(index as f64 * 256.0 / f64::from(rate));
processor.render.push(&frame).unwrap();
fresh.render.push(&frame).unwrap();
actual.extend(std::iter::from_fn(|| processor.render.next()));
expected.extend(std::iter::from_fn(|| fresh.render.next()));
}
assert!(!actual.is_empty());
assert_eq!(actual, expected);
}
assert!(!actual.is_empty());
assert_eq!(actual, expected);
}
#[test]