Switch audio devices without reopening the connection

A new input device used to wait for the next connect, and the shared mixer
kept its line on the old output until it went idle for good. The microphone
now reopens on the new device as the settings are applied, and the mixer
moves its line over when the output device changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 18:30:23 +00:00
parent 967fcadf77
commit ed526897dc
4 changed files with 25 additions and 4 deletions

View File

@@ -76,7 +76,7 @@ public final class CaptureVoiceInput implements VoiceInput {
private final AudioIo io; private final AudioIo io;
private final OpusCodec opus; private final OpusCodec opus;
private final String deviceName; private volatile String deviceName;
private final Object encoderLock = new Object(); private final Object encoderLock = new Object();
private volatile OpusParameters params; private volatile OpusParameters params;
@@ -117,6 +117,17 @@ public final class CaptureVoiceInput implements VoiceInput {
// ---- live configuration (safe to call from the UI thread) ---- // ---- live configuration (safe to call from the UI thread) ----
@Override
public synchronized void setInputDevice(String deviceId) {
String device = deviceId == null ? "" : deviceId;
if (device.equals(deviceName)) return;
deviceName = device;
if (running.get()) {
stop();
start();
}
}
public void setMode(Settings.InputMode mode) { public void setMode(Settings.InputMode mode) {
this.mode = mode; this.mode = mode;
} }

View File

@@ -66,9 +66,15 @@ final class MixedPlayout implements Playout {
float[] mix = null; float[] mix = null;
float[] scratch = null; float[] scratch = null;
byte[] out = null; byte[] out = null;
String device = null;
while (awaitSpeech()) { while (awaitSpeech()) {
if (line != null && !device.equals(outputDevice.get())) {
line.close();
line = null;
}
if (line == null) { if (line == null) {
line = io.openPlayback(outputDevice.get(), VoiceFormat.MAX_CHANNELS); device = outputDevice.get();
line = io.openPlayback(device, VoiceFormat.MAX_CHANNELS);
line.start(); line.start();
channels = line.channels(); channels = line.channels();
mix = new float[FRAME * channels]; mix = new float[FRAME * channels];

View File

@@ -60,7 +60,7 @@ public final class StreamingVoiceOutput implements VoiceOutput {
public StreamingVoiceOutput(AudioIo io, OpusCodec opus, String outputDevice, Lines lines) { public StreamingVoiceOutput(AudioIo io, OpusCodec opus, String outputDevice, Lines lines) {
this.opus = opus; this.opus = opus;
this.outputDevice = outputDevice; this.outputDevice = outputDevice == null ? "" : outputDevice;
this.playout = switch (lines) { this.playout = switch (lines) {
case PER_SPEAKER -> new PerSpeakerPlayout(io, () -> this.outputDevice, this::gainFor, clock); case PER_SPEAKER -> new PerSpeakerPlayout(io, () -> this.outputDevice, this::gainFor, clock);
case SHARED -> new MixedPlayout(io, () -> this.outputDevice, this::gainFor, streams.values()); case SHARED -> new MixedPlayout(io, () -> this.outputDevice, this::gainFor, streams.values());
@@ -137,7 +137,7 @@ public final class StreamingVoiceOutput implements VoiceOutput {
@Override @Override
public void setOutputDevice(String device) { public void setOutputDevice(String device) {
this.outputDevice = device; this.outputDevice = device == null ? "" : device;
} }
/** Entry point wired into {@code client.setVoiceHandler(...)}. */ /** Entry point wired into {@code client.setVoiceHandler(...)}. */

View File

@@ -27,6 +27,9 @@ public interface VoiceInput extends Microphone {
boolean isLocalMuted(); boolean isLocalMuted();
/** Moves capture to another device, reopening at once if capturing; empty is the default. */
void setInputDevice(String deviceId);
void setMode(Settings.InputMode mode); void setMode(Settings.InputMode mode);
void setVadMode(Settings.VadMode mode); void setVadMode(Settings.VadMode mode);
@@ -80,6 +83,7 @@ public interface VoiceInput extends Microphone {
/** Takes on the voice settings, as they stand now, while capturing. */ /** Takes on the voice settings, as they stand now, while capturing. */
default void apply(Settings settings) { default void apply(Settings settings) {
setInputDevice(settings.inputDevice);
setMode(settings.inputMode); setMode(settings.inputMode);
setVadMode(settings.vadMode); setVadMode(settings.vadMode);
setThresholdDb(settings.vadThresholdDb); setThresholdDb(settings.vadThresholdDb);