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
- Configure recording with
output.primary.enabled: true.
- Record for one or two seconds on iOS.
- Call
const recording = await stopRecording().
- Immediately open
recording.fileUri in a strict WAV parser or converter.
- 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.
- 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:
- Send primary PCM appends through one dedicated serial file-write queue.
- On stop, prevent new appends, stop/remove the audio tap, and drain that queue.
- On the same serialized context, derive the final byte count from the file, update both WAV size
fields, synchronize, and close the file handle.
- Build the result from the finalized file and only then return from
AudioStreamManager.stopRecording().
- 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.
Environment
@siteed/audio-studio:3.2.1await 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 itsRecordingResultfrom cached values. The finalRIFF/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:
RecordingResultis constructed from cached valuesstopRecording()then returns immediatelyupdateWavHeaderwrites RIFF and data sizes separatelyReproduction / validation probe
output.primary.enabled: true.const recording = await stopRecording().recording.fileUriin a strict WAV parser or converter.fileLength - 8;fileLength - 44;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 validatormessage as
Promise.swift:65, so we did not capture a header/file-size mismatch at the moment offailure. 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 fromstopRecording(). The promise can therefore resolvebefore those operations finish.
Proposed native fix
Make primary-WAV finalization part of the stop contract:
fields, synchronize, and close the file handle.
AudioStreamManager.stopRecording().riffSize == fileSize - 8,dataSize == fileSize - 44, and non-empty PCM.This also avoids concurrent access through the persistent append
FileHandleand the separateheader-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.