Skip to content

[BUG] iOS: #223 invalid-format fallback installs a synthetic-format tap → fatal uncatchable NSException ("Failed to create tap due to format mismatch") #417

Description

@pooyahrtn

Summary

The #223 invalid-hardware-format fallback in installTapWithHardwareFormat() installs a tap with a synthetic format on the live input bus, which throws an uncatchable Objective-C NSException ("Failed to create tap due to format mismatch, AVAudioFormat 16000 Hz Float32") and crashes the process. The guard correctly detects the bad state, then does the one thing guaranteed to crash in it.

This is still present on main (and @siteed/audio-studio@3.2.0):
packages/audio-studio/ios/AudioStreamManager.swift L944–959.

// Validate hardware format before installing tap (#223)
if inputHardwareFormat.channelCount == 0 || inputHardwareFormat.sampleRate == 0 {
    let fallbackSampleRate = Double(recordingSettings?.sampleRate ?? 16000)
    let fallbackChannels = AVAudioChannelCount(recordingSettings?.numberOfChannels ?? 1)
    guard let fallbackFormat = AVAudioFormat(standardFormatWithSampleRate: fallbackSampleRate, channels: fallbackChannels) else { ... }
    inputNode.installTap(onBus: 0, bufferSize: bufferSize, format: fallbackFormat, block: tapBlock) // ← throws when fallbackFormat ≠ live bus format
    return fallbackFormat
}

AVAudioNode.installTap requires the tap format to match the input bus's format. When the bus reports channelCount == 0 || sampleRate == 0, installing a tap with any concrete format (the synthetic 16 kHz fallback) mismatches the live bus and AVAudioEngineImpl::InstallTapOnNode raises an ObjC NSException. Swift try/catch cannot catch it → fatal/unhandled.

When it fires

The bus reads 0 Hz / 0 ch only transiently — the AVAudioSession isn't active yet. We hit it when prepareRecording() is called in the window right after a fresh requestRecordingPermissions grant, or during a contended route (Bluetooth handoff / incoming call / another app holding the mic via MixWithOthers). Rare per-user but fatal when it lands; it's a crash, not a degraded recording.

Environment

  • @siteed/audio-studio@3.0.2 (confirmed identical on main / 3.2.0)
  • iOS, config: sampleRate: 16000, channels: 1, PlayAndRecord + DefaultToSpeaker + AllowBluetooth + MixWithOthers
  • Sentry pins the culprit symbol to checkMicrophonePermission (nearest symbol on the prepare/permission thread); the real frame is the tap install.

Related

Same area as #192 / #218 / #223 — the #223 fix (this fallback) traded the original IsFormatSampleRateAndChannelCountValid crash for a different fatal in the same spot.

Suggested fix

When the hardware format is invalid, don't install a tap at all — skip it and return. The engine stays valid without an input tap (it just delivers no buffers), so the caller's recovery/retry runs once a real route exists, instead of the process dying:

if inputHardwareFormat.channelCount == 0 || inputHardwareFormat.sampleRate == 0 {
    Logger.debug("AudioStreamManager", "Invalid hardware format (\(inputHardwareFormat.channelCount)ch/\(inputHardwareFormat.sampleRate)Hz); skipping tap to avoid fatal format mismatch.")
    if prepareEngine { audioEngine.prepare() }
    return inputHardwareFormat
}

App-side, activating the session (setAudioMode allowsRecording: true) before prepareRecording avoids the trigger for the common case, but the library installing a mismatched tap on an invalid bus is still a latent fatal for any contended-route path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions