Skip to content

Commit d3ae11a

Browse files
authored
Merge pull request #14 from tamlyn/feature/vfs-audio-resources-v4
feat: add VFS audio resource loading and upgrade to Elementary v4
2 parents 9305335 + 2610676 commit d3ae11a

File tree

28 files changed

+880
-124
lines changed

28 files changed

+880
-124
lines changed

android/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ set(CMAKE_CXX_STANDARD 17) # Updated to C++17
77
add_library(react-native-elementary SHARED
88
../cpp/react-native-elementary.cpp
99
../cpp/audioengine.cpp
10+
../cpp/AudioResourceLoader.cpp
1011
cpp-adapter.cpp
1112
)
1213

android/cpp-adapter.cpp

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Java_com_elementary_ElementaryModule_nativeApplyInstructions(JNIEnv *env, jclass
2626

2727

2828
env->ReleaseStringUTFChars(instructions, instrCStr);
29-
29+
3030
auto jsonInstructions = elem::js::parseJSON(instrStr);
3131

3232
audioEngine->getRuntime().applyInstructions(jsonInstructions);
@@ -38,3 +38,78 @@ JNIEXPORT jint JNICALL
3838
Java_com_elementary_ElementaryModule_nativeGetSampleRate(JNIEnv *env, jclass type) {
3939
return audioEngine.get() ? audioEngine->getSampleRate() : 0;
4040
}
41+
42+
extern "C"
43+
JNIEXPORT jobject JNICALL
44+
Java_com_elementary_ElementaryModule_nativeLoadAudioResource(JNIEnv *env, jclass type, jstring key, jstring filePath) {
45+
if (!audioEngine) {
46+
return nullptr;
47+
}
48+
49+
const char *keyCStr = env->GetStringUTFChars(key, nullptr);
50+
const char *filePathCStr = env->GetStringUTFChars(filePath, nullptr);
51+
52+
if (!keyCStr || !filePathCStr) {
53+
if (keyCStr) env->ReleaseStringUTFChars(key, keyCStr);
54+
if (filePathCStr) env->ReleaseStringUTFChars(filePath, filePathCStr);
55+
return nullptr;
56+
}
57+
58+
std::string keyStr(keyCStr);
59+
std::string filePathStr(filePathCStr);
60+
61+
env->ReleaseStringUTFChars(key, keyCStr);
62+
env->ReleaseStringUTFChars(filePath, filePathCStr);
63+
64+
// Load the audio resource
65+
elementary::AudioLoadResult result = audioEngine->loadAudioResource(keyStr, filePathStr);
66+
67+
// Find the AudioResourceInfo class
68+
jclass infoClass = env->FindClass("com/elementary/AudioResourceInfo");
69+
if (!infoClass) {
70+
return nullptr;
71+
}
72+
73+
// Get the constructor
74+
jmethodID constructor = env->GetMethodID(infoClass, "<init>", "(ZLjava/lang/String;Ljava/lang/String;IJID)V");
75+
if (!constructor) {
76+
return nullptr;
77+
}
78+
79+
// Create the result object
80+
jstring jKey = env->NewStringUTF(result.info.key.c_str());
81+
jstring jError = env->NewStringUTF(result.error.c_str());
82+
83+
jobject infoObj = env->NewObject(
84+
infoClass,
85+
constructor,
86+
static_cast<jboolean>(result.success),
87+
jError,
88+
jKey,
89+
static_cast<jint>(result.info.channels),
90+
static_cast<jlong>(result.info.sampleCount),
91+
static_cast<jint>(result.info.sampleRate),
92+
static_cast<jdouble>(result.info.durationMs)
93+
);
94+
95+
return infoObj;
96+
}
97+
98+
extern "C"
99+
JNIEXPORT jboolean JNICALL
100+
Java_com_elementary_ElementaryModule_nativeUnloadAudioResource(JNIEnv *env, jclass type, jstring key) {
101+
if (!audioEngine) {
102+
return JNI_FALSE;
103+
}
104+
105+
const char *keyCStr = env->GetStringUTFChars(key, nullptr);
106+
if (!keyCStr) {
107+
return JNI_FALSE;
108+
}
109+
110+
std::string keyStr(keyCStr);
111+
env->ReleaseStringUTFChars(key, keyCStr);
112+
113+
bool result = audioEngine->unloadAudioResource(keyStr);
114+
return result ? JNI_TRUE : JNI_FALSE;
115+
}

android/src/main/java/com/elementary/ElementaryModule.kt

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,23 @@ import com.facebook.react.bridge.ReactApplicationContext
44
import com.facebook.react.bridge.ReactContextBaseJavaModule
55
import com.facebook.react.bridge.ReactMethod
66
import com.facebook.react.bridge.Promise
7+
import com.facebook.react.bridge.Arguments
78
import com.facebook.react.bridge.WritableMap
89
import com.facebook.react.modules.core.DeviceEventManagerModule
910

11+
/**
12+
* Data class for audio resource information returned from native code
13+
*/
14+
data class AudioResourceInfo(
15+
val success: Boolean,
16+
val error: String,
17+
val key: String,
18+
val channels: Int,
19+
val sampleCount: Long,
20+
val sampleRate: Int,
21+
val durationMs: Double
22+
)
23+
1024
class ElementaryModule(reactContext: ReactApplicationContext) :
1125
ReactContextBaseJavaModule(reactContext) {
1226

@@ -34,6 +48,51 @@ class ElementaryModule(reactContext: ReactApplicationContext) :
3448
// No-op
3549
}
3650

51+
@ReactMethod
52+
fun loadAudioResource(key: String, filePath: String, promise: Promise) {
53+
Thread {
54+
try {
55+
val result = nativeLoadAudioResource(key, filePath)
56+
if (result == null) {
57+
promise.reject("E_NATIVE_ERROR", "Native audio engine not initialized")
58+
return@Thread
59+
}
60+
61+
if (!result.success) {
62+
promise.reject("E_LOAD_FAILED", result.error)
63+
return@Thread
64+
}
65+
66+
val info = Arguments.createMap().apply {
67+
putString("key", result.key)
68+
putInt("channels", result.channels)
69+
putDouble("sampleCount", result.sampleCount.toDouble())
70+
putInt("sampleRate", result.sampleRate)
71+
putDouble("durationMs", result.durationMs)
72+
}
73+
promise.resolve(info)
74+
} catch (e: Exception) {
75+
promise.reject("E_LOAD_FAILED", e.message, e)
76+
}
77+
}.start()
78+
}
79+
80+
@ReactMethod
81+
fun unloadAudioResource(key: String, promise: Promise) {
82+
try {
83+
val result = nativeUnloadAudioResource(key)
84+
promise.resolve(result)
85+
} catch (e: Exception) {
86+
promise.reject("E_UNLOAD_FAILED", e.message, e)
87+
}
88+
}
89+
90+
@ReactMethod
91+
fun getDocumentsDirectory(promise: Promise) {
92+
val documentsDir = reactApplicationContext.filesDir.absolutePath
93+
promise.resolve(documentsDir)
94+
}
95+
3796
// Helper to emit events
3897
private fun sendEvent(eventName: String, params: WritableMap?) {
3998
reactApplicationContext
@@ -54,7 +113,9 @@ class ElementaryModule(reactContext: ReactApplicationContext) :
54113
nativeStartAudioEngine();
55114
}
56115

57-
external fun nativeGetSampleRate(): Int;
58-
external fun nativeApplyInstructions(message: String);
59-
external fun nativeStartAudioEngine();
116+
external fun nativeGetSampleRate(): Int
117+
external fun nativeApplyInstructions(message: String)
118+
external fun nativeStartAudioEngine()
119+
external fun nativeLoadAudioResource(key: String, filePath: String): AudioResourceInfo?
120+
external fun nativeUnloadAudioResource(key: String): Boolean
60121
}

