Skip to content

[BUG] Thread deadlock when system mic changes #624

Description

@dain

Describe the bug

I have a macbook with a Shure microphone connected via USBC, which I use exclusively for dictation. When this mic is selected and I put on my AirpodMax 2 headphones, the mac switches both input and output the the AirpodMax automatically. Something like 50% of the time, the causes Fluid Voice to lockup and I have to force quit the application and restart it. Since this can happen any time I take my headphone of it is pretty frustrating (in an otherwise amazing application).

Reproduction steps

  1. open FluidVoice
  2. select the share microphone
  3. take AirPod Max off
  4. wait a second
  5. put them back on.

Expected behavior

Ideally, just ignore the system change a say with my shure mic (there is a separate issue for this), but at the very least, don't lock up.

Actual behavior

Fluid void freezes

App Version

1.6.4 build 15

macOS Version

macOS 26.5.2 build 25F84

Architecture

Apple Silicon

Logs or crash report

I had codex analyze the log and pull ou the relevant sections.

## 1. Strongest apparent hang: blocked while instantiating the input node


[12:30:53.379] [DEBUG] [ASRService] Audio engine retired (idle_route_change:default input changed)
[12:30:53.410] [DEBUG] [ASRService] 🔧 configureSession() - ENTERED
[12:30:53.410] [DEBUG] [ASRService] ℹ️ Creating audio engine lazily
[12:30:53.410] [DEBUG] [ASRService] 📍 Forcing input node instantiation...

... no "Input node instantiated" or "configureSession() - COMPLETED" entry ...

[RUN] PID=<REDACTED_PROCESS_ID> AppVersion=1.6.4 Build=15 OS=Version 26.5.2 (Build 25F84)
[12:31:17.077] [INFO] [AppServices] 📦 AppServices singleton created (services not yet initialized)


Light analysis: the last operation logged by the old process is input-node instantiation during handling of a default-input change. The next event is a fresh Fluid process about 24 seconds later. This is consistent with the application blocking inside or immediately around `AVAudioEngine.inputNode` until it is force-quit and relaunched.

For comparison, the same initialization path completes normally after relaunch:


[12:31:18.896] [DEBUG] [ASRService] 🔧 configureSession() - ENTERED
[12:31:18.897] [DEBUG] [ASRService] ℹ️ Creating audio engine lazily
[12:31:18.897] [DEBUG] [ASRService] 📍 Forcing input node instantiation...
[12:31:19.014] [DEBUG] [ASRService] Input node instantiated
[12:31:19.014] [DEBUG] [ASRService] 📍 Forcing output node instantiation...
[12:31:19.030] [DEBUG] [ASRService] ✅ Output node instantiated
[12:31:19.030] [DEBUG] [ASRService] ✅ configureSession() - COMPLETED
[12:31:19.030] [INFO] [ASRBenchmark] ASR_BENCH session=0 audio_engine_prewarm reason=startup elapsedMs=134


## 2. Another apparent hang following an output-route change


[12:28:59.163] [DEBUG] [ASRService] Audio engine retired (idle_route_change:default output changed)

... no further entries from that process ...

[RUN] PID=<REDACTED_PROCESS_ID> AppVersion=1.6.4 Build=15 OS=Version 26.5.2 (Build 25F84)
[12:30:45.819] [INFO] [AppServices] 📦 AppServices singleton created (services not yet initialized)


Light analysis: after Fluid retires the engine in response to a default-output change, logging stops. A new process starts about 1 minute 46 seconds later. This looks like a second hang in the idle route-change path, although this excerpt alone cannot identify the exact blocking call.

## 3. AirPods arrival causes a burst of duplicate disconnect callbacks


[11:34:14.685] [DEBUG] [ASRService] Audio engine retired (idle_route_change:default output changed)
[11:34:14.806] [DEBUG] [ASRService] ✅ configureSession() - COMPLETED
[11:34:14.806] [INFO]  [ASRService] ⚠️ Device availability changed for ID: <REDACTED_DEVICE_ID>
[11:34:14.806] [WARN]  [ASRService] ❌ Monitored device (ID: <REDACTED_DEVICE_ID>) DISCONNECTED
[11:34:14.806] [DEBUG] [ASRService] Current input devices: AirPods Max 2, <OTHER_INPUT_DEVICES_REDACTED>, Shure MV7+
[11:34:14.808] [INFO]  [ASRService] 🎧 New Bluetooth device detected: 'AirPods Max 2'. Auto-switching...
[11:34:14.808] [INFO]  [ASRService] Not recording - Bluetooth device will be used on next recording

... the same monitored-device DISCONNECTED callback occurs 7 more times ...
... all 8 callbacks occur between 11:34:14.806 and 11:34:14.867 ...

[11:34:15.335] [DEBUG] [ASRService] Audio engine retired (idle_route_change:default input changed)
[11:34:15.466] [DEBUG] [ASRService] ✅ configureSession() - COMPLETED
[11:34:15.466] [INFO]  [ASRBenchmark] ASR_BENCH session=8 audio_engine_prewarm reason=idle_route_change elapsedMs=104


Light analysis: a single Bluetooth/device transition produces eight disconnect callbacks within roughly 61 ms. This may indicate duplicate observer registration, reentrant notification handling, or a need to coalesce route-change notifications. Repeated callbacks could cause overlapping engine retirement/recreation work and increase the chance of a deadlock or blocked Core Audio call.

## 4. Route change while recording: 48 kHz to 24 kHz


