Skip to content
Open
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
1 change: 1 addition & 0 deletions android/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ set(CMAKE_CXX_STANDARD 17) # Updated to C++17
add_library(react-native-elementary SHARED
../cpp/react-native-elementary.cpp
../cpp/audioengine.cpp
../cpp/AudioResourceLoader.cpp
cpp-adapter.cpp
)

Expand Down
77 changes: 76 additions & 1 deletion android/cpp-adapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Java_com_elementary_ElementaryModule_nativeApplyInstructions(JNIEnv *env, jclass


env->ReleaseStringUTFChars(instructions, instrCStr);

auto jsonInstructions = elem::js::parseJSON(instrStr);

audioEngine->getRuntime().applyInstructions(jsonInstructions);
Expand All @@ -38,3 +38,78 @@ JNIEXPORT jint JNICALL
Java_com_elementary_ElementaryModule_nativeGetSampleRate(JNIEnv *env, jclass type) {
return audioEngine.get() ? audioEngine->getSampleRate() : 0;
}

extern "C"
JNIEXPORT jobject JNICALL
Java_com_elementary_ElementaryModule_nativeLoadAudioResource(JNIEnv *env, jclass type, jstring key, jstring filePath) {
if (!audioEngine) {
return nullptr;
}

const char *keyCStr = env->GetStringUTFChars(key, nullptr);
const char *filePathCStr = env->GetStringUTFChars(filePath, nullptr);

if (!keyCStr || !filePathCStr) {
if (keyCStr) env->ReleaseStringUTFChars(key, keyCStr);
if (filePathCStr) env->ReleaseStringUTFChars(filePath, filePathCStr);
return nullptr;
}

std::string keyStr(keyCStr);
std::string filePathStr(filePathCStr);

env->ReleaseStringUTFChars(key, keyCStr);
env->ReleaseStringUTFChars(filePath, filePathCStr);

// Load the audio resource
elementary::AudioLoadResult result = audioEngine->loadAudioResource(keyStr, filePathStr);

// Find the AudioResourceInfo class
jclass infoClass = env->FindClass("com/elementary/AudioResourceInfo");
if (!infoClass) {
return nullptr;
}

// Get the constructor
jmethodID constructor = env->GetMethodID(infoClass, "<init>", "(ZLjava/lang/String;Ljava/lang/String;IJID)V");
if (!constructor) {
return nullptr;
}

// Create the result object
jstring jKey = env->NewStringUTF(result.info.key.c_str());
jstring jError = env->NewStringUTF(result.error.c_str());

jobject infoObj = env->NewObject(
infoClass,
constructor,
static_cast<jboolean>(result.success),
jError,
jKey,
static_cast<jint>(result.info.channels),
static_cast<jlong>(result.info.sampleCount),
static_cast<jint>(result.info.sampleRate),
static_cast<jdouble>(result.info.durationMs)
);

return infoObj;
}

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_elementary_ElementaryModule_nativeUnloadAudioResource(JNIEnv *env, jclass type, jstring key) {
if (!audioEngine) {
return JNI_FALSE;
}

const char *keyCStr = env->GetStringUTFChars(key, nullptr);
if (!keyCStr) {
return JNI_FALSE;
}

std::string keyStr(keyCStr);
env->ReleaseStringUTFChars(key, keyCStr);

bool result = audioEngine->unloadAudioResource(keyStr);
return result ? JNI_TRUE : JNI_FALSE;
}
67 changes: 64 additions & 3 deletions android/src/main/java/com/elementary/ElementaryModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,23 @@ import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.modules.core.DeviceEventManagerModule

/**
* Data class for audio resource information returned from native code
*/
data class AudioResourceInfo(
val success: Boolean,
val error: String,
val key: String,
val channels: Int,
val sampleCount: Long,
val sampleRate: Int,
val durationMs: Double
)

class ElementaryModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {

Expand Down Expand Up @@ -34,6 +48,51 @@ class ElementaryModule(reactContext: ReactApplicationContext) :
// No-op
}

@ReactMethod
fun loadAudioResource(key: String, filePath: String, promise: Promise) {
Thread {
try {
val result = nativeLoadAudioResource(key, filePath)
if (result == null) {
promise.reject("E_NATIVE_ERROR", "Native audio engine not initialized")
return@Thread
}

if (!result.success) {
promise.reject("E_LOAD_FAILED", result.error)
return@Thread
}

val info = Arguments.createMap().apply {
putString("key", result.key)
putInt("channels", result.channels)
putDouble("sampleCount", result.sampleCount.toDouble())
putInt("sampleRate", result.sampleRate)
putDouble("durationMs", result.durationMs)
}
promise.resolve(info)
} catch (e: Exception) {
promise.reject("E_LOAD_FAILED", e.message, e)
}
}.start()
}

@ReactMethod
fun unloadAudioResource(key: String, promise: Promise) {
try {
val result = nativeUnloadAudioResource(key)
promise.resolve(result)
} catch (e: Exception) {
promise.reject("E_UNLOAD_FAILED", e.message, e)
}
}

@ReactMethod
fun getDocumentsDirectory(promise: Promise) {
val documentsDir = reactApplicationContext.filesDir.absolutePath
promise.resolve(documentsDir)
}

// Helper to emit events
private fun sendEvent(eventName: String, params: WritableMap?) {
reactApplicationContext
Expand All @@ -54,7 +113,9 @@ class ElementaryModule(reactContext: ReactApplicationContext) :
nativeStartAudioEngine();
}

external fun nativeGetSampleRate(): Int;
external fun nativeApplyInstructions(message: String);
external fun nativeStartAudioEngine();
external fun nativeGetSampleRate(): Int
external fun nativeApplyInstructions(message: String)
external fun nativeStartAudioEngine()
external fun nativeLoadAudioResource(key: String, filePath: String): AudioResourceInfo?
external fun nativeUnloadAudioResource(key: String): Boolean
}
17 changes: 16 additions & 1 deletion android/src/newarch/com/elementary/ElementaryTurboModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,19 @@ public void addListener(String eventName) {
public void removeListeners(double count) {
module.removeListeners(count);
}
}

@Override
public void loadAudioResource(String key, String filePath, Promise promise) {
module.loadAudioResource(key, filePath, promise);
}

@Override
public void unloadAudioResource(String key, Promise promise) {
module.unloadAudioResource(key, promise);
}

@Override
public void getDocumentsDirectory(Promise promise) {
module.getDocumentsDirectory(promise);
}
}
61 changes: 61 additions & 0 deletions cpp/AudioResourceLoader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#include "AudioResourceLoader.h"
#include "miniaudio.h"

namespace elementary {

AudioLoadResult AudioResourceLoader::loadFile(const std::string& key, const std::string& filePath) {
AudioLoadResult result;
result.success = false;
result.info.key = key;

ma_decoder decoder;
ma_decoder_config config = ma_decoder_config_init(ma_format_f32, 0, 0);

ma_result initResult = ma_decoder_init_file(filePath.c_str(), &config, &decoder);
if (initResult != MA_SUCCESS) {
result.error = "Failed to open audio file: " + filePath;
return result;
}

ma_uint64 totalFrames;
ma_result lengthResult = ma_decoder_get_length_in_pcm_frames(&decoder, &totalFrames);
if (lengthResult != MA_SUCCESS) {
result.error = "Failed to get audio file length";
ma_decoder_uninit(&decoder);
return result;
}

uint32_t channels = decoder.outputChannels;
uint32_t sampleRate = decoder.outputSampleRate;

std::vector<float> interleavedData(totalFrames * channels);
ma_uint64 framesRead;
ma_result readResult = ma_decoder_read_pcm_frames(&decoder, interleavedData.data(), totalFrames, &framesRead);

ma_decoder_uninit(&decoder);

if (readResult != MA_SUCCESS && readResult != MA_AT_END) {
result.error = "Failed to read audio data";
return result;
}

interleavedData.resize(framesRead * channels);

// Deinterleave into separate channel data
result.data.resize(framesRead * channels);
for (uint32_t ch = 0; ch < channels; ++ch) {
for (ma_uint64 frame = 0; frame < framesRead; ++frame) {
result.data[ch * framesRead + frame] = interleavedData[frame * channels + ch];
}
}

result.info.channels = channels;
result.info.sampleCount = static_cast<uint64_t>(framesRead);
result.info.sampleRate = sampleRate;
result.info.durationMs = (static_cast<double>(framesRead) / static_cast<double>(sampleRate)) * 1000.0;

result.success = true;
return result;
}

} // namespace elementary
51 changes: 51 additions & 0 deletions cpp/AudioResourceLoader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#ifndef AUDIORESOURCELOADER_H
#define AUDIORESOURCELOADER_H

#include <string>
#include <vector>
#include <memory>
#include <cstdint>

namespace elementary {

/**
* Metadata about a loaded audio resource
*/
struct AudioResourceInfo {
std::string key;
uint32_t channels;
uint64_t sampleCount; // samples per channel
uint32_t sampleRate;
double durationMs;
};

/**
* Result of loading an audio resource
*/
struct AudioLoadResult {
bool success;
std::string error;
AudioResourceInfo info;
std::vector<float> data; // Deinterleaved: all ch0 samples, then all ch1 samples, etc.
};

/**
* Audio Resource Loader using miniaudio
*
* Loads audio files (WAV, MP3, FLAC, etc.) and converts to deinterleaved float32 format
* suitable for Elementary Audio's Virtual File System.
*/
class AudioResourceLoader {
public:
/**
* Load an audio file from disk
* @param key - Unique identifier for this resource
* @param filePath - Absolute path to the audio file
* @return AudioLoadResult containing the audio data and metadata, or error info
*/
static AudioLoadResult loadFile(const std::string& key, const std::string& filePath);
};

} // namespace elementary

#endif // AUDIORESOURCELOADER_H
56 changes: 56 additions & 0 deletions cpp/audioengine.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "audioengine.h"
#include "vendor/elementary/runtime/elem/AudioBufferResource.h"
#define MINIAUDIO_IMPLEMENTATION
#include "miniaudio.h"

Expand All @@ -21,6 +22,61 @@ namespace elementary {
return device.sampleRate;
}

AudioLoadResult AudioEngine::loadAudioResource(const std::string& key, const std::string& filePath) {
AudioLoadResult result = AudioResourceLoader::loadFile(key, filePath);

if (!result.success) {
return result;
}

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);
}

auto resource = std::make_unique<elem::AudioBufferResource>(
channelPtrs.data(),
numChannels,
numSamples
);
bool added = proxy->runtime.addSharedResource(key, std::move(resource));

if (!added) {
result.success = false;
result.error = "Resource with key '" + key + "' already exists";
return result;
}

{
std::lock_guard<std::mutex> lock(resourceMutex);
loadedResources.insert(key);
}

return result;
}

bool AudioEngine::unloadAudioResource(const std::string& key) {
bool found = false;

// Only hold the lock while modifying loadedResources
{
std::lock_guard<std::mutex> lock(resourceMutex);
auto it = loadedResources.find(key);
if (it != loadedResources.end()) {
loadedResources.erase(it);
found = true;
}
}

if (found) {
proxy->runtime.pruneSharedResources();
}

return found;
}

void AudioEngine::initializeDevice() {
proxy = std::make_unique<DeviceProxy>(44100.0, 1024);

Expand Down
Loading