Skip to content

iOS: reused AVAudioEngine in RecorderBytesStreamEngine can silently stop delivering onAudioChunk after a stop → restart cycle #505

Description

@arun9005

On iOS, RecorderController.record() can return successfully and report RecorderState.recording, but the onAudioChunks stream (and therefore the live waveform / any byte-streaming consumer) never receives another event — intermittently, on the 2nd, 3rd, etc. recording started in the same app session (not the first).

Environment

  • audio_waveforms: 2.0.2
  • Flutter: 3.38.5 (stable)
  • Dart: 3.10.4
  • Platform: iOS 26.2, tested on iOS Simulator (iPhone 17 Pro), overrideAudioSession: true
  • Recorder settings: custom sample rate, AndroidEncoder/IosEncoder set for AAC

Steps to reproduce

  1. Call recorderController.record(path: ..., recorderSettings: ...), listen to recorderController.onAudioChunks, confirm chunks arrive and waveData grows.
  2. Call recorderController.stop() (or stop(false) + reset()).
  3. Call recorderController.record(...) again with a new file path.
  4. Repeat step 2–3 a few times.

Expected: every record() call that resolves without throwing, and reports RecorderState.recording, delivers onAudioChunk events (and therefore waveform amplitude data) just like the first call did.

Actual: intermittently (roughly 1 in 3 attempts in our testing), a restart reports RecorderState.recording with no error, but zero onAudioChunk events ever arrive for that session. The next restart after that often works fine again. This reproduces even though nothing else in the app touches AVAudioSession/AVAudioEngine directly — only audio_waveforms calls are involved.

Root cause (as far as we could tell)

In ios/Classes/RecorderBytesStreamEngine.swift, audioEngine is a single AVAudioEngine instance held for the entire app/plugin lifetime:

private var audioEngine = AVAudioEngine()

attach() reinstalls a tap on audioEngine.inputNode and calls audioEngine.start() again; detach() just calls removeTap/stop() on the same instance. Restarting the same AVAudioEngine instance in place after a full stop is a known-flaky pattern on iOS — engine.start() can return without throwing while the input-node tap silently never fires again. This matches our observed symptom exactly (success reported, zero data delivered).

Suggested fix

Create a brand-new AVAudioEngine() on every attach() call instead of reusing one across sessions, and fully release the old one in detach():

class RecorderBytesStreamEngine {
    private var audioEngine: AVAudioEngine?
    // ...

    func attach(result: @escaping FlutterResult, sampleRate: Int) {
        let engine = AVAudioEngine()
        audioEngine = engine
        paused = false
        totalFrames = 0

        let inputNode = engine.inputNode
        inputNode.installTap(onBus: 0, bufferSize: 1024, format: nil) { (buffer, time) in
            // ...unchanged...
        }
        do {
            try engine.start()
        } catch {
            result(FlutterError(code: Constants.audioWaveforms, message: "Error starting Audio Engine", details: error.localizedDescription))
        }
    }

    func detach() {
        totalFrames = 0
        audioEngine?.inputNode.removeTap(onBus: 0)
        audioEngine?.stop()
        audioEngine = nil
    }
    // ...
}

We patched a local fork with exactly this change and verified it resolves the issue: before the patch, roughly 1 in 3 stop→restart cycles failed to deliver any audio chunks; after the patch, 3+ consecutive cycles all worked correctly in our testing.

Happy to open a PR with this change if that's useful — let us know.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions