Apple mic uplink: the microphone was never in the audio graph, so nothing ever pulled it #94

Merged
enricobuehler merged 2 commits from fix/apple-mic-uplink into main 2026-08-07 14:33:49 +00:00
2 Commits
Author SHA1 Message Date
enricobuehler d2d5058d0b fix(client/apple): the mic was never in the graph, so nothing ever pulled it
ci / web (pull_request) Successful in 1m1s
ci / docs-site (pull_request) Successful in 1m15s
ci / bun-nix (pull_request) Successful in 20s
apple / swift (pull_request) Successful in 1m30s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m45s
ci / rust (pull_request) Successful in 5m43s
ROOT CAUSE, from the reporter's device log:

  16:25:49.093  mic capture: 48000 Hz, 1 ch          <- tap installed, format fine
  16:25:49.235  audio engines joined - voice processing active
  ... 13 s of session, no errors, and the 10 s silence verdict NEVER fires

The engine started clean and the tap was installed against a valid
format - so neither the format timing nor the encoder was the fault. The
tripwire fires after ten seconds of CAPTURED frames and never fired
across a 13-second session: the tap received nothing at all.

Because the capture side must be pulled, and only the render graph pulls
anything. On the combined engine the input node carried a tap and no
connection, so it was not in the graph and nobody drove it: the IO unit
came up (the recording indicator lit for a beat, then went out as the
input went idle) and not one buffer ever reached the tap. No error, no
failed start - a session that quietly sent no microphone.

The input now runs through a silent sink into the main mixer, which is
what Apple's own voice-processing sample does. outputVolume = 0 because
the mic must reach the graph and never the speaker. The split path never
needed this - a capture-only engine has the input node AS its graph - so
this broke exactly when the combined topology became the default.

Verified: swift build (macOS), swift build --triple arm64-apple-ios17.0,
swift test 208 passed. Awaiting the reporter's on-device confirmation.
2026-08-07 16:30:29 +02:00
enricobuehler 7e34ca3d5e fix(client/apple): a session that can't voice-process still sends its microphone
ci / bun-nix (pull_request) Successful in 25s
ci / docs-site (pull_request) Successful in 1m7s
ci / web (pull_request) Successful in 1m8s
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m29s
ci / rust (pull_request) Successful in 5m44s
Field report: mic uplink dead on iOS, iPadOS and macOS alike, while
Android on the same host works - so the host and the wire are fine.

Two defects in the combined (voice-processing) engine, which became the
default on all three Apple platforms a week ago and has never run on a
device - CI only runs swift test on macOS, and the loopback test counts
datagrams without decoding them.

- The tap read the input format before the engine was prepared. Enabling
  voice processing swaps the engine's IO unit for the VPIO one and
  renegotiates its formats; until prepare() the input node can still
  report the pre-swap state, 0 Hz / 0 channels included, which
  installMicTap correctly refuses as 'no usable input device'. Both
  topologies now prepare first, so the chain is built against what the
  voice processor actually emits.

- A mic chain that failed on the voice-processed engine took the whole
  uplink down for the session: that arm fell back to playback ONLY. The
  sibling failure a few lines above - the voice processor refusing to
  engage at all - already falls back to the split path, which is a
  working mic without echo cancellation. Both arms do that now. The mic
  outranks the AEC.

Not reproduced locally (no Punktfunk entries in this Mac's log store,
and collecting the device's log needs root), so this is a strong
inference plus one proven logic defect rather than a confirmed fix. If
it persists, Console filtered to subsystem io.unom.punktfunk / category
audio names the stage: 'mic capture: N Hz' then 'audio engines joined'
then, 10 s in, either 'mic uplink OK - peak ...' or the SILENCE warning.

Follow-up worth doing separately: nothing reports whether the uplink
actually opened, so the HUD offers a Mute Microphone button over a
session sending nothing. Android gates that on a real micRunning signal.
2026-08-07 16:09:33 +02:00