fix(ios): dispatch AVAudioEngine access to main thread#27
Open
txbrown wants to merge 7 commits into
Open
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces an explicit audio engine lifecycle API (start/stop/pause/resume) across the JS surface and native iOS/Android implementations, aiming to let callers release or pause audio resources more deliberately while preserving backward compatibility.
Changes:
- Added JS/TS TurboModule spec + JS wrappers for
startEngine,stopEngine,pauseEngine,resumeEngine. - Implemented engine lifecycle methods in iOS (
Elementary.mm/.h) and Android (ElementaryModule.kt) and wired new-arch Android routing (ElementaryTurboModule.java). - Bumped package version to
0.4.1-beta.2and updated the changelog.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/NativeElementary.ts | Extends TurboModule spec with engine lifecycle methods. |
| src/index.tsx | Exposes JS wrapper functions for engine lifecycle methods. |
| ios/Elementary.h | Introduces an engine state enum/property for lifecycle tracking. |
| ios/Elementary.mm | Adds lifecycle methods and updates engine state on config change. |
| android/src/main/java/com/elementary/ElementaryModule.kt | Adds lifecycle methods and changes host resume/pause behavior. |
| android/src/newarch/com/elementary/ElementaryTurboModule.java | Adds new-arch forwarding stubs for lifecycle methods. |
| CHANGELOG.md | Adds 0.4.1-beta.2 entry (currently missing lifecycle mention). |
| package.json | Version bump to 0.4.1-beta.2. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
47e0736 to
5f92c9a
Compare
txbrown
added a commit
that referenced
this pull request
Jun 2, 2026
- iOS: set engineState on audio interruption begin (Interrupted) and end (Running/Idle) so the state machine accurately reflects engine status - Android: replace phantom acquireAudioEngine() references with startEngine() - Android: remove reference to non-existent ElementaryAudioEngine in onHostPause comment - Android TurboModule: correct misleading routing comment - NativeElementary.ts: clarify that auto-start is iOS-only; Android requires explicit startEngine() - CHANGELOG: add engine lifecycle API entry to 0.4.1-beta.2
AVAudioEngine.outputNode must be accessed on the main thread to avoid an RPC timeout in AURemoteIO::Cleanup when called from a background queue. Wrap getAudioInfo, getSampleRate, loadAudioResource, and unloadAudioResource in dispatch_async(dispatch_get_main_queue()). applyInstructions and setProperty are left as-is since they are performance-critical and only hit the AV engine on first call (subsequent calls take the audioEngineInitialized fast path). Patch insight from midicircuit-rn.
64867ff to
56feb87
Compare
pruneSharedResources() in unloadAudioResource was called without the _runtimeMutex, while addSharedResource() in loadAudioResource acquires it. This is a data race — pruneSharedResources may mutate internal state that the audio render callback reads concurrently under the same mutex.
If the engine was never initialized, there are no resources to unload. Resolve NO immediately without touching AVAudioEngine, skipping the main-thread dispatch entirely. Previously this would reject with E_RUNTIME_NOT_INITIALIZED, which is semantically wrong — the caller asked to remove a resource that doesn't exist, not to start the engine.
Both methods now have a dual-path pattern: - Fast path: engine already initialized → process synchronously (zero overhead) - Cold path: engine not yet initialized → dispatch_async(main) for safe AVAudioEngine access, avoiding AURemoteIO::Cleanup RPC timeout Added NSAssert in initializeAudioEngineIfNeeded to catch any future caller that reaches the cold path from a background thread.
…ing docs
- Extract repeated dispatch_async(main) + engine-init-guard pattern into
dispatchMainWithEngineInit:reject: helper method
- Add file-level dispatch policy documentation covering:
- Main-thread requirement for AVAudioEngine
- dispatch_async vs dispatch_sync vs dispatch_after rationale
- Self-capture policy (strong for one-shot blocks, weak/strongSelf for
long-lived blocks)
- Add NSAssert in initializeAudioEngineIfNeeded to catch off-main-thread
callers during development builds
- Refactor getAudioInfo, getSampleRate, loadAudioResource to use helper
17 tests covering all exported functions: getSampleRate, loadAudioResource, unloadAudioResource, setProperty, event polling, audio session controls, and useRenderer. Key regression: unload resolves false without engine init (never rejects). TDD foundation for the engine-lifecycle hardening work.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Prevents AURemoteIO RPC timeouts when AVAudioEngine is accessed off the main thread in iOS.
Changes
getAudioInfo,getSampleRate,loadAudioResource, andunloadAudioResourcenow dispatch engine access to the main queue_runtimeMutexguard matchingaddSharedResources— fixes data race with audio render callbackfalseinstead of rejecting when engine was never starteddispatchMainWithEngineInit:reject:Manual testing
unloadAudioResourcebefore any audio playback — should resolvefalse, not rejectloadAudioResourcecalls during cold start — no RPC timeout in logs