Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,38 @@

> 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;
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
[i15]: https://github.com/tamlyn/react-native-elementary/issues/15

## Installation

```sh
Expand Down
169 changes: 93 additions & 76 deletions ios/Elementary.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
});
}

Expand Down Expand Up @@ -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
Expand All @@ -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<float*> 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<elem::AudioBufferResource>(
channelPtrs.data(),
numChannels,
numSamples
);
bool added;
{
std::lock_guard<std::mutex> 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<float*> 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<elem::AudioBufferResource>(
channelPtrs.data(),
numChannels,
numSamples
);
bool added;
{
std::lock_guard<std::mutex> 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);
});
});
}

Expand All @@ -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
Expand Down
Loading