[12:20:56.747] [DEBUG] [ASRService] Engine IO device = 48000.0Hz, Input format = 48000.0Hz 1ch
[12:20:56.850] [INFO]  [ASRService] AVAudioEngine started successfully on attempt 1
[12:20:56.851] [INFO]  [ASRService] ✅ Audio capture running
[12:20:57.263] [WARN]  [ASRService] Audio route changed while recording; scheduling recovery (engine configuration changed)
[12:20:58.265] [INFO]  [ASRService] Recovering audio route after engine configuration changed
[12:20:58.266] [DEBUG] [ASRService] Audio engine retired (audio_route_recovery)
[12:20:58.266] [DEBUG] [ASRService] 🔧 configureSession() - ENTERED
[12:20:58.362] [DEBUG] [ASRService] ✅ configureSession() - COMPLETED
[12:20:58.362] [INFO]  [ASRService] Sync mode enabled - using system default input device
[12:20:58.374] [DEBUG] [ASRService] Engine IO device = 24000.0Hz, Input format = 24000.0Hz 1ch
[12:20:58.391] [INFO]  [ASRService] AVAudioEngine started successfully on attempt 1
[12:20:58.393] [INFO]  [ASRService] Audio route recovery succeeded
[12:20:58.393] [WARN]  [ASRService] Audio buffer not growing after 6 seconds (count: 0). Audio capture may have failed. Check if engine is running and tap is installed.
[12:20:58.575] [INFO]  [ASRBenchmark] ASR_BENCH session=5 first_audio sampleCount=2731 frameLength=4096 sampleRate=24000 bufferMs=171 acquisitionMs=126 elapsedMs=309


Light analysis: this instance recovered, but it demonstrates the relevant transition. The active engine changes from 48 kHz to 24 kHz during recording, is retired and recreated, temporarily has a zero-length capture buffer, and then receives audio. The 24 kHz route is consistent with the AirPods microphone route seen elsewhere in the log.

## 5. Explicit Shure to AirPods transition followed by an immediate Shure rebind


[17:36:06.450] [DEBUG] [ASRService] Audio engine retired (idle_route_change:default output changed)
[17:36:06.483] [INFO]  [ASRService] Prepared direct Core Audio input 'Shure MV7+' (48000Hz, 512 frames, reason=idle_route_change)
[17:36:10.929] [DEBUG] [ASRService] Audio engine retired (idle_route_change:default input changed)
[17:36:10.948] [INFO]  [ASRService] Prepared direct Core Audio input 'AirPods Max 2' (24000Hz, 480 frames, reason=idle_route_change)
[17:36:10.974] [INFO]  [ASRService] Preferred device 'Shure MV7+' reconnected. Auto-switching...
[17:36:10.974] [INFO]  [ASRService] Not recording - updating binding for next session
[17:36:11.061] [INFO]  [ASRService] Bound ASR input to 'Shure MV7+' (uid: <REDACTED_DEVICE_UID>, id: <REDACTED_DEVICE_ID>)


A later occurrence generates the same Shure reconnection and binding sequence four times within approximately 3 ms:


[18:19:21.484] [DEBUG] [ASRService] Audio engine retired (idle_route_change:default input changed)
[18:19:21.503] [INFO]  [ASRService] Prepared direct Core Audio input 'AirPods Max 2' (48000Hz, 480 frames, reason=idle_route_change)
[18:19:21.552] [INFO]  [ASRService] Preferred device 'Shure MV7+' reconnected. Auto-switching...
[18:19:21.637] [INFO]  [ASRService] Bound ASR input to 'Shure MV7+' (uid: <REDACTED_DEVICE_UID>, id: <REDACTED_DEVICE_ID>)
[18:19:21.652] [INFO]  [ASRService] Preferred device 'Shure MV7+' reconnected. Auto-switching...
[18:19:21.652] [INFO]  [ASRService] Bound ASR input to 'Shure MV7+' (uid: <REDACTED_DEVICE_UID>, id: <REDACTED_DEVICE_ID>)
[18:19:21.653] [INFO]  [ASRService] Preferred device 'Shure MV7+' reconnected. Auto-switching...
[18:19:21.653] [INFO]  [ASRService] Bound ASR input to 'Shure MV7+' (uid: <REDACTED_DEVICE_UID>, id: <REDACTED_DEVICE_ID>)
[18:19:21.655] [INFO]  [ASRService] Preferred device 'Shure MV7+' reconnected. Auto-switching...
[18:19:21.655] [INFO]  [ASRService] Bound ASR input to 'Shure MV7+' (uid: <REDACTED_DEVICE_UID>, id: <REDACTED_DEVICE_ID>)


Light analysis: Fluid is not merely observing the system route change; its preferred-device behavior immediately attempts to switch the input back to the Shure. The duplicated rebind sequence suggests multiple callbacks can act on the same device event.

## Overall interpretation

The logs appear to narrow the persistent hang to the audio-engine recreation path triggered by default-device changes. The strongest case stops immediately after `Forcing input node instantiation...`, whereas a healthy run logs `Input node instantiated` about 117 ms later.

Possible areas to investigate:

- Calling `AVAudioEngine.inputNode` or creating/configuring `AVAudioEngine` synchronously while Core Audio is still rebuilding a Bluetooth/USB route.
- Multiple device observers or reentrant callbacks issuing duplicate disconnect/reconnect handling.
- Overlapping default-input, default-output, engine-configuration, and preferred-device auto-switch events.
- Serializing and coalescing route changes before retiring or recreating the engine.
- Ensuring route-change work does not block the main/UI thread.

Screenshots or screen recording

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions