Skip to content

iOS: stopRecording resolves before WAV finalization #424

Description

@jurgenj136

Environment

  • @siteed/audio-studio: 3.2.1
  • Platform: iOS
  • Output: primary WAV enabled
  • Consumer: immediately parses/converts the returned WAV after await stopRecording()

Problem

await stopRecording() can resolve with a WAV URI before the file is finalized.

In 3.2.1, primary PCM blocks are appended asynchronously on a global utility queue, while
AudioStreamManager.stopRecording() constructs its RecordingResult from cached values. The final
RIFF/data-size header update is then scheduled asynchronously on another global utility queue, and
the method returns immediately.

Because there is no completion barrier, an immediate consumer is not guaranteed to observe the
final header and file contents from those background operations.

Relevant 3.2.1 source:

Reproduction / validation probe

  1. Configure recording with output.primary.enabled: true.
  2. Record for one or two seconds on iOS.
  3. Call const recording = await stopRecording().
  4. Immediately open recording.fileUri in a strict WAV parser or converter.
  5. Validate that:
    • the RIFF size equals fileLength - 8;
    • for this fixed-layout WAV, the data size equals fileLength - 44;
    • the data chunk is non-empty.
  6. Repeat under device load or in a loop.

In our app, immediately starting a strict WAV-to-MP3 conversion after Stop intermittently failed
with a native AudioCodecValidationError. Expo's error transport obscured the underlying validator
message as Promise.swift:65, so we did not capture a header/file-size mismatch at the moment of
failure. The source ordering makes incomplete finalization a strong suspected cause, but an
instrumented loop or native regression test is still needed to confirm that causal link.

Expected

When stopRecording() resolves, the returned primary WAV is complete and internally consistent,
so consumers can safely parse, upload, or convert it immediately.

Actual

The implementation has no barrier that drains queued PCM writes and completes
updateWavHeader(...) before returning from stopRecording(). The promise can therefore resolve
before those operations finish.

Proposed native fix

Make primary-WAV finalization part of the stop contract:

  1. Send primary PCM appends through one dedicated serial file-write queue.
  2. On stop, prevent new appends, stop/remove the audio tap, and drain that queue.
  3. On the same serialized context, derive the final byte count from the file, update both WAV size
    fields, synchronize, and close the file handle.
  4. Build the result from the finalized file and only then return from
    AudioStreamManager.stopRecording().
  5. Add an iOS regression test that repeatedly records/stops and immediately verifies
    riffSize == fileSize - 8, dataSize == fileSize - 44, and non-empty PCM.

This also avoids concurrent access through the persistent append FileHandle and the separate
header-update FileHandle.

Downstream mitigation

Our app now waits before conversion for two consecutive bounded header/file-size snapshots that
are stable and internally consistent. On timeout it preserves the source WAV instead of converting
a partial file. This avoids a fixed delay, but remains a consumer-side mitigation;
stopRecording() should provide the readiness guarantee.

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