Skip to content

Audio Engine Improvements#16

Merged
txbrown merged 12 commits intomainfrom
feature/ios-production-hardening
May 5, 2026
Merged

Audio Engine Improvements#16
txbrown merged 12 commits intomainfrom
feature/ios-production-hardening

Conversation

@txbrown
Copy link
Copy Markdown
Collaborator

@txbrown txbrown commented Mar 17, 2026

While integrating react-native-elementary with Midicircuit (which is/was fully native) I found a couple missing elements for assets loading, detecting hardware changes and midi trigger of notes. So I am adding those in here.

txbrown and others added 3 commits March 17, 2026 13:13
…fety

- Add AVAudioSessionInterruptionNotification handler to restart engine
  after phone calls, Siri, or background interruptions
- Add AVAudioEngineConfigurationChangeNotification handler to restart
  engine after headphone plug/unplug or route changes
- Use MIN(numOutputChannels, actualChannels) in render callback to
  handle dynamic channel count changes safely (was using UInt8 loop)
- Log init diagnostics (channel count, sample rate)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allows updating node properties directly on the audio thread without
re-rendering the entire graph. Essential for MIDI triggering, parameter
automation, and any time-critical updates.

- Native: builds SET_PROPERTY instruction batch (opcode 3)
- TurboModule spec: setProperty(nodeHash, key, value)
- JS export: setProperty() with JSDoc

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- getBundlePath(): returns app bundle resource path for loading bundled
  audio assets (pairs with existing getDocumentsDirectory)
- getAudioInfo(): diagnostic method returning channels, sample rate,
  engine running status, and runtime readiness
- sharedInstance: class method for native code to access the Elementary
  runtime outside the RN bridge (e.g. for real-time MIDI triggering)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Owner

@tamlyn tamlyn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, but the Android build is failing 🤔

@txbrown
Copy link
Copy Markdown
Collaborator Author

txbrown commented Mar 17, 2026

Looks good, but the Android build is failing 🤔

thanks. could you add harness for blocking merges without all green please? I am fixing this in the meantime.

Implement the two missing abstract methods from NativeElementarySpec
that were added to the JS spec and iOS but not wired up on Android,
fixing the new-arch build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@txbrown txbrown changed the title iOS Audio Engine Improvements Audio Engine Improvements Mar 17, 2026
…fety on Android

- Add AudioManager.OnAudioFocusChangeListener to stop/restart device
  after phone calls, other media apps, or transient focus loss
- Add BroadcastReceiver for ACTION_AUDIO_BECOMING_NOISY to handle
  headphone disconnect (equivalent to iOS AVAudioEngineConfigurationChangeNotification)
- Clamp channel count in DeviceProxy::process() to prevent out-of-bounds
  if device reports more than 2 channels (matches iOS MIN safety)
- Add getAudioInfo() diagnostic method returning channels, sampleRate,
  engineRunning, and runtimeReady
- Add C++ device lifecycle control: stopDevice/startDevice/isDeviceRunning
- Log init diagnostics (channel count, sample rate)
- Clean up audio focus and receiver on host destroy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@txbrown
Copy link
Copy Markdown
Collaborator Author

txbrown commented Mar 17, 2026

@tamlyn I still want to test this PR before we merge. Will try to run some tests with my android device.

I already made several improvements on Android by actually testing on real device in various scenarios. But I also found an issue with audio sequencing drift over time. I think I have fix for it.

@txbrown txbrown marked this pull request as draft March 17, 2026 21:27
txbrown and others added 6 commits March 17, 2026 21:33
- Add atomic mute flag to silence audio callback instantly on focus
  loss, eliminating audible glitch during stop
- Reinitialize miniaudio device if ma_device_start fails after
  extended stop (device can go stale after prolonged background)
- Re-request audio focus and restart device on host resume after
  permanent focus loss (AUDIOFOCUS_LOSS doesn't send GAIN callback)
- Add diagnostic logging for device start/running state

Tested on device: YouTube focus steal, headphone plug/unplug,
extended background periods — all recover cleanly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ndroid)

Added 30Hz polling timer that calls runtime->processQueuedEvents()
and forwards events (el.snapshot, el.meter, el.scope, el.fft) to JS
as "elementaryEvent".

iOS: dispatch_source_t timer on main thread, RCTEventEmitter
Android: Handler timer on main thread, RCTDeviceEventEmitter
Both: listener tracking to skip polling when no JS listeners

This enables audio-thread data (beat position, levels, spectrum) to
reach JS for UI updates. Previously these events were queued but
never consumed on either platform.

iOS:
- Elementary.h: eventPollTimer property
- Elementary.mm: startEventPolling, stopEventPolling (dealloc),
  _hasEventListeners gating, elementaryEvent in supportedEvents

Android:
- ElementaryModule.kt: startEventPolling, stopEventPolling
  (onHostDestroy), hasEventListeners gating, JSON parsing,
  nativeProcessQueuedEvents external declaration
- cpp-adapter.cpp: nativeProcessQueuedEvents JNI method
  returns JSON array of queued events
The Elementary runtime's applyInstructions (graph mutation) was called
from the JS/RN thread while process() ran on the audio thread with no
synchronization. This caused heap corruption (nanov2_guard_corruption_detected)
during appendChild when allocating InletConnection vectors.

Fix: Add std::mutex to guard runtime access on both iOS and Android.
- Audio callback uses try_lock — outputs silence on contention (no blocking)
- applyInstructions/setProperty/addSharedResource hold the lock briefly
- Worst case: one silent audio block (~11ms) during graph rebuild

Also:
- Fix addListener/removeListeners to call [super ...] (suppresses
  'no listeners registered' warnings from RCTEventEmitter parent)
- Move _listenerCount/_hasEventListeners to proper ivars in header
addSharedResource and pruneSharedResources modify the SharedResourceMap
(std::unordered_map) without holding runtimeMutex, while applyInstructions
reads from it on the JS thread with the mutex held. This is undefined
behavior when loadAudioResource runs on a background thread (Kotlin
Thread{}). iOS already locks _runtimeMutex in Elementary.mm:308 — this
brings Android in line.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When AVAudioEngineConfigurationChangeNotification fires (headphone
plug/unplug, Bluetooth connect, etc.), the audio subsystem is still
mid-reconfiguration. Calling startAndReturnError: synchronously inside
the notification handler causes AudioUnitInitialize to RPC the audio
server, which deadlocks because the server is processing the same
change. The RPC times out → _ReportRPCTimeout → abort() → SIGABRT.

Fix: stop the engine immediately (it's inconsistent), then defer
restart by 200ms via dispatch_after so the OS finishes releasing
locks and settling the new audio route before reinitialization.
@txbrown txbrown requested a review from Copilot May 5, 2026 16:50
@txbrown txbrown marked this pull request as ready for review May 5, 2026 16:50
@txbrown
Copy link
Copy Markdown
Collaborator Author

txbrown commented May 5, 2026

@tamlyn been battle testing this in Midicircuit with a few beta versions out now and another app for practising bass guitar (unreleased). I think it's good so merging. Feel free to rollback if not happy. Thank you!

@txbrown txbrown merged commit 8054118 into main May 5, 2026
5 checks passed
@txbrown txbrown review requested due to automatic review settings May 5, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants