Skip to content

Restart microphone capture on audio device change#311

Open
MaxHeimbrock wants to merge 2 commits into
mainfrom
max/mic-restart-on-device-change
Open

Restart microphone capture on audio device change#311
MaxHeimbrock wants to merge 2 commits into
mainfrom
max/mic-restart-on-device-change

Conversation

@MaxHeimbrock

@MaxHeimbrock MaxHeimbrock commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Problem

When a capture device disappears mid-call (e.g. unplugging a Bluetooth headset and falling back to the built-in mic), the local microphone went silent and never recovered:

  • MicrophoneSource stayed bound to the now-gone device name and never re-resolved to the new default.
  • Unity tears down its audio graph on a device change, which detaches the AudioProbe.OnAudioFilterRead tap — so even the capture tap stopped firing.

The playback side already handles this in AudioStream.OnAudioConfigurationChanged; this mirrors that pattern for capture.

Change

  • Subscribe MicrophoneSource to AudioSettings.OnAudioConfigurationChanged and restart capture on a device change (reusing the existing RestartMicrophone coroutine, which re-adds the AudioSource/AudioProbe and re-registers the tap on the rebuilt graph).
  • Resolve the capture device to the OS default (null) when the preferred device is gone, so an unplugged headset transparently hands off to the built-in mic.
  • Track the active device separately from the preferred one so Microphone.IsRecording/GetPosition/End target the device actually recording.
  • Guard against overlapping restarts when the event fires repeatedly around one hardware swap.

The native source's rate is fixed at construction, so if the device change moves Unity's DSP output rate, frames are still dropped; this case is warned about clearly. Full rate-change recovery is a follow-up (stacked PR).

Out Of Scope

Restarts capture on a device change, but the native audio source's rate is fixed at construction. If the new device has a different sample rate, RtcAudioSource silently drops every captured frame (it rejects frames whose rate/channels don't match the source) — so audio stays dead. Full recovery possible with follow up: #312

var outputSampleRate = (uint)AudioSettings.outputSampleRate;
if (outputSampleRate != _expectedSampleRate)
{
Utils.Warning($"MicrophoneSource: audio device change moved the DSP output rate to {outputSampleRate}Hz, but the native source is fixed at {_expectedSampleRate}Hz. Captured frames will be dropped until the track is recreated at the new rate.");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Triggered on Unity 6 editor on mac starting with MDR-1000x, disabling Bluetooth to switch to built in mic, then connecting to MDR-1000x again:

LiveKit: MicrophoneSource: audio device change moved the DSP output rate to 44100Hz, but the native source is fixed at 48000Hz. Captured frames will be dropped until the track is recreated at the new rate.
UnityEngine.Logger:LogWarning (string,object)
LiveKit.Internal.Utils:Warning (object) (at /Users/maxheimbrock/dev/unity/client-sdk-unity/Runtime/Scripts/Internal/Utils.cs:41)
LiveKit.MicrophoneSource:OnAudioConfigurationChanged (bool) (at /Users/maxheimbrock/dev/unity/client-sdk-unity/Runtime/Scripts/MicrophoneSource.cs:248)
UnityEngine.AudioSettings:InvokeOnAudioConfigurationChanged (bool) (at /Users/bokken/build/output/unity/unity/Modules/Audio/Public/ScriptBindings/Audio.bindings.cs:413)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MaxHeimbrock MaxHeimbrock force-pushed the max/mic-samplerate-device-init branch from 266730d to 2a26265 Compare June 15, 2026 14:23
@MaxHeimbrock MaxHeimbrock force-pushed the max/mic-restart-on-device-change branch from 37725a6 to d19145d Compare June 15, 2026 14:26
Base automatically changed from max/mic-samplerate-device-init to main June 16, 2026 08:45
@MaxHeimbrock MaxHeimbrock force-pushed the max/mic-restart-on-device-change branch from d19145d to 442e2b3 Compare June 16, 2026 12:17
When a capture device disappears mid-call (e.g. unplugging a Bluetooth
headset), the local microphone went silent and never recovered:
MicrophoneSource stayed bound to the gone device name and never
re-registered the AudioProbe tap that Unity detaches when it rebuilds
its audio graph.

Subscribe to AudioSettings.OnAudioConfigurationChanged (mirroring the
playback-side AudioStream handler) and restart capture on a device
change, resolving to the OS default device when the preferred device is
no longer present. Track the active device separately from the preferred
one so Microphone.IsRecording/GetPosition/End target the right device,
and guard against overlapping restarts.

The native source's rate is fixed at construction, so if the device
change moves Unity's DSP output rate, frames are still dropped; warn
clearly in that case. Full rate-change recovery follows separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MaxHeimbrock MaxHeimbrock force-pushed the max/mic-restart-on-device-change branch from 442e2b3 to ace30fa Compare June 16, 2026 12:59
@MaxHeimbrock

Copy link
Copy Markdown
Contributor Author

New commit adds debug logs to investigate mobile platforms:

Done. The diagnostics are in MicrophoneSource.cs (uncommitted on max/mic-restart-on-device-change), compiling clean. Build and deploy that branch to
  Android, then capture logcat while reproducing.

  Capture logs

  adb logcat -c    # clear
  adb logcat | grep -iE "MicrophoneSource|LiveKit"

  All lines come through under the Unity tag prefixed with LiveKit:.

  Reproduce

  1. Start the call with the BT headset connected, confirm audio reaches the other side.
  2. Watch for the steady-state health line every ~2s: health framesLast2s=<non-zero> isRecording=True position=... muted=False.
  3. Switch Bluetooth off to disconnect the headset, and keep watching.

  What the logs will tell us

  The decisive lines after you switch BT off:

  ┌───────────────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────┐
  │                              Observation                              │                             Conclusion → fix                             │
  ├───────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
  │ No OnAudioConfigurationChanged line, and health framesLast2s drops to │ Event never fires on Android; capture stalls silently → frame-flow       │
  │  0                                                                    │ watchdog is the right fix                                                │
  ├───────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
  │ OnAudioConfigurationChanged … does appear, restart begin/started      │ Event fires and we restart, but the restart doesn't recover on Android → │
  │ successfully/restart end follow, but framesLast2s stays 0             │  deeper issue in StartMicrophone/Microphone.Start                        │
  ├───────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
  │ framesLast2s stays non-zero after BT off, yet the receiver hears      │ Buffers flow but audio is silent/dropped downstream → problem is in the  │
  │ nothing                                                               │ native source path, not capture                                          │
  └───────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────┘

  Paste the chunk of log from a few seconds before the BT toggle to ~10s after, and that picks the fix.

  Two housekeeping notes so nothing gets lost:
  - Your unrelated working-tree edits (scene/ProjectSettings/manifest/.vscode) are still stashed — I'll restore them once we wrap up.
  - #312 is stale (it diverged onto an older base and still has the pre-gate-fix restart commit). I'll rebuild it cleanly as "#311 + republish commit"
  after the Android fix lands, so we don't keep churning it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant