Conversation
…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>
tamlyn
left a comment
There was a problem hiding this comment.
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>
…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>
|
@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. |
- 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.
|
@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! |
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.