android/src/newarch/com/elementary/ElementaryTurboModule.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,19 @@ public void addListener(String eventName) {
3232
public void removeListeners(double count) {
3333
module.removeListeners(count);
3434
}
35-
}
35+
36+
@Override
37+
public void loadAudioResource(String key, String filePath, Promise promise) {
38+
module.loadAudioResource(key, filePath, promise);
39+
}
40+
41+
@Override
42+
public void unloadAudioResource(String key, Promise promise) {
43+
module.unloadAudioResource(key, promise);
44+
}
45+
46+
@Override
47+
public void getDocumentsDirectory(Promise promise) {
48+
module.getDocumentsDirectory(promise);
49+
}
50+
}

cpp/AudioResourceLoader.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#include "AudioResourceLoader.h"
2+
#include "miniaudio.h"
3+
4+
namespace elementary {
5+
6+
AudioLoadResult AudioResourceLoader::loadFile(const std::string& key, const std::string& filePath) {
7+
AudioLoadResult result;
8+
result.success = false;
9+
result.info.key = key;
10+
11+
ma_decoder decoder;
12+
ma_decoder_config config = ma_decoder_config_init(ma_format_f32, 0, 0);
13+
14+
ma_result initResult = ma_decoder_init_file(filePath.c_str(), &config, &decoder);
15+
if (initResult != MA_SUCCESS) {
16+
result.error = "Failed to open audio file: " + filePath;
17+
return result;
18+
}
19+
20+
ma_uint64 totalFrames;
21+
ma_result lengthResult = ma_decoder_get_length_in_pcm_frames(&decoder, &totalFrames);
22+
if (lengthResult != MA_SUCCESS) {
23+
result.error = "Failed to get audio file length";
24+
ma_decoder_uninit(&decoder);
25+
return result;
26+
}
27+
28+
uint32_t channels = decoder.outputChannels;
29+
uint32_t sampleRate = decoder.outputSampleRate;
30+
31+
std::vector<float> interleavedData(totalFrames * channels);
32+
ma_uint64 framesRead;
33+
ma_result readResult = ma_decoder_read_pcm_frames(&decoder, interleavedData.data(), totalFrames, &framesRead);
34+
35+
ma_decoder_uninit(&decoder);
36+
37+
if (readResult != MA_SUCCESS && readResult != MA_AT_END) {
38+
result.error = "Failed to read audio data";
39+
return result;
40+
}
41+
42+
interleavedData.resize(framesRead * channels);
43+
44+
// Deinterleave into separate channel data
45+
result.data.resize(framesRead * channels);
46+
for (uint32_t ch = 0; ch < channels; ++ch) {
47+
for (ma_uint64 frame = 0; frame < framesRead; ++frame) {
48+
result.data[ch * framesRead + frame] = interleavedData[frame * channels + ch];
49+
}
50+
}
51+
52+
result.info.channels = channels;
53+
result.info.sampleCount = static_cast<uint64_t>(framesRead);
54+
result.info.sampleRate = sampleRate;
55+
result.info.durationMs = (static_cast<double>(framesRead) / static_cast<double>(sampleRate)) * 1000.0;
56+
57+
result.success = true;
58+
return result;
59+
}
60+
61+
} // namespace elementary

