Skip to content

Data race in AudioProcessor: audioEnergy/audioSamples accessed from multiple threads without synchronization #442

@shawnzhu

Description

@shawnzhu

Summary

AudioProcessor has data races on audioEnergy and audioSamples — they are written from the audio tap callback thread and read from arbitrary threads (e.g., main thread) without any synchronization.

Details

In AudioProcessor.swift, processBuffer() is called from the audio engine tap's callback thread:

// ~line 907-917, called from audio tap callback thread
func processBuffer(_ buffer: [Float]) {
    audioSamples.append(contentsOf: buffer)          // write
    // ...
    self.audioEnergy.append(newEnergy)                // write
}

Meanwhile, relativeEnergy reads these from any thread:

// ~line 210, read from any thread
public var relativeEnergy: [Float] {
    return self.audioEnergy.map { $0.rel }
}

And audioSamples is also read externally:

public var audioSamples: ContiguousArray<Float> = []  // no synchronization

This is a Swift Concurrency data race — concurrent read/write on non-isolated, non-sendable mutable state.

Impact

  • When polling relativeEnergy from the main thread (e.g., for VAD-based end-of-speech detection), values can be stale, skipped, or inconsistent.
  • In our case, this caused AudioProcessor.isVoiceDetected() to not work reliably when called from a @MainActor-isolated context while recording.
  • Could potentially crash in optimized builds due to unsynchronized array mutations.

Reproduction

  1. Start recording with audioProcessor.startRecordingLive()
  2. Poll audioProcessor.relativeEnergy from the main thread in a timer
  3. Observe that values are inconsistent or empty

Suggested Fix

Either:

  • Make AudioProcessor an actor
  • Or protect audioEnergy / audioSamples with a lock (e.g., NSLock or os_unfair_lock)

Environment

  • WhisperKit 0.17.0
  • Xcode 26.3
  • iOS 26.3 / macOS 15

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions