diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index fcefba66..f1c7b96d 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -4,22 +4,33 @@ project(react-native-audio-context) set (CMAKE_VERBOSE_MAKEFILE ON) set (CMAKE_CXX_STANDARD 14) -add_library(react-native-audio-context - SHARED - ../cpp/JSIExampleHostObject.cpp - cpp-adapter.cpp -) +include(../node_modules/react-native/ReactAndroid/cmake-utils/folly-flags.cmake) +add_compile_options(${folly_FLAGS}) -# Specifies a path to native header files. include_directories( - ../cpp - ../node_modules/react-native/ReactCommon/jsi + ../cpp + src/main/cpp + ../node_modules/react-native/ReactCommon/jsi + ../node_modules/react-native/ReactAndroid/src/main/jni/react/jni + ../node_modules/react-native/ReactAndroid/src/main/jni/third-party/folly +) + +add_library(react-native-audio-context SHARED + src/main/cpp/OnLoad.cpp + src/main/cpp/AudioContext + src/main/cpp/OscillatorNode + ../cpp/AudioContextHostObject + ../cpp/OscillatorNodeHostObject ) find_package(ReactAndroid REQUIRED CONFIG) +find_package(fbjni REQUIRED CONFIG) -target_link_libraries( - react-native-audio-context - ReactAndroid::jsi - android +target_link_libraries(react-native-audio-context + ReactAndroid::jsi + ReactAndroid::reactnativejni + fbjni::fbjni + ReactAndroid::folly_runtime + android + log ) diff --git a/android/build.gradle b/android/build.gradle index 431dd98a..65874cc2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -72,6 +72,10 @@ android { } } + packagingOptions { + excludes = ["**/libc++_shared.so", "**/libfbjni.so", "**/libjsi.so", "**/libreactnativejni.so", "**/libfolly_json.so", "**/libreanimated.so", "**/libjscexecutor.so", "**/libhermes.so"] + } + externalNativeBuild { cmake { path "CMakeLists.txt" @@ -125,6 +129,7 @@ dependencies { //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" implementation 'androidx.core:core-ktx:1.13.1' + implementation 'com.facebook.fbjni:fbjni:0.6.0' } if (isNewArchitectureEnabled()) { diff --git a/android/cpp-adapter.cpp b/android/cpp-adapter.cpp deleted file mode 100644 index a7b0247d..00000000 --- a/android/cpp-adapter.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include <jni.h> -#include <jsi/jsi.h> -#include "JSIExampleHostObject.h" - -using namespace facebook; - -void install(jsi::Runtime& runtime) { - auto hostObject = std::make_shared<example::JSIExampleHostObject>(); - auto object = jsi::Object::createFromHostObject(runtime, hostObject); - runtime.global().setProperty(runtime, "__JSIExampleProxy", std::move(object)); -} - -extern "C" -JNIEXPORT void JNICALL -Java_com_audiocontext_jsi_JSIExampleModule_00024Companion_nativeInstall(JNIEnv *env, jobject clazz, jlong jsiPtr) { - auto runtime = reinterpret_cast<jsi::Runtime*>(jsiPtr); - if (runtime) { - install(*runtime); - } -} diff --git a/android/src/main/cpp/AudioContext.cpp b/android/src/main/cpp/AudioContext.cpp new file mode 100644 index 00000000..731d04a2 --- /dev/null +++ b/android/src/main/cpp/AudioContext.cpp @@ -0,0 +1,28 @@ +#include "AudioContext.h" +#include "AudioContextHostObject.h" +#include "OscillatorNode.h" +#include <fbjni/fbjni.h> +#include <jsi/jsi.h> +#include <android/log.h> + +namespace audiocontext { + + using namespace facebook::jni; + + AudioContext::AudioContext(jni::alias_ref<AudioContext::jhybridobject> &jThis, + jlong jsContext): javaObject_(make_global(jThis)) { + auto runtime = reinterpret_cast<jsi::Runtime *>(jsContext); + auto hostObject = std::make_shared<AudioContextHostObject>(this); + auto object = jsi::Object::createFromHostObject(*runtime, hostObject); + runtime->global().setProperty(*runtime, "__AudioContextProxy", std::move(object)); + } + + jsi::Object AudioContext::createOscillator() { + static const auto method = javaClassLocal()->getMethod<OscillatorNode()>("createOscillator"); + auto oscillator = method(javaObject_.get()); + auto oscillatorCppInstance = oscillator->cthis(); + + return oscillatorCppInstance->createOscillatorNodeHostObject(); + } + +} // namespace audiocontext diff --git a/android/src/main/cpp/AudioContext.h b/android/src/main/cpp/AudioContext.h new file mode 100644 index 00000000..d50a1775 --- /dev/null +++ b/android/src/main/cpp/AudioContext.h @@ -0,0 +1,41 @@ +#pragma once + +#include <fbjni/fbjni.h> +#include <jsi/jsi.h> +#include <react/jni/CxxModuleWrapper.h> +#include <react/jni/JMessageQueueThread.h> +#include "AudioContextHostObject.h" +#include "OscillatorNode.h" + +namespace audiocontext { + + using namespace facebook; + using namespace facebook::jni; + + class AudioContext : public jni::HybridClass<AudioContext> { + public: + static auto constexpr kJavaDescriptor = "Lcom/audiocontext/context/AudioContext;"; + + static jni::local_ref<AudioContext::jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis, jlong jsContext) + { + return makeCxxInstance(jThis, jsContext); + } + + static void registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", AudioContext::initHybrid), + }); + } + + jsi::Object createOscillator(); + + private: + friend HybridBase; + + global_ref<AudioContext::javaobject> javaObject_; + std::shared_ptr<jsi::Runtime> runtime_; + + explicit AudioContext(jni::alias_ref<AudioContext::jhybridobject>& jThis, jlong jsContext); + }; + +} // namespace audiocontext diff --git a/android/src/main/cpp/OnLoad.cpp b/android/src/main/cpp/OnLoad.cpp new file mode 100644 index 00000000..93daacda --- /dev/null +++ b/android/src/main/cpp/OnLoad.cpp @@ -0,0 +1,13 @@ +#include <fbjni/fbjni.h> +#include "OscillatorNode.h" +#include "AudioContext.h" + +using namespace audiocontext; + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) +{ + return facebook::jni::initialize(vm, [] { + OscillatorNode::registerNatives(); + AudioContext::registerNatives(); + }); +} diff --git a/android/src/main/cpp/OscillatorNode.cpp b/android/src/main/cpp/OscillatorNode.cpp new file mode 100644 index 00000000..5e3899b3 --- /dev/null +++ b/android/src/main/cpp/OscillatorNode.cpp @@ -0,0 +1,48 @@ +#include "OscillatorNode.h" +#include <fbjni/fbjni.h> +#include <jsi/jsi.h> +#include <android/log.h> + +namespace audiocontext { + + using namespace facebook::jni; + + OscillatorNode::OscillatorNode(jni::alias_ref<OscillatorNode::jhybridobject> &jThis, jlong jsContext) + : javaObject_(make_global(jThis)), jsContext(jsContext){} + + jsi::Object OscillatorNode::createOscillatorNodeHostObject() { + auto runtime = reinterpret_cast<jsi::Runtime *>(jsContext); + auto hostObject = std::make_shared<OscillatorNodeHostObject>(this); + return jsi::Object::createFromHostObject(*runtime, hostObject); + } + + void OscillatorNode::start() { + static const auto method = javaClassStatic()->getMethod<void()>("start"); + method(javaObject_.get()); + } + + void OscillatorNode::stop() { + static const auto method = javaClassStatic()->getMethod<void()>("stop"); + method(javaObject_.get()); + } + + void OscillatorNode::setFrequency(jdouble frequency) { + static const auto method = javaClassStatic()->getMethod<void(jdouble)>("setFrequency"); + method(javaObject_.get(), frequency); + } + + void OscillatorNode::setDetune(jdouble detune) { + static const auto method = javaClassStatic()->getMethod<void(jdouble)>("setDetune"); + method(javaObject_.get(), detune); + } + + jdouble OscillatorNode::getFrequency() { + static const auto method = javaClassLocal()->getMethod<jdouble()>("getFrequency"); + return method(javaObject_.get()); + } + + jdouble OscillatorNode::getDetune() { + static const auto method = javaClassStatic()->getMethod<jdouble()>("getDetune"); + return method(javaObject_.get()); + } +} // namespace audiocontext diff --git a/android/src/main/cpp/OscillatorNode.h b/android/src/main/cpp/OscillatorNode.h new file mode 100644 index 00000000..8471ab17 --- /dev/null +++ b/android/src/main/cpp/OscillatorNode.h @@ -0,0 +1,47 @@ +#pragma once + +#include <fbjni/fbjni.h> +#include <jsi/jsi.h> +#include <react/jni/CxxModuleWrapper.h> +#include <react/jni/JMessageQueueThread.h> +#include "OscillatorNodeHostObject.h" + +namespace audiocontext { + + using namespace facebook; + using namespace facebook::jni; + + class OscillatorNode : public jni::HybridClass<OscillatorNode> { + public: + static auto constexpr kJavaDescriptor = "Lcom/audiocontext/nodes/oscillator/OscillatorNode;"; + + static jni::local_ref<OscillatorNode::jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis, jlong jsContext) + { + return makeCxxInstance(jThis, jsContext); + } + + static void registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", OscillatorNode::initHybrid), + }); + } + + void start(); + void stop(); + void setFrequency(jdouble frequency); + void setDetune(jdouble detune); + jdouble getFrequency(); + jdouble getDetune(); + + jsi::Object createOscillatorNodeHostObject(); + + private: + friend HybridBase; + + global_ref<OscillatorNode::javaobject> javaObject_; + jlong jsContext; + + explicit OscillatorNode(jni::alias_ref<OscillatorNode::jhybridobject>& jThis, jlong jsContext); + }; + +} // namespace audiocontext diff --git a/android/src/main/java/com/audiocontext/JSIExamplePackage.kt b/android/src/main/java/com/audiocontext/AudioContextPackage.kt similarity index 72% rename from android/src/main/java/com/audiocontext/JSIExamplePackage.kt rename to android/src/main/java/com/audiocontext/AudioContextPackage.kt index 81750b80..e4cb0d94 100644 --- a/android/src/main/java/com/audiocontext/JSIExamplePackage.kt +++ b/android/src/main/java/com/audiocontext/AudioContextPackage.kt @@ -1,14 +1,14 @@ package com.audiocontext -import com.audiocontext.jsi.JSIExampleModule +import com.audiocontext.nativemodules.AudioContextModule import com.facebook.react.ReactPackage import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.ViewManager -class JSIExamplePackage : ReactPackage { +class AudioContextPackage : ReactPackage { override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> { - return listOf<NativeModule>(JSIExampleModule(reactContext)) + return listOf<NativeModule>(AudioContextModule(reactContext)) } override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> { diff --git a/android/src/main/java/com/audiocontext/context/AudioContext.kt b/android/src/main/java/com/audiocontext/context/AudioContext.kt new file mode 100644 index 00000000..86f02201 --- /dev/null +++ b/android/src/main/java/com/audiocontext/context/AudioContext.kt @@ -0,0 +1,40 @@ +package com.audiocontext.context + +import android.media.AudioTrack +import com.audiocontext.nodes.AudioDestinationNode +import com.audiocontext.nodes.AudioNode +import com.audiocontext.nodes.oscillator.OscillatorNode +import com.facebook.jni.HybridData +import com.facebook.react.bridge.ReactApplicationContext +import java.util.concurrent.CopyOnWriteArrayList + +class AudioContext(private val reactContext: ReactApplicationContext) : BaseAudioContext { + override var sampleRate: Int = 44100 + override val destination: AudioDestinationNode = AudioDestinationNode(this) + override val sources = CopyOnWriteArrayList<AudioNode>() + + private val mHybridData: HybridData?; + + companion object { + init { + System.loadLibrary("react-native-audio-context") + } + } + + init { + mHybridData = initHybrid(reactContext.javaScriptContextHolder!!.get()) + } + + external fun initHybrid(l: Long): HybridData? + + private fun addNode(node: AudioNode) { + sources.add(node) + } + + override fun createOscillator(): OscillatorNode { + val oscillator = OscillatorNode(this, reactContext) + oscillator.connect(destination) + addNode(oscillator) + return oscillator + } +} diff --git a/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt b/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt new file mode 100644 index 00000000..80175c5d --- /dev/null +++ b/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt @@ -0,0 +1,15 @@ +package com.audiocontext.context + +import android.media.AudioTrack +import com.audiocontext.nodes.AudioDestinationNode +import com.audiocontext.nodes.AudioNode +import com.audiocontext.nodes.oscillator.OscillatorNode +import com.facebook.jni.HybridData + +interface BaseAudioContext { + val sampleRate: Int + val destination: AudioDestinationNode + val sources: List<AudioNode> + + abstract fun createOscillator(): OscillatorNode +} diff --git a/android/src/main/java/com/audiocontext/jsi/JSIExampleModule.kt b/android/src/main/java/com/audiocontext/jsi/JSIExampleModule.kt deleted file mode 100644 index 371c2886..00000000 --- a/android/src/main/java/com/audiocontext/jsi/JSIExampleModule.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.audiocontext.jsi - -import android.util.Log -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.ReactMethod -import com.facebook.react.module.annotations.ReactModule - -@ReactModule(name = JSIExampleModule.NAME) -class JSIExampleModule(reactContext: ReactApplicationContext?) : - ReactContextBaseJavaModule(reactContext) { - override fun getName(): String { - return NAME - } - - @ReactMethod(isBlockingSynchronousMethod = true) - fun install(): Boolean { - try { - System.loadLibrary("react-native-audio-context") - - val jsContext = reactApplicationContext.javaScriptContextHolder - - nativeInstall(jsContext!!.get()) - return true - } catch (exception: Exception) { - Log.e(NAME, "Failed to install JSI Bindings for react-native-audio-context", exception) - return false - } - } - - companion object { - const val NAME: String = "JSIExample" - - private external fun nativeInstall(jsiPtr: Long) - } -} diff --git a/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt b/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt new file mode 100644 index 00000000..97440cb7 --- /dev/null +++ b/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt @@ -0,0 +1,18 @@ +package com.audiocontext.nativemodules + +import com.audiocontext.context.AudioContext +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod + +class AudioContextModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + override fun getName(): String { + return "AudioContextModule" + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun initAudioContext() { + AudioContext(reactContext) + } +} diff --git a/android/src/main/java/com/audiocontext/nodes/AudioDestinationNode.kt b/android/src/main/java/com/audiocontext/nodes/AudioDestinationNode.kt new file mode 100644 index 00000000..96c121a0 --- /dev/null +++ b/android/src/main/java/com/audiocontext/nodes/AudioDestinationNode.kt @@ -0,0 +1,14 @@ +package com.audiocontext.nodes + +import android.media.AudioTrack +import com.audiocontext.context.BaseAudioContext + + +class AudioDestinationNode(context: BaseAudioContext): AudioNode(context) { + override val numberOfInputs = 1 + override val numberOfOutputs = 0 + + override fun process(buffer: ShortArray, audioTrack: AudioTrack) { + audioTrack.write(buffer, 0, buffer.size) + } +} diff --git a/android/src/main/java/com/audiocontext/nodes/AudioNode.kt b/android/src/main/java/com/audiocontext/nodes/AudioNode.kt new file mode 100644 index 00000000..9c68beaa --- /dev/null +++ b/android/src/main/java/com/audiocontext/nodes/AudioNode.kt @@ -0,0 +1,25 @@ +package com.audiocontext.nodes + +import android.media.AudioTrack +import com.audiocontext.context.BaseAudioContext + + +abstract class AudioNode(val context: BaseAudioContext) { + abstract val numberOfInputs: Int; + abstract val numberOfOutputs: Int; + private val connectedNodes = mutableListOf<AudioNode>() + + fun connect(destination: AudioNode) { + if(this.numberOfOutputs > 0) { + connectedNodes.add(destination) + } + } + + fun disconnect() { + connectedNodes.clear() + } + + open fun process(buffer: ShortArray, audioTrack: AudioTrack) { + connectedNodes.forEach { it.process(buffer, audioTrack) } + } +} diff --git a/android/src/main/java/com/audiocontext/nodes/AudioScheduledSourceNode.kt b/android/src/main/java/com/audiocontext/nodes/AudioScheduledSourceNode.kt new file mode 100644 index 00000000..b2fcc5d6 --- /dev/null +++ b/android/src/main/java/com/audiocontext/nodes/AudioScheduledSourceNode.kt @@ -0,0 +1,8 @@ +package com.audiocontext.nodes + +import com.audiocontext.context.BaseAudioContext + +abstract class AudioScheduledSourceNode(context: BaseAudioContext) : AudioNode(context) { + abstract fun start() + abstract fun stop() +} diff --git a/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt new file mode 100644 index 00000000..f05373a4 --- /dev/null +++ b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt @@ -0,0 +1,120 @@ +package com.audiocontext.nodes.oscillator + +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioTrack +import android.util.Log +import com.audiocontext.context.BaseAudioContext +import com.audiocontext.nodes.AudioScheduledSourceNode +import com.facebook.jni.HybridData +import com.facebook.react.bridge.ReactApplicationContext +import kotlin.math.abs +import kotlin.math.floor +import kotlin.math.sin + +class OscillatorNode(context: BaseAudioContext, reactContext: ReactApplicationContext) : AudioScheduledSourceNode(context) { + override val numberOfInputs: Int = 0 + override val numberOfOutputs: Int = 1 + private var frequency: Double = 440.0 + private var detune: Double = 0.0 + private var waveType: WaveType = WaveType.SINE + + private val audioTrack: AudioTrack + @Volatile private var isPlaying: Boolean = false + private var playbackThread: Thread? = null + private var buffer: ShortArray = ShortArray(1024) + + private val mHybridData: HybridData?; + + companion object { + init { + System.loadLibrary("react-native-audio-context") + } + } + + init { + mHybridData = initHybrid(reactContext.javaScriptContextHolder!!.get()) + val bufferSize = AudioTrack.getMinBufferSize( + context.sampleRate, + AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT) + this.audioTrack = AudioTrack( + AudioManager.STREAM_MUSIC, context.sampleRate, AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM + ) + } + + external fun initHybrid(l: Long): HybridData? + + fun getFrequency(): Double { + return frequency + } + + fun getDetune(): Double { + return detune + } + + fun setFrequency(frequency: Double) { + this.frequency = frequency + } + + fun setDetune(detune: Double) { + this.detune = detune + } + + override fun start() { + if(isPlaying) { + return + } + + isPlaying = true + audioTrack.play() + playbackThread = Thread { generateSound() }.apply{ start()} + } + + override fun stop() { + if(!isPlaying) { + return + } + + isPlaying = false + audioTrack.stop() + playbackThread?.join() + } + + private fun generateSound() { + var wavePhase = 0.0 + var phaseChange: Double + + while(isPlaying) { + phaseChange = 2 * Math.PI * (frequency + detune) / context.sampleRate + + for(i in buffer.indices) { + buffer[i] = when(waveType) { + WaveType.SINE -> sineWaveBuffer(wavePhase) + WaveType.SQUARE -> squareWaveBuffer(wavePhase) + WaveType.SAWTOOTH -> sawtoothWaveBuffer(wavePhase) + WaveType.TRIANGLE -> triangleWaveBuffer(wavePhase) + } + wavePhase += phaseChange + } + process(buffer, audioTrack) + } + audioTrack.flush() + } + + private fun sineWaveBuffer(wavePhase: Double): Short { + return (sin(wavePhase) * Short.MAX_VALUE).toInt().toShort() + } + + private fun squareWaveBuffer(wavePhase: Double): Short { + return ((if (sin(wavePhase) >= 0) 1 else -1) * Short.MAX_VALUE).toShort() + } + + private fun sawtoothWaveBuffer(wavePhase: Double): Short { + return ((2 * (wavePhase / (2 * Math.PI) - floor(wavePhase / (2 * Math.PI) + 0.5))) * Short.MAX_VALUE).toInt().toShort() + } + + private fun triangleWaveBuffer(wavePhase: Double): Short { + return ((2 * abs(2 * (wavePhase / (2 * Math.PI) - floor(wavePhase / (2 * Math.PI) + 0.5))) - 1) * Short.MAX_VALUE).toInt().toShort() + } +} diff --git a/android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt b/android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt new file mode 100644 index 00000000..5d506dca --- /dev/null +++ b/android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt @@ -0,0 +1,20 @@ +package com.audiocontext.nodes.oscillator + +enum class WaveType { + SINE, + SQUARE, + SAWTOOTH, + TRIANGLE; + + companion object { + fun fromString(type: String): WaveType { + return when (type.uppercase()) { + "SINE" -> SINE + "SQUARE" -> SQUARE + "SAWTOOTH" -> SAWTOOTH + "TRIANGLE" -> TRIANGLE + else -> throw IllegalArgumentException("Unknown wave type: $type") + } + } + } +} diff --git a/cpp/AudioContextHostObject.cpp b/cpp/AudioContextHostObject.cpp new file mode 100644 index 00000000..6a16841b --- /dev/null +++ b/cpp/AudioContextHostObject.cpp @@ -0,0 +1,35 @@ +#include "AudioContextHostObject.h" + +namespace audiocontext { + using namespace facebook; + + std::vector<jsi::PropNameID> AudioContextHostObject::getPropertyNames(jsi::Runtime& runtime) { + std::vector<jsi::PropNameID> propertyNames; + propertyNames.push_back(jsi::PropNameID::forUtf8(runtime, "createOscillator")); + return propertyNames; + } + + jsi::Value AudioContextHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propNameId) { + auto propName = propNameId.utf8(runtime); + + if(propName == "createOscillator") { + return createOscillator(runtime, propNameId); + } + + throw std::runtime_error("Not yet implemented!"); + } + + void AudioContextHostObject::set(jsi::Runtime& runtime, const jsi::PropNameID& propNameId, const jsi::Value& value) { + auto propName = propNameId.utf8(runtime); + + throw std::runtime_error("Not yet implemented!"); + } + + jsi::Value AudioContextHostObject::createOscillator(jsi::Runtime &runtime, + const jsi::PropNameID &propNameId) { + return jsi::Function::createFromHostFunction(runtime, propNameId, 0, [this](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value { + return audiocontext_->createOscillator(); + }); + } + +} diff --git a/cpp/AudioContextHostObject.h b/cpp/AudioContextHostObject.h new file mode 100644 index 00000000..af9e07fd --- /dev/null +++ b/cpp/AudioContextHostObject.h @@ -0,0 +1,26 @@ +#pragma once + +#include <jsi/jsi.h> +#include <fbjni/fbjni.h> +#include <fbjni/detail/Hybrid.h> +#include "AudioContext.h" + +namespace audiocontext { + using namespace facebook; + + class AudioContext; + + class AudioContextHostObject : public jsi::HostObject { + private: + AudioContext* audiocontext_; + + public: + explicit AudioContextHostObject(AudioContext* audiocontext) : audiocontext_(audiocontext) {} + + jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& name) override; + void set(jsi::Runtime& runtime, const jsi::PropNameID& name, const jsi::Value& value) override; + std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime& rt) override; + + jsi::Value createOscillator(jsi::Runtime& runtime, const jsi::PropNameID& propNameId); + }; +} // namespace audiocontext diff --git a/cpp/JSIExampleHostObject.cpp b/cpp/JSIExampleHostObject.cpp deleted file mode 100644 index bc55ccb1..00000000 --- a/cpp/JSIExampleHostObject.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include "JSIExampleHostObject.h" -#include <jsi/jsi.h> - -namespace example { - using namespace facebook; - - std::vector<jsi::PropNameID> JSIExampleHostObject::getPropertyNames(jsi::Runtime &runtime) - { - std::vector<jsi::PropNameID> propertyNames; - propertyNames.push_back(jsi::PropNameID::forAscii(runtime, "multiply")); - return propertyNames; - } - - jsi::Value JSIExampleHostObject::get(jsi::Runtime &runtime, const jsi::PropNameID &propNameId) { - auto propName = propNameId.utf8(runtime); - - if (propName == "multiply") { - return jsi::Function::createFromHostFunction(runtime, propNameId, 2, - [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) { - if (count != 2) { - throw std::invalid_argument("multiply expects exactly two arguments"); - } - return this->multiply(rt, args[0], args[1]); - }); - } - - throw std::runtime_error("Not yet implemented!"); - } - - void JSIExampleHostObject::set(jsi::Runtime &runtime, const jsi::PropNameID &propNameId, const jsi::Value &value) - { - auto propName = propNameId.utf8(runtime); - if (propName == "multiply") - { - // Do nothing - return; - } - throw std::runtime_error("Not yet implemented!"); - } - - jsi::Value JSIExampleHostObject::multiply(jsi::Runtime &runtime, const jsi::Value &value, const jsi::Value &value2) { - if (value.isNumber() && value2.isNumber()) { - // Extract numbers and add them - double result = value.asNumber() + value2.asNumber(); - return jsi::Value(result); - } else { - // Handle other cases (e.g., one is a number and the other is a string) - return jsi::Value::undefined(); - } - } -} diff --git a/cpp/JSIExampleHostObject.h b/cpp/JSIExampleHostObject.h deleted file mode 100644 index 2ea3a4ba..00000000 --- a/cpp/JSIExampleHostObject.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef JSIEXAMPLEHOSTOBJECT_H -#define JSIEXAMPLEHOSTOBJECT_H - -#include <jsi/jsi.h> - -namespace example -{ - - using namespace facebook; - - class JSI_EXPORT JSIExampleHostObject : public jsi::HostObject - { - public: - explicit JSIExampleHostObject() = default; - - public: - jsi::Value get(jsi::Runtime &, const jsi::PropNameID &name) override; - void set(jsi::Runtime &, const jsi::PropNameID &name, const jsi::Value &value) override; - std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime &rt) override; - static jsi::Value multiply(jsi::Runtime &, const jsi::Value &value, const jsi::Value &value2); - }; - -} // namespace margelo - -#endif /* JSIEXAMPLEHOSTOBJECT_H */ diff --git a/cpp/OscillatorNodeHostObject.cpp b/cpp/OscillatorNodeHostObject.cpp new file mode 100644 index 00000000..9d387fcc --- /dev/null +++ b/cpp/OscillatorNodeHostObject.cpp @@ -0,0 +1,84 @@ +#include "OscillatorNodeHostObject.h" +#include <android/log.h> + +namespace audiocontext { + using namespace facebook; + + std::vector<jsi::PropNameID> OscillatorNodeHostObject::getPropertyNames(jsi::Runtime& runtime) { + std::vector<jsi::PropNameID> propertyNames; + propertyNames.push_back(jsi::PropNameID::forAscii(runtime, "start")); + propertyNames.push_back(jsi::PropNameID::forAscii(runtime, "stop")); + return propertyNames; + } + + jsi::Value OscillatorNodeHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propNameId) { + auto propName = propNameId.utf8(runtime); + + if (propName == "start") { + return start(runtime, propNameId); + } + + if (propName == "stop") { + return stop(runtime, propNameId); + } + + if (propName == "frequency") { + return frequency(runtime, propNameId); + } + + if (propName == "detune") { + return detune(runtime, propNameId); + } + + throw std::runtime_error("Prop not yet implemented!"); + } + + void OscillatorNodeHostObject::set(jsi::Runtime& runtime, const jsi::PropNameID& propNameId, const jsi::Value& value) { + auto propName = propNameId.utf8(runtime); + + if (propName == "frequency") { + auto frequency = value.asNumber(); + oscillator_->setFrequency(frequency); + return; + } + + if (propName == "detune") { + auto detune = value.asNumber(); + oscillator_->setDetune(detune); + return; + } + + throw std::runtime_error("Not yet implemented!"); + } + + jsi::Value OscillatorNodeHostObject::start(jsi::Runtime& runtime, const jsi::PropNameID& propNameId) { + return jsi::Function::createFromHostFunction(runtime, propNameId, 0, [this](jsi::Runtime& rt, const jsi::Value& thisValue, const jsi::Value* args, size_t count) -> jsi::Value { + oscillator_->start(); + return jsi::Value::undefined(); + }); + } + + jsi::Value OscillatorNodeHostObject::stop(jsi::Runtime &runtime, + const jsi::PropNameID &propNameId) { + return jsi::Function::createFromHostFunction(runtime, propNameId, 0, [this](jsi::Runtime& rt, const jsi::Value& thisValue, const jsi::Value* args, size_t count) -> jsi::Value { + oscillator_->stop(); + return jsi::Value::undefined(); + }); + } + + jsi::Value OscillatorNodeHostObject::frequency(jsi::Runtime &runtime, + const jsi::PropNameID &propNameId) { + return jsi::Function::createFromHostFunction(runtime, propNameId, 0, [this](jsi::Runtime& rt, const jsi::Value& thisValue, const jsi::Value* args, size_t count) -> jsi::Value { + auto frequency = oscillator_->getFrequency(); + return jsi::Value(frequency); + }); + } + + jsi::Value OscillatorNodeHostObject::detune(jsi::Runtime &runtime, + const jsi::PropNameID &propNameId) { + return jsi::Function::createFromHostFunction(runtime, propNameId, 0, [this](jsi::Runtime& rt, const jsi::Value& thisValue, const jsi::Value* args, size_t count) -> jsi::Value { + auto detune = oscillator_->getDetune(); + return jsi::Value(detune); + }); + } +} diff --git a/cpp/OscillatorNodeHostObject.h b/cpp/OscillatorNodeHostObject.h new file mode 100644 index 00000000..9a0822f9 --- /dev/null +++ b/cpp/OscillatorNodeHostObject.h @@ -0,0 +1,29 @@ +#pragma once + +#include <jsi/jsi.h> +#include <fbjni/fbjni.h> +#include <fbjni/detail/Hybrid.h> +#include "OscillatorNode.h" + +namespace audiocontext { + using namespace facebook; + + class OscillatorNode; + + class OscillatorNodeHostObject : public jsi::HostObject { + private: + OscillatorNode* oscillator_; + + public: + explicit OscillatorNodeHostObject(OscillatorNode* oscillator) : oscillator_(oscillator) {} + + jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& name) override; + void set(jsi::Runtime& runtime, const jsi::PropNameID& name, const jsi::Value& value) override; + std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime& rt) override; + + jsi::Value start(jsi::Runtime& runtime, const jsi::PropNameID& propNameId); + jsi::Value stop(jsi::Runtime& runtime, const jsi::PropNameID& propNameId); + jsi::Value frequency(jsi::Runtime& runtime, const jsi::PropNameID& propNameId); + jsi::Value detune(jsi::Runtime& runtime, const jsi::PropNameID& propNameId); + }; +} // namespace audiocontext diff --git a/example/src/App.tsx b/example/src/App.tsx index a17ec8af..e3c9ce43 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,47 +1,55 @@ -import React from 'react'; -import { StyleSheet, View, Text } from 'react-native'; -import JSIExample from '../../src/JSIExample/JSIExample'; +/* eslint-disable react/react-in-jsx-scope */ +import { Button, StyleSheet, Text, View } from 'react-native'; +import { useRef, useEffect } from 'react'; -const App: React.FC = () => { - const multiply = () => { - return JSIExample.multiply(2, 3); +import { AudioContext, type Oscillator } from 'react-native-audio-context'; + +const App = () => { + const audioContextRef = useRef<AudioContext | null>(null); + const oscillatorRef = useRef<Oscillator | null>(null); + const secondaryOscillatorRef = useRef<Oscillator | null>(null); + + useEffect(() => { + audioContextRef.current = new AudioContext(); + oscillatorRef.current = audioContextRef.current.createOscillator(); + secondaryOscillatorRef.current = audioContextRef.current.createOscillator(); + secondaryOscillatorRef.current.frequency = 300; + + return () => { + //TODO + }; + }, []); + + const startOscillator = () => { + oscillatorRef.current?.start(); + secondaryOscillatorRef.current?.start(); + }; + const stopOscillator = () => { + oscillatorRef.current?.stop(); + secondaryOscillatorRef.current?.stop(); }; return ( <View style={styles.container}> - <Text>{multiply()}</Text> + <Text style={styles.title}>React Native Oscillator</Text> + <Button title="Start Oscillator" onPress={startOscillator} /> + <Button title="Stop Oscillator" onPress={stopOscillator} /> </View> ); }; -export default App; - const styles = StyleSheet.create({ container: { flex: 1, - alignItems: 'center', justifyContent: 'center', - paddingHorizontal: 20, - }, - keys: { - fontSize: 14, - color: 'grey', - }, - title: { - fontSize: 16, - color: 'black', - marginRight: 10, - }, - row: { - flexDirection: 'row', alignItems: 'center', + backgroundColor: '#F5FCFF', }, - textInput: { - flex: 1, - marginVertical: 20, - borderWidth: StyleSheet.hairlineWidth, - borderColor: 'black', - borderRadius: 5, - padding: 10, + title: { + fontSize: 20, + textAlign: 'center', + margin: 10, }, }); + +export default App; diff --git a/src/JSIExample/JSIExample.ts b/src/JSIExample/JSIExample.ts deleted file mode 100644 index 8895ad3a..00000000 --- a/src/JSIExample/JSIExample.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { NativeModules, Platform } from 'react-native'; -import type { JSIExampleWrapper } from './types'; - -function verifyExpoGo() { - const ExpoConstants = - NativeModules.NativeUnimoduleProxy?.modulesConstants?.ExponentConstants; - if (ExpoConstants != null) { - if (ExpoConstants.appOwnership === 'expo') { - throw new Error( - 'react-native-fast-crypto is not supported in Expo Go! Use EAS (`expo prebuild`) or eject to a bare workflow instead.' - ); - } else { - throw new Error('\n* Make sure you ran `expo prebuild`.'); - } - } -} - -function getJSIExample() { - const JSIExampleModule = NativeModules.JSIExample; - if (JSIExampleModule == null) { - let message = - 'Failed to install react-native-fast-crypto: The native `JSIExample` Module could not be found.'; - message += - '\n* Make sure react-native-fast-crypto is correctly autolinked (run `npx react-native config` to verify)'; - if (Platform.OS === 'ios' || Platform.OS === 'macos') { - message += '\n* Make sure you ran `pod install` in the ios/ directory.'; - } - if (Platform.OS === 'android') { - message += '\n* Make sure gradle is synced.'; - } - message += '\n* Make sure you rebuilt the app.'; - throw new Error(message); - } - return JSIExampleModule; -} - -function verifyOnDevice(JSIExampleModule: any) { - if (global.nativeCallSyncHook == null || JSIExampleModule.install == null) { - throw new Error( - 'Failed to install react-native-fast-crypto: React Native is not running on-device. JSIExample can only be used when synchronous method invocations (JSI) are possible. If you are using a remote debugger (e.g. Chrome), switch to an on-device debugger (e.g. Flipper) instead.' - ); - } -} - -function installModule(JSIExampleModule: any) { - const result = JSIExampleModule.install(); - if (result !== true) - throw new Error( - `Failed to install react-native-fast-crypto: The native JSIExample Module could not be installed! Looks like something went wrong when installing JSI bindings: ${result}` - ); -} - -function verifyInstallation() { - if (global.__JSIExampleProxy == null) - throw new Error( - 'Failed to install react-native-fast-crypto, the native initializer function does not exist. Are you trying to use JSIExample from different JS Runtimes?' - ); -} - -function createJSIExampleProxy(): JSIExampleWrapper { - if (global.__JSIExampleProxy) { - return global.__JSIExampleProxy; - } - - verifyExpoGo(); - - const JSIExampleModule = getJSIExample(); - - verifyOnDevice(JSIExampleModule); - installModule(JSIExampleModule); - verifyInstallation(); - - if (global.__JSIExampleProxy == null) { - throw new Error('Failed to initialize __JSIExampleProxy.'); - } - - return global.__JSIExampleProxy; -} - -// Call the creator and export what it returns -export default createJSIExampleProxy(); diff --git a/src/JSIExample/types.ts b/src/JSIExample/types.ts deleted file mode 100644 index dac41a93..00000000 --- a/src/JSIExample/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface JSIExampleWrapper { - multiply(a: number, b: number): number; -} - -// global func declaration for JSI functions -declare global { - function nativeCallSyncHook(): unknown; - var __JSIExampleProxy: JSIExampleWrapper | undefined; -} diff --git a/src/index.ts b/src/index.ts index 0942be3c..255b48fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,20 @@ -export * from './JSIExample/JSIExample'; -export * from './JSIExample/types'; +import { NativeModules } from 'react-native'; +const { AudioContextModule } = NativeModules; +import type { Oscillator, BaseAudioContext } from './types'; + +declare global { + function nativeCallSyncHook(): unknown; + var __AudioContextProxy: BaseAudioContext; +} + +export class AudioContext implements BaseAudioContext { + constructor() { + AudioContextModule.initAudioContext(); + } + + createOscillator(): Oscillator { + return global.__AudioContextProxy.createOscillator(); + } +} + +export type { Oscillator }; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..ee3747c1 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,22 @@ +export interface BaseAudioContext { + createOscillator(): Oscillator; +} + +export interface AudioNode { + context: BaseAudioContext; + connect: (destination: AudioNode) => void; + disconnect: () => void; +} + +export interface AudioScheduledSourceNode extends AudioNode { + start: () => void; + stop: () => void; +} + +type WaveType = 'sine' | 'square' | 'sawtooth' | 'triangle'; + +export interface Oscillator extends AudioScheduledSourceNode { + frequency: number; + wave: WaveType; + detune: number; +}