cpp/AudioResourceLoader.h

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#ifndef AUDIORESOURCELOADER_H
2+
#define AUDIORESOURCELOADER_H
3+
4+
#include <string>
5+
#include <vector>
6+
#include <memory>
7+
#include <cstdint>
8+
9+
namespace elementary {
10+
11+
/**
12+
* Metadata about a loaded audio resource
13+
*/
14+
struct AudioResourceInfo {
15+
std::string key;
16+
uint32_t channels;
17+
uint64_t sampleCount; // samples per channel
18+
uint32_t sampleRate;
19+
double durationMs;
20+
};
21+
22+
/**
23+
* Result of loading an audio resource
24+
*/
25+
struct AudioLoadResult {
26+
bool success;
27+
std::string error;
28+
AudioResourceInfo info;
29+
std::vector<float> data; // Deinterleaved: all ch0 samples, then all ch1 samples, etc.
30+
};
31+
32+
/**
33+
* Audio Resource Loader using miniaudio
34+
*
35+
* Loads audio files (WAV, MP3, FLAC, etc.) and converts to deinterleaved float32 format
36+
* suitable for Elementary Audio's Virtual File System.
37+
*/
38+
class AudioResourceLoader {
39+
public:
40+
/**
41+
* Load an audio file from disk
42+
* @param key - Unique identifier for this resource
43+
* @param filePath - Absolute path to the audio file
44+
* @return AudioLoadResult containing the audio data and metadata, or error info
45+
*/
46+
static AudioLoadResult loadFile(const std::string& key, const std::string& filePath);
47+
};
48+
49+
} // namespace elementary
50+
51+
#endif // AUDIORESOURCELOADER_H

cpp/audioengine.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "audioengine.h"
2+
#include "vendor/elementary/runtime/elem/AudioBufferResource.h"
23
#define MINIAUDIO_IMPLEMENTATION
34
#include "miniaudio.h"
45

@@ -21,6 +22,61 @@ namespace elementary {
2122
return device.sampleRate;
2223
}
2324

25+
AudioLoadResult AudioEngine::loadAudioResource(const std::string& key, const std::string& filePath) {
26+
AudioLoadResult result = AudioResourceLoader::loadFile(key, filePath);
27+
28+
if (!result.success) {
29+
return result;
30+
}
31+
32+
size_t numChannels = result.info.channels;
33+
size_t numSamples = result.info.sampleCount;
34+
std::vector<float*> channelPtrs(numChannels);
35+
for (size_t ch = 0; ch < numChannels; ++ch) {
36+
channelPtrs[ch] = result.data.data() + (ch * numSamples);
37+
}
38+
39+
auto resource = std::make_unique<elem::AudioBufferResource>(
40+
channelPtrs.data(),
41+
numChannels,
42+
numSamples
43+
);
44+
bool added = proxy->runtime.addSharedResource(key, std::move(resource));
45+
46+
if (!added) {
47+
result.success = false;
48+
result.error = "Resource with key '" + key + "' already exists";
49+
return result;
50+
}
51+
52+
{
53+
std::lock_guard<std::mutex> lock(resourceMutex);
54+
loadedResources.insert(key);
55+
}
56+
57+
return result;
58+
}
59+
60+
bool AudioEngine::unloadAudioResource(const std::string& key) {
61+
bool found = false;
62+
63+
// Only hold the lock while modifying loadedResources
64+
{
65+
std::lock_guard<std::mutex> lock(resourceMutex);
66+
auto it = loadedResources.find(key);
67+
if (it != loadedResources.end()) {
68+
loadedResources.erase(it);
69+
found = true;
70+
}
71+
}
72+
73+
if (found) {
74+
proxy->runtime.pruneSharedResources();
75+
}
76+
77+
return found;
78+
}
79+
2480
void AudioEngine::initializeDevice() {
2581
proxy = std::make_unique<DeviceProxy>(44100.0, 1024);
2682

0 commit comments

Comments
 (0)