From c58a5a91f4721f47ceb91288365ecb3ea5acfda0 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Fri, 5 Jun 2026 19:02:58 +0900 Subject: [PATCH 1/3] docs: add project status section to README - Add alpha status badge - Document supported platforms and features - List known issues with links to tracker Closes #21 --- README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c1b2b2c..92a06f6 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,37 @@ > Use [Elementary Audio][elem] in your React Native app -This is alpha quality software. +[![Alpha](https://img.shields.io/badge/status-alpha-orange)](https://github.com/tamlyn/react-native-elementary/releases) [elem]: https://elementary.audio +## Project status + +Alpha. The API is subject to change. + +### Supported platforms + +- iOS +- Android + +### Supported features + +- Native Elementary Audio renderer via `useRenderer` hook +- Real-time `setProperty` for graph parameter updates +- iOS audio session configuration and management +- Configurable event polling (`el.snapshot`, `el.meter`, `el.scope`, `el.fft`) +- Audio resource loading via VFS + +### Known issues + +- Native node types (`el.metro`, `el.time`, `el.fft`, `el.convolve`) are not yet + supported (see [#4][i4]) +- Audio I/O may not update automatically when device connection changes + (see [#15][i15]) + +[i4]: https://github.com/tamlyn/react-native-elementary/issues/4 +[i15]: https://github.com/tamlyn/react-native-elementary/issues/15 + ## Installation ```sh From 46bfc196f963cabb12398c0336d6e9852543f3e9 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Fri, 5 Jun 2026 19:37:09 +0900 Subject: [PATCH 2/3] fix(ios): dispatch AVAudioEngine access to main thread 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. --- ios/Elementary.mm | 169 +++++++++++++++++++++++++--------------------- 1 file changed, 93 insertions(+), 76 deletions(-) diff --git a/ios/Elementary.mm b/ios/Elementary.mm index 78e4b5e..2b94f68 100644 --- a/ios/Elementary.mm +++ b/ios/Elementary.mm @@ -456,17 +456,21 @@ - (void)disableAudioSessionManagement RCT_EXPORT_METHOD(getAudioInfo:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - if (![self initializeAudioEngineIfNeeded]) { - reject(@"E_AUDIO_ENGINE", @"Failed to initialize audio engine", nil); - return; - } + // AVAudioEngine.outputNode must be accessed on the main thread to avoid + // an RPC timeout in AURemoteIO::Cleanup when called from a background queue. + dispatch_async(dispatch_get_main_queue(), ^{ + if (![self initializeAudioEngineIfNeeded]) { + reject(@"E_AUDIO_ENGINE", @"Failed to initialize audio engine", nil); + return; + } - AVAudioFormat *format = [self.audioEngine.outputNode outputFormatForBus:0]; - resolve(@{ - @"channels": @(format.channelCount), - @"sampleRate": @(format.sampleRate), - @"engineRunning": @(self.audioEngine.isRunning), - @"runtimeReady": @(self.runtime != nullptr), + AVAudioFormat *format = [self.audioEngine.outputNode outputFormatForBus:0]; + resolve(@{ + @"channels": @(format.channelCount), + @"sampleRate": @(format.sampleRate), + @"engineRunning": @(self.audioEngine.isRunning), + @"runtimeReady": @(self.runtime != nullptr), + }); }); } @@ -521,13 +525,17 @@ - (void)getSampleRate:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) #endif { - if (![self initializeAudioEngineIfNeeded]) { - reject(@"E_AUDIO_ENGINE", @"Failed to initialize audio engine", nil); - return; - } + // AVAudioEngine.outputNode must be accessed on the main thread to avoid + // an RPC timeout in AURemoteIO::Cleanup when called from a background queue. + dispatch_async(dispatch_get_main_queue(), ^{ + if (![self initializeAudioEngineIfNeeded]) { + reject(@"E_AUDIO_ENGINE", @"Failed to initialize audio engine", nil); + return; + } - NSNumber *sampleRate = @([self.audioEngine.outputNode outputFormatForBus:0].sampleRate); - resolve(sampleRate); + NSNumber *sampleRate = @([self.audioEngine.outputNode outputFormatForBus:0].sampleRate); + resolve(sampleRate); + }); } #ifdef RCT_NEW_ARCH_ENABLED @@ -542,63 +550,68 @@ - (void)loadAudioResource:(NSString *)key rejecter:(RCTPromiseRejectBlock)reject) #endif { - if (![self initializeAudioEngineIfNeeded]) { - reject(@"E_AUDIO_ENGINE", @"Failed to initialize audio engine", nil); - return; - } - - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - if (self.runtime == nullptr) { - reject(@"E_RUNTIME_NOT_INITIALIZED", @"Audio runtime not initialized", nil); + // AVAudioEngine.outputNode must be accessed on the main thread to avoid + // an RPC timeout in AURemoteIO::Cleanup when called from a background queue. + // Perform engine initialization on main thread before dispatching heavy work. + dispatch_async(dispatch_get_main_queue(), ^{ + if (![self initializeAudioEngineIfNeeded]) { + reject(@"E_AUDIO_ENGINE", @"Failed to initialize audio engine", nil); return; } - std::string keyStr = [key UTF8String]; - std::string filePathStr = [filePath UTF8String]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (self.runtime == nullptr) { + reject(@"E_RUNTIME_NOT_INITIALIZED", @"Audio runtime not initialized", nil); + return; + } - elementary::AudioLoadResult result = elementary::AudioResourceLoader::loadFile(keyStr, filePathStr); + std::string keyStr = [key UTF8String]; + std::string filePathStr = [filePath UTF8String]; - if (!result.success) { - reject(@"E_LOAD_FAILED", [NSString stringWithUTF8String:result.error.c_str()], nil); - return; - } + elementary::AudioLoadResult result = elementary::AudioResourceLoader::loadFile(keyStr, filePathStr); - size_t numChannels = result.info.channels; - size_t numSamples = result.info.sampleCount; - std::vector channelPtrs(numChannels); - for (size_t ch = 0; ch < numChannels; ++ch) { - channelPtrs[ch] = result.data.data() + (ch * numSamples); - } + if (!result.success) { + reject(@"E_LOAD_FAILED", [NSString stringWithUTF8String:result.error.c_str()], nil); + return; + } - auto resource = std::make_unique( - channelPtrs.data(), - numChannels, - numSamples - ); - bool added; - { - std::lock_guard lock(self->_runtimeMutex); - added = self.runtime->addSharedResource(keyStr, std::move(resource)); - } + size_t numChannels = result.info.channels; + size_t numSamples = result.info.sampleCount; + std::vector channelPtrs(numChannels); + for (size_t ch = 0; ch < numChannels; ++ch) { + channelPtrs[ch] = result.data.data() + (ch * numSamples); + } - if (!added) { - reject(@"E_KEY_EXISTS", [NSString stringWithFormat:@"Resource with key '%@' already exists", key], nil); - return; - } + auto resource = std::make_unique( + channelPtrs.data(), + numChannels, + numSamples + ); + bool added; + { + std::lock_guard lock(self->_runtimeMutex); + added = self.runtime->addSharedResource(keyStr, std::move(resource)); + } - @synchronized(self.loadedResources) { - [self.loadedResources addObject:key]; - } + if (!added) { + reject(@"E_KEY_EXISTS", [NSString stringWithFormat:@"Resource with key '%@' already exists", key], nil); + return; + } - NSDictionary *info = @{ - @"key": key, - @"channels": @(result.info.channels), - @"sampleCount": @(result.info.sampleCount), - @"sampleRate": @(result.info.sampleRate), - @"durationMs": @(result.info.durationMs) - }; + @synchronized(self.loadedResources) { + [self.loadedResources addObject:key]; + } - resolve(info); + NSDictionary *info = @{ + @"key": key, + @"channels": @(result.info.channels), + @"sampleCount": @(result.info.sampleCount), + @"sampleRate": @(result.info.sampleRate), + @"durationMs": @(result.info.durationMs) + }; + + resolve(info); + }); }); } @@ -612,24 +625,28 @@ - (void)unloadAudioResource:(NSString *)key rejecter:(RCTPromiseRejectBlock)reject) #endif { - if (![self initializeAudioEngineIfNeeded] || self.runtime == nullptr) { - reject(@"E_RUNTIME_NOT_INITIALIZED", @"Audio runtime not initialized", nil); - return; - } + // AVAudioEngine.outputNode must be accessed on the main thread to avoid + // an RPC timeout in AURemoteIO::Cleanup when called from a background queue. + dispatch_async(dispatch_get_main_queue(), ^{ + if (![self initializeAudioEngineIfNeeded] || self.runtime == nullptr) { + reject(@"E_RUNTIME_NOT_INITIALIZED", @"Audio runtime not initialized", nil); + return; + } - BOOL found = NO; - @synchronized(self.loadedResources) { - if ([self.loadedResources containsObject:key]) { - [self.loadedResources removeObject:key]; - found = YES; + BOOL found = NO; + @synchronized(self.loadedResources) { + if ([self.loadedResources containsObject:key]) { + [self.loadedResources removeObject:key]; + found = YES; + } } - } - if (found) { - self.runtime->pruneSharedResources(); - } + if (found) { + self.runtime->pruneSharedResources(); + } - resolve(@(found)); + resolve(@(found)); + }); } #ifdef RCT_NEW_ARCH_ENABLED From feeb312d4fc81352c3b76f1a949648d9fc89664f Mon Sep 17 00:00:00 2001 From: Ricardo Date: Fri, 5 Jun 2026 19:37:38 +0900 Subject: [PATCH 3/3] docs: update known issues to reflect partial #15 progress --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 92a06f6..5bf9c17 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ Alpha. The API is subject to change. - Native node types (`el.metro`, `el.time`, `el.fft`, `el.convolve`) are not yet supported (see [#4][i4]) -- Audio I/O may not update automatically when device connection changes +- Audio I/O may not update automatically when device connection changes; + engine restart on route change is handled, re-graph against new route is pending (see [#15][i15]) [i4]: https://github.com/tamlyn/react-native-elementary/issues/4