From e602011370927e1f1fd3faccfef719820f8bd8c5 Mon Sep 17 00:00:00 2001 From: Maciej Makowski <maciej.makowski@swmansion.com> Date: Thu, 11 Jul 2024 12:50:24 +0200 Subject: [PATCH 1/9] feat: implemented osicillator kotlin class with hybridclass and host object --- android/CMakeLists.txt | 33 +++++--- android/build.gradle | 5 ++ android/cpp-adapter.cpp | 34 ++++---- android/src/main/cpp/OnLoad.cpp | 12 +++ android/src/main/cpp/Oscillator.cpp | 27 +++++++ android/src/main/cpp/Oscillator.h | 40 +++++++++ ...amplePackage.kt => AudioContextPackage.kt} | 6 +- .../main/java/com/audiocontext/Oscillator.kt | 76 +++++++++++++++++ .../com/audiocontext/context/AudioContext.kt | 43 ++++++++++ .../audiocontext/context/AudioContextState.kt | 7 ++ .../audiocontext/context/BaseAudioContext.kt | 21 +++++ .../com/audiocontext/jsi/JSIExampleModule.kt | 36 --------- .../nativemodules/AudioContextModule.kt | 17 ++++ .../nodes/AudioDestinationNode.kt | 14 ++++ .../java/com/audiocontext/nodes/AudioNode.kt | 25 ++++++ .../nodes/AudioScheduledSourceNode.kt | 8 ++ .../com/audiocontext/nodes/gain/GainNode.kt | 22 +++++ .../nodes/oscillator/OscillatorNode.kt | 69 ++++++++++++++++ .../audiocontext/nodes/oscillator/WaveType.kt | 8 ++ cpp/JSIExampleHostObject.cpp | 45 ----------- cpp/JSIExampleHostObject.h | 25 ------ cpp/OscillatorHostObject.cpp | 38 +++++++++ cpp/OscillatorHostObject.h | 24 ++++++ example/src/App.tsx | 50 +++++------- src/AudioContext.tsx | 76 +++++++++++++++++ src/JSIExample/JSIExample.ts | 81 ------------------- src/JSIExample/types.ts | 9 --- src/index.ts | 2 - src/types.ts | 4 + 29 files changed, 598 insertions(+), 259 deletions(-) create mode 100644 android/src/main/cpp/OnLoad.cpp create mode 100644 android/src/main/cpp/Oscillator.cpp create mode 100644 android/src/main/cpp/Oscillator.h rename android/src/main/java/com/audiocontext/{JSIExamplePackage.kt => AudioContextPackage.kt} (72%) create mode 100644 android/src/main/java/com/audiocontext/Oscillator.kt create mode 100644 android/src/main/java/com/audiocontext/context/AudioContext.kt create mode 100644 android/src/main/java/com/audiocontext/context/AudioContextState.kt create mode 100644 android/src/main/java/com/audiocontext/context/BaseAudioContext.kt delete mode 100644 android/src/main/java/com/audiocontext/jsi/JSIExampleModule.kt create mode 100644 android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt create mode 100644 android/src/main/java/com/audiocontext/nodes/AudioDestinationNode.kt create mode 100644 android/src/main/java/com/audiocontext/nodes/AudioNode.kt create mode 100644 android/src/main/java/com/audiocontext/nodes/AudioScheduledSourceNode.kt create mode 100644 android/src/main/java/com/audiocontext/nodes/gain/GainNode.kt create mode 100644 android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt create mode 100644 android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt delete mode 100644 cpp/JSIExampleHostObject.cpp delete mode 100644 cpp/JSIExampleHostObject.h create mode 100644 cpp/OscillatorHostObject.cpp create mode 100644 cpp/OscillatorHostObject.h create mode 100644 src/AudioContext.tsx delete mode 100644 src/JSIExample/JSIExample.ts delete mode 100644 src/JSIExample/types.ts delete mode 100644 src/index.ts create mode 100644 src/types.ts diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index fcefba66..5bd4fd5f 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -4,22 +4,31 @@ 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/Oscillator + ../cpp/OscillatorHostObject + cpp-adapter.cpp ) 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 index a7b0247d..9446b1a3 100644 --- a/android/cpp-adapter.cpp +++ b/android/cpp-adapter.cpp @@ -1,20 +1,20 @@ -#include <jni.h> -#include <jsi/jsi.h> -#include "JSIExampleHostObject.h" +// #include <jni.h> +// #include <jsi/jsi.h> +// #include "JSIExampleHostObject.h" -using namespace facebook; +// 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)); -} +// 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); - } -} +// 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/OnLoad.cpp b/android/src/main/cpp/OnLoad.cpp new file mode 100644 index 00000000..4ac29070 --- /dev/null +++ b/android/src/main/cpp/OnLoad.cpp @@ -0,0 +1,12 @@ +#include <fbjni/fbjni.h> +#include "Oscillator.h" +#include "OscillatorHostObject.h" + +using namespace audiocontext; + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) +{ + return facebook::jni::initialize(vm, [] { + Oscillator::registerNatives(); + }); +} diff --git a/android/src/main/cpp/Oscillator.cpp b/android/src/main/cpp/Oscillator.cpp new file mode 100644 index 00000000..187829b7 --- /dev/null +++ b/android/src/main/cpp/Oscillator.cpp @@ -0,0 +1,27 @@ +#include "Oscillator.h" +#include <fbjni/fbjni.h> +#include <jsi/jsi.h> + +namespace audiocontext { + + using namespace facebook::jni; + + Oscillator::Oscillator(const jni::alias_ref<Oscillator::jhybridobject> &jThis, + jlong jsContext): javaObject_(make_global(jThis)) { + auto runtime = reinterpret_cast<jsi::Runtime *>(jsContext); + auto hostObject = std::make_shared<OscillatorHostObject>(this); + auto object = jsi::Object::createFromHostObject(*runtime, hostObject); + runtime->global().setProperty(*runtime, "__OscillatorProxy", std::move(object)); + } + + void Oscillator::start() { + static const auto method = javaClassStatic()->getMethod<void()>("start"); + method(javaObject_.get()); + } + + void Oscillator::stop() { + static const auto method = javaClassStatic()->getMethod<void()>("stop"); + method(javaObject_.get()); + } + +} // namespace audiocontext diff --git a/android/src/main/cpp/Oscillator.h b/android/src/main/cpp/Oscillator.h new file mode 100644 index 00000000..6eb7ee62 --- /dev/null +++ b/android/src/main/cpp/Oscillator.h @@ -0,0 +1,40 @@ +#pragma once + +#include <fbjni/fbjni.h> +#include <jsi/jsi.h> +#include <react/jni/CxxModuleWrapper.h> +#include <react/jni/JMessageQueueThread.h> +#include "OscillatorHostObject.h" + +namespace audiocontext { + + using namespace facebook; + using namespace facebook::jni; + + class Oscillator : public jni::HybridClass<Oscillator> { + public: + static auto constexpr kJavaDescriptor = "Lcom/audiocontext/Oscillator;"; + + static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis, jlong jsContext) + { + return makeCxxInstance(jThis, jsContext); + } + + static void registerNatives() { + javaClassStatic()->registerNatives({ + makeNativeMethod("initHybrid", Oscillator::initHybrid), + }); + } + + void start(); + void stop(); + + private: + friend HybridBase; + + global_ref<Oscillator::javaobject> javaObject_; + + explicit Oscillator(const jni::alias_ref<Oscillator::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/Oscillator.kt b/android/src/main/java/com/audiocontext/Oscillator.kt new file mode 100644 index 00000000..0a06fd2a --- /dev/null +++ b/android/src/main/java/com/audiocontext/Oscillator.kt @@ -0,0 +1,76 @@ +package com.audiocontext + +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioTrack +import com.audiocontext.nodes.oscillator.WaveType +import com.facebook.jni.HybridData +import com.facebook.react.bridge.ReactApplicationContext +import kotlin.math.abs +import kotlin.math.floor +import kotlin.math.sin + +class Oscillator(reactContext: ReactApplicationContext) { + val numberOfInputs: Int = 0 + 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?; + + init { + mHybridData = initHybrid(reactContext.javaScriptContextHolder!!.get()) + + val bufferSize = AudioTrack.getMinBufferSize( + 44100, + AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT) + this.audioTrack = AudioTrack( + AudioManager.STREAM_MUSIC, 44100, AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM + ) + } + + external fun initHybrid(l: Long): HybridData? + + fun start() { + if(isPlaying) return + isPlaying = true + audioTrack.play() + playbackThread = Thread { generateSound() }.apply{ start()} + } + + 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) / 44100 + + for(i in buffer.indices) { + buffer[i] = when(waveType) { + WaveType.SINE -> (sin(wavePhase) * Short.MAX_VALUE).toInt().toShort() + WaveType.SQUARE -> ((if (sin(wavePhase) >= 0) 1 else -1) * Short.MAX_VALUE).toShort() + WaveType.SAWTOOTH -> ((2 * (wavePhase / (2 * Math.PI) - floor(wavePhase / (2 * Math.PI) + 0.5))) * Short.MAX_VALUE).toInt().toShort() + WaveType.TRIANGLE -> ((2 * abs(2 * (wavePhase / (2 * Math.PI) - floor(wavePhase / (2 * Math.PI) + 0.5))) - 1) * Short.MAX_VALUE).toInt().toShort() + } + wavePhase += phaseChange + } + + audioTrack.write(buffer, 0, buffer.size) + } + audioTrack.flush() + } +} 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..f17c2273 --- /dev/null +++ b/android/src/main/java/com/audiocontext/context/AudioContext.kt @@ -0,0 +1,43 @@ +package com.audiocontext.context + +import android.media.AudioTrack +import com.audiocontext.nodes.AudioDestinationNode +import com.audiocontext.nodes.AudioNode +import com.audiocontext.nodes.gain.GainNode +import com.audiocontext.nodes.oscillator.OscillatorNode +import java.util.concurrent.CopyOnWriteArrayList + +class AudioContext : BaseAudioContext { + override var sampleRate: Int = 44100 + override val destination: AudioDestinationNode = AudioDestinationNode(this) + override val sources = CopyOnWriteArrayList<AudioNode>() + + override fun addNode(node: AudioNode) { + sources.add(node) + } + + override fun removeNode(node: AudioNode) { + sources.remove(node) + } + + override fun createOscillatorNode(): OscillatorNode { + val oscillatorNode = OscillatorNode(this) + addNode(oscillatorNode) + return oscillatorNode + } + + override fun createGainNode(): GainNode { + val gainNode = GainNode(this) + return gainNode + } + + override fun dispatchAudio(buffer: ShortArray, audioTrack: AudioTrack) { + val currentBuffer = buffer.clone() + + synchronized(sources) { + sources.forEach { source -> + source.process(currentBuffer, audioTrack) + } + } + } +} diff --git a/android/src/main/java/com/audiocontext/context/AudioContextState.kt b/android/src/main/java/com/audiocontext/context/AudioContextState.kt new file mode 100644 index 00000000..d10683a0 --- /dev/null +++ b/android/src/main/java/com/audiocontext/context/AudioContextState.kt @@ -0,0 +1,7 @@ +package com.audiocontext.context + +enum class AudioContextState { + SUSPENDED, + RUNNING, + CLOSED +} 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..5ab91e1a --- /dev/null +++ b/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt @@ -0,0 +1,21 @@ +package com.audiocontext.context + +import android.media.AudioTrack +import android.provider.MediaStore.Audio +import com.audiocontext.nodes.AudioDestinationNode +import com.audiocontext.nodes.AudioNode +import com.audiocontext.nodes.gain.GainNode +import com.audiocontext.nodes.oscillator.OscillatorNode +import java.util.concurrent.CopyOnWriteArrayList + +interface BaseAudioContext { + val sampleRate: Int + val destination: AudioDestinationNode + val sources: List<AudioNode> + + fun createOscillatorNode(): OscillatorNode + fun createGainNode(): GainNode + fun dispatchAudio(buffer: ShortArray, audioTrack: AudioTrack) + fun addNode(node: AudioNode) + fun removeNode(node: AudioNode) +} 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..ab084076 --- /dev/null +++ b/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt @@ -0,0 +1,17 @@ +package com.audiocontext.nativemodules + +import com.audiocontext.Oscillator +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 createOscillatorNode() { + Oscillator(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/gain/GainNode.kt b/android/src/main/java/com/audiocontext/nodes/gain/GainNode.kt new file mode 100644 index 00000000..c76ce424 --- /dev/null +++ b/android/src/main/java/com/audiocontext/nodes/gain/GainNode.kt @@ -0,0 +1,22 @@ +package com.audiocontext.nodes.gain + +import android.media.AudioTrack +import com.audiocontext.context.BaseAudioContext +import com.audiocontext.nodes.AudioNode + +class GainNode(context: BaseAudioContext): AudioNode(context) { + override val numberOfInputs = 1 + override val numberOfOutputs = 1 + var gain: Double = 1.0 + get() = field + set(value) { + field = value + if (field < 0) field = 0.0 + if (field > 1) field = 1.0 + } + + override fun process(buffer: ShortArray, audioTrack: AudioTrack) { + audioTrack.setVolume(gain.toFloat()) + super.process(buffer, audioTrack) + } +} 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..aea07ee6 --- /dev/null +++ b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt @@ -0,0 +1,69 @@ +package com.audiocontext.nodes.oscillator + +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioTrack +import com.audiocontext.context.BaseAudioContext +import com.audiocontext.nodes.AudioScheduledSourceNode +import kotlin.math.abs +import kotlin.math.floor +import kotlin.math.sin + +class OscillatorNode(context: BaseAudioContext) : 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) + + init { + 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 + ) + } + + 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 -> (sin(wavePhase) * Short.MAX_VALUE).toInt().toShort() + WaveType.SQUARE -> ((if (sin(wavePhase) >= 0) 1 else -1) * Short.MAX_VALUE).toShort() + WaveType.SAWTOOTH -> ((2 * (wavePhase / (2 * Math.PI) - floor(wavePhase / (2 * Math.PI) + 0.5))) * Short.MAX_VALUE).toInt().toShort() + WaveType.TRIANGLE -> ((2 * abs(2 * (wavePhase / (2 * Math.PI) - floor(wavePhase / (2 * Math.PI) + 0.5))) - 1) * Short.MAX_VALUE).toInt().toShort() + } + wavePhase += phaseChange + } + + context.dispatchAudio(buffer, audioTrack) + } + audioTrack.flush() + } +} 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..ffd63ff2 --- /dev/null +++ b/android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt @@ -0,0 +1,8 @@ +package com.audiocontext.nodes.oscillator + +enum class WaveType { + SINE, + SQUARE, + SAWTOOTH, + TRIANGLE +} diff --git a/cpp/JSIExampleHostObject.cpp b/cpp/JSIExampleHostObject.cpp deleted file mode 100644 index 1f28c9e6..00000000 --- a/cpp/JSIExampleHostObject.cpp +++ /dev/null @@ -1,45 +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, "helloWorld")); - return propertyNames; - } - - jsi::Value JSIExampleHostObject::get(jsi::Runtime &runtime, const jsi::PropNameID &propNameId) - { - auto propName = propNameId.utf8(runtime); - - if (propName == "helloWorld") - { - return jsi::Function::createFromHostFunction(runtime, propNameId, 0, - [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) - { - return this->helloWorld(rt); - }); - } - - 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 == "helloWorld") - { - // Do nothing - return; - } - throw std::runtime_error("Not yet implemented!"); - } - - jsi::Value JSIExampleHostObject::helloWorld(jsi::Runtime &runtime) - { - return jsi::String::createFromUtf8(runtime, "Hello World using jsi::HostObject!"); - } -} diff --git a/cpp/JSIExampleHostObject.h b/cpp/JSIExampleHostObject.h deleted file mode 100644 index 3ec12423..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 helloWorld(jsi::Runtime &); - }; - -} // namespace margelo - -#endif /* JSIEXAMPLEHOSTOBJECT_H */ diff --git a/cpp/OscillatorHostObject.cpp b/cpp/OscillatorHostObject.cpp new file mode 100644 index 00000000..80c14f5c --- /dev/null +++ b/cpp/OscillatorHostObject.cpp @@ -0,0 +1,38 @@ +#include "OscillatorHostObject.h" + +namespace audiocontext { + using namespace facebook; + + std::vector<jsi::PropNameID> OscillatorHostObject::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 OscillatorHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propNameId) { + auto propName = propNameId.utf8(runtime); + + if (propName == "start") { + 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(); + }); + } + + if (propName == "stop") { + 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(); + }); + } + + throw std::runtime_error("Prop not yet implemented!"); + } + + void OscillatorHostObject::set(jsi::Runtime& runtime, const jsi::PropNameID& propNameId, const jsi::Value& value) { + auto propName = propNameId.utf8(runtime); + + throw std::runtime_error("Not yet implemented!"); + } +} diff --git a/cpp/OscillatorHostObject.h b/cpp/OscillatorHostObject.h new file mode 100644 index 00000000..9a8d512e --- /dev/null +++ b/cpp/OscillatorHostObject.h @@ -0,0 +1,24 @@ +#pragma once + +#include <jsi/jsi.h> +#include <fbjni/fbjni.h> +#include <fbjni/detail/Hybrid.h> +#include "Oscillator.h" + +namespace audiocontext { + using namespace facebook; + + class Oscillator; + + class OscillatorHostObject : public jsi::HostObject { + private: + Oscillator* oscillator_; + + public: + explicit OscillatorHostObject(Oscillator* 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; + }; +} // namespace audiocontext diff --git a/example/src/App.tsx b/example/src/App.tsx index 479543b5..636988d8 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,45 +1,37 @@ -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 { Oscillator } from '../../src/jsiOscillator'; -export default function App() { - const sayHello = () => { - //JSIExample.helloWorld = 'Hello World'; - return JSIExample.helloWorld(); +const App = () => { + const startOscillator = () => { + //Oscillator.start(); + }; + + const stopOscillator = () => { + //Oscillator.stop(); }; return ( <View style={styles.container}> - <Text>{sayHello()}</Text> + <Text style={styles.title}>React Native Oscillator</Text> + <Button title="Start Oscillator" onPress={startOscillator} /> + <Button title="Stop Oscillator" onPress={stopOscillator} /> </View> ); -} +}; 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/AudioContext.tsx b/src/AudioContext.tsx new file mode 100644 index 00000000..c888ec9a --- /dev/null +++ b/src/AudioContext.tsx @@ -0,0 +1,76 @@ +import { NativeModules } from 'react-native'; +const { AudioContextModule } = NativeModules; + +interface AudioScheduleSourceNode extends AudioNode { + context: AudioContext; + start(): void; + stop(): void; +} + +interface AudioNode { + connect(): void; + disconnect(): void; +} + +export class AudioContext { + private source: AudioScheduleSourceNode | null = null; + private gain: GainNode | null = null; + + constructor() {} + + public createOscillator() { + AudioContextModule.createOscillatorNode(); + this.source = new Oscillator(this); + return this.source; + } + + public createGain() { + AudioContextModule.createGainNode(); + this.gain = new GainNode(); + return this.gain; + } +} + +export class Oscillator implements AudioScheduleSourceNode { + context: AudioContext; + + constructor(context: AudioContext) { + this.context = context; + } + + public start() { + AudioContextModule.start(); + } + + public stop() { + AudioContextModule.stop(); + } + + public connect() { + AudioContextModule.connect(); + } + + public disconnect() { + AudioContextModule.disconnect(); + } +} + +export class GainNode implements AudioNode { + gain: number = AudioContextModule.getVolume(); + + public connect() { + AudioContextModule.connect(); + } + + public disconnect() { + AudioContextModule.disconnect(); + } + + public getGain() { + return AudioContextModule.getVolume(); + } + + public setGain(gain: number) { + AudioContextModule.setVolume(gain); + } +} 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 f891d6ca..00000000 --- a/src/JSIExample/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface JSIExampleWrapper { - helloWorld(): string; -} - -// 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 deleted file mode 100644 index 0942be3c..00000000 --- a/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './JSIExample/JSIExample'; -export * from './JSIExample/types'; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..43a2d4fe --- /dev/null +++ b/src/types.ts @@ -0,0 +1,4 @@ +export interface JSIExampleWrapper { + start(): void; + stop(): void; +} From c2b37c06f85e603f635f4f9bccbaed32960995ea Mon Sep 17 00:00:00 2001 From: Maciej Makowski <maciej.makowski@swmansion.com> Date: Thu, 11 Jul 2024 15:49:48 +0200 Subject: [PATCH 2/9] feat: implemented working oscillator node using jni and jsi --- android/CMakeLists.txt | 2 +- android/cpp-adapter.cpp | 20 ----- android/src/main/cpp/Oscillator.cpp | 2 +- android/src/main/cpp/Oscillator.h | 10 +-- .../main/java/com/audiocontext/Oscillator.kt | 6 ++ .../nativemodules/AudioContextModule.kt | 2 +- example/src/App.tsx | 10 ++- src/AudioContext.ts | 8 ++ src/AudioContext.tsx | 76 ------------------- src/Oscillator.ts | 11 +++ src/index.ts | 2 + 11 files changed, 42 insertions(+), 107 deletions(-) delete mode 100644 android/cpp-adapter.cpp create mode 100644 src/AudioContext.ts delete mode 100644 src/AudioContext.tsx create mode 100644 src/Oscillator.ts create mode 100644 src/index.ts diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index 5bd4fd5f..49122d66 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -16,9 +16,9 @@ include_directories( ) add_library(react-native-audio-context SHARED + src/main/cpp/OnLoad.cpp src/main/cpp/Oscillator ../cpp/OscillatorHostObject - cpp-adapter.cpp ) find_package(ReactAndroid REQUIRED CONFIG) diff --git a/android/cpp-adapter.cpp b/android/cpp-adapter.cpp deleted file mode 100644 index 9446b1a3..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/Oscillator.cpp b/android/src/main/cpp/Oscillator.cpp index 187829b7..12ff1a00 100644 --- a/android/src/main/cpp/Oscillator.cpp +++ b/android/src/main/cpp/Oscillator.cpp @@ -6,7 +6,7 @@ namespace audiocontext { using namespace facebook::jni; - Oscillator::Oscillator(const jni::alias_ref<Oscillator::jhybridobject> &jThis, + Oscillator::Oscillator(jni::alias_ref<Oscillator::jhybridobject> &jThis, jlong jsContext): javaObject_(make_global(jThis)) { auto runtime = reinterpret_cast<jsi::Runtime *>(jsContext); auto hostObject = std::make_shared<OscillatorHostObject>(this); diff --git a/android/src/main/cpp/Oscillator.h b/android/src/main/cpp/Oscillator.h index 6eb7ee62..529890ad 100644 --- a/android/src/main/cpp/Oscillator.h +++ b/android/src/main/cpp/Oscillator.h @@ -15,15 +15,15 @@ namespace audiocontext { public: static auto constexpr kJavaDescriptor = "Lcom/audiocontext/Oscillator;"; - static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis, jlong jsContext) + static jni::local_ref<Oscillator::jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis, jlong jsContext) { return makeCxxInstance(jThis, jsContext); } static void registerNatives() { - javaClassStatic()->registerNatives({ - makeNativeMethod("initHybrid", Oscillator::initHybrid), - }); + registerHybrid({ + makeNativeMethod("initHybrid", Oscillator::initHybrid), + }); } void start(); @@ -34,7 +34,7 @@ namespace audiocontext { global_ref<Oscillator::javaobject> javaObject_; - explicit Oscillator(const jni::alias_ref<Oscillator::jhybridobject>& jThis, jlong jsContext); + explicit Oscillator(jni::alias_ref<Oscillator::jhybridobject>& jThis, jlong jsContext); }; } // namespace audiocontext diff --git a/android/src/main/java/com/audiocontext/Oscillator.kt b/android/src/main/java/com/audiocontext/Oscillator.kt index 0a06fd2a..01469ca8 100644 --- a/android/src/main/java/com/audiocontext/Oscillator.kt +++ b/android/src/main/java/com/audiocontext/Oscillator.kt @@ -24,6 +24,12 @@ class Oscillator(reactContext: ReactApplicationContext) { private val mHybridData: HybridData?; + companion object { + init { + System.loadLibrary("react-native-audio-context") + } + } + init { mHybridData = initHybrid(reactContext.javaScriptContextHolder!!.get()) diff --git a/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt b/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt index ab084076..68c75f86 100644 --- a/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt +++ b/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt @@ -11,7 +11,7 @@ class AudioContextModule(private val reactContext: ReactApplicationContext) : Re } @ReactMethod(isBlockingSynchronousMethod = true) - fun createOscillatorNode() { + fun createOscillator() { Oscillator(reactContext) } } diff --git a/example/src/App.tsx b/example/src/App.tsx index 636988d8..362335d9 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,14 +1,18 @@ /* eslint-disable react/react-in-jsx-scope */ import { Button, StyleSheet, Text, View } from 'react-native'; -//import { Oscillator } from '../../src/jsiOscillator'; +import { AudioContext } from 'react-native-audio-context'; +import type { OscillatorWrapper } from 'react-native-audio-context'; const App = () => { + AudioContext.createOscillator(); + const oscillator = global.__OscillatorProxy as OscillatorWrapper; + const startOscillator = () => { - //Oscillator.start(); + oscillator.start(); }; const stopOscillator = () => { - //Oscillator.stop(); + oscillator.stop(); }; return ( diff --git a/src/AudioContext.ts b/src/AudioContext.ts new file mode 100644 index 00000000..038ddf73 --- /dev/null +++ b/src/AudioContext.ts @@ -0,0 +1,8 @@ +import { NativeModules } from 'react-native'; +const { AudioContextModule } = NativeModules; + +interface AudioContextInterface { + createOscillator(): void; +} + +export const AudioContext = AudioContextModule as AudioContextInterface; diff --git a/src/AudioContext.tsx b/src/AudioContext.tsx deleted file mode 100644 index c888ec9a..00000000 --- a/src/AudioContext.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { NativeModules } from 'react-native'; -const { AudioContextModule } = NativeModules; - -interface AudioScheduleSourceNode extends AudioNode { - context: AudioContext; - start(): void; - stop(): void; -} - -interface AudioNode { - connect(): void; - disconnect(): void; -} - -export class AudioContext { - private source: AudioScheduleSourceNode | null = null; - private gain: GainNode | null = null; - - constructor() {} - - public createOscillator() { - AudioContextModule.createOscillatorNode(); - this.source = new Oscillator(this); - return this.source; - } - - public createGain() { - AudioContextModule.createGainNode(); - this.gain = new GainNode(); - return this.gain; - } -} - -export class Oscillator implements AudioScheduleSourceNode { - context: AudioContext; - - constructor(context: AudioContext) { - this.context = context; - } - - public start() { - AudioContextModule.start(); - } - - public stop() { - AudioContextModule.stop(); - } - - public connect() { - AudioContextModule.connect(); - } - - public disconnect() { - AudioContextModule.disconnect(); - } -} - -export class GainNode implements AudioNode { - gain: number = AudioContextModule.getVolume(); - - public connect() { - AudioContextModule.connect(); - } - - public disconnect() { - AudioContextModule.disconnect(); - } - - public getGain() { - return AudioContextModule.getVolume(); - } - - public setGain(gain: number) { - AudioContextModule.setVolume(gain); - } -} diff --git a/src/Oscillator.ts b/src/Oscillator.ts new file mode 100644 index 00000000..1d72483f --- /dev/null +++ b/src/Oscillator.ts @@ -0,0 +1,11 @@ +export interface OscillatorWrapper { + start(): undefined; + stop(): undefined; +} + +declare global { + function nativeCallSyncHook(): unknown; + var __OscillatorProxy: OscillatorWrapper | undefined; +} + +export const Oscillator = global.__OscillatorProxy as OscillatorWrapper; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..cfe45d34 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export * from './Oscillator'; +export * from './AudioContext'; From b8d17acbfa755af227f71ba25731050ceb7312ae Mon Sep 17 00:00:00 2001 From: Maciej Makowski <maciej.makowski@swmansion.com> Date: Fri, 12 Jul 2024 09:03:11 +0200 Subject: [PATCH 3/9] fix: remove unused files --- .../{nodes/oscillator => }/WaveType.kt | 0 .../com/audiocontext/context/AudioContext.kt | 43 ------------ .../audiocontext/context/AudioContextState.kt | 7 -- .../audiocontext/context/BaseAudioContext.kt | 21 ------ .../nodes/AudioDestinationNode.kt | 14 ---- .../java/com/audiocontext/nodes/AudioNode.kt | 25 ------- .../nodes/AudioScheduledSourceNode.kt | 8 --- .../com/audiocontext/nodes/gain/GainNode.kt | 22 ------ .../nodes/oscillator/OscillatorNode.kt | 69 ------------------- 9 files changed, 209 deletions(-) rename android/src/main/java/com/audiocontext/{nodes/oscillator => }/WaveType.kt (100%) delete mode 100644 android/src/main/java/com/audiocontext/context/AudioContext.kt delete mode 100644 android/src/main/java/com/audiocontext/context/AudioContextState.kt delete mode 100644 android/src/main/java/com/audiocontext/context/BaseAudioContext.kt delete mode 100644 android/src/main/java/com/audiocontext/nodes/AudioDestinationNode.kt delete mode 100644 android/src/main/java/com/audiocontext/nodes/AudioNode.kt delete mode 100644 android/src/main/java/com/audiocontext/nodes/AudioScheduledSourceNode.kt delete mode 100644 android/src/main/java/com/audiocontext/nodes/gain/GainNode.kt delete mode 100644 android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt diff --git a/android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt b/android/src/main/java/com/audiocontext/WaveType.kt similarity index 100% rename from android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt rename to android/src/main/java/com/audiocontext/WaveType.kt diff --git a/android/src/main/java/com/audiocontext/context/AudioContext.kt b/android/src/main/java/com/audiocontext/context/AudioContext.kt deleted file mode 100644 index f17c2273..00000000 --- a/android/src/main/java/com/audiocontext/context/AudioContext.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.audiocontext.context - -import android.media.AudioTrack -import com.audiocontext.nodes.AudioDestinationNode -import com.audiocontext.nodes.AudioNode -import com.audiocontext.nodes.gain.GainNode -import com.audiocontext.nodes.oscillator.OscillatorNode -import java.util.concurrent.CopyOnWriteArrayList - -class AudioContext : BaseAudioContext { - override var sampleRate: Int = 44100 - override val destination: AudioDestinationNode = AudioDestinationNode(this) - override val sources = CopyOnWriteArrayList<AudioNode>() - - override fun addNode(node: AudioNode) { - sources.add(node) - } - - override fun removeNode(node: AudioNode) { - sources.remove(node) - } - - override fun createOscillatorNode(): OscillatorNode { - val oscillatorNode = OscillatorNode(this) - addNode(oscillatorNode) - return oscillatorNode - } - - override fun createGainNode(): GainNode { - val gainNode = GainNode(this) - return gainNode - } - - override fun dispatchAudio(buffer: ShortArray, audioTrack: AudioTrack) { - val currentBuffer = buffer.clone() - - synchronized(sources) { - sources.forEach { source -> - source.process(currentBuffer, audioTrack) - } - } - } -} diff --git a/android/src/main/java/com/audiocontext/context/AudioContextState.kt b/android/src/main/java/com/audiocontext/context/AudioContextState.kt deleted file mode 100644 index d10683a0..00000000 --- a/android/src/main/java/com/audiocontext/context/AudioContextState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.audiocontext.context - -enum class AudioContextState { - SUSPENDED, - RUNNING, - CLOSED -} diff --git a/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt b/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt deleted file mode 100644 index 5ab91e1a..00000000 --- a/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.audiocontext.context - -import android.media.AudioTrack -import android.provider.MediaStore.Audio -import com.audiocontext.nodes.AudioDestinationNode -import com.audiocontext.nodes.AudioNode -import com.audiocontext.nodes.gain.GainNode -import com.audiocontext.nodes.oscillator.OscillatorNode -import java.util.concurrent.CopyOnWriteArrayList - -interface BaseAudioContext { - val sampleRate: Int - val destination: AudioDestinationNode - val sources: List<AudioNode> - - fun createOscillatorNode(): OscillatorNode - fun createGainNode(): GainNode - fun dispatchAudio(buffer: ShortArray, audioTrack: AudioTrack) - fun addNode(node: AudioNode) - fun removeNode(node: AudioNode) -} diff --git a/android/src/main/java/com/audiocontext/nodes/AudioDestinationNode.kt b/android/src/main/java/com/audiocontext/nodes/AudioDestinationNode.kt deleted file mode 100644 index 96c121a0..00000000 --- a/android/src/main/java/com/audiocontext/nodes/AudioDestinationNode.kt +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 9c68beaa..00000000 --- a/android/src/main/java/com/audiocontext/nodes/AudioNode.kt +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index b2fcc5d6..00000000 --- a/android/src/main/java/com/audiocontext/nodes/AudioScheduledSourceNode.kt +++ /dev/null @@ -1,8 +0,0 @@ -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/gain/GainNode.kt b/android/src/main/java/com/audiocontext/nodes/gain/GainNode.kt deleted file mode 100644 index c76ce424..00000000 --- a/android/src/main/java/com/audiocontext/nodes/gain/GainNode.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.audiocontext.nodes.gain - -import android.media.AudioTrack -import com.audiocontext.context.BaseAudioContext -import com.audiocontext.nodes.AudioNode - -class GainNode(context: BaseAudioContext): AudioNode(context) { - override val numberOfInputs = 1 - override val numberOfOutputs = 1 - var gain: Double = 1.0 - get() = field - set(value) { - field = value - if (field < 0) field = 0.0 - if (field > 1) field = 1.0 - } - - override fun process(buffer: ShortArray, audioTrack: AudioTrack) { - audioTrack.setVolume(gain.toFloat()) - super.process(buffer, audioTrack) - } -} diff --git a/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt deleted file mode 100644 index aea07ee6..00000000 --- a/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.audiocontext.nodes.oscillator - -import android.media.AudioFormat -import android.media.AudioManager -import android.media.AudioTrack -import com.audiocontext.context.BaseAudioContext -import com.audiocontext.nodes.AudioScheduledSourceNode -import kotlin.math.abs -import kotlin.math.floor -import kotlin.math.sin - -class OscillatorNode(context: BaseAudioContext) : 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) - - init { - 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 - ) - } - - 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 -> (sin(wavePhase) * Short.MAX_VALUE).toInt().toShort() - WaveType.SQUARE -> ((if (sin(wavePhase) >= 0) 1 else -1) * Short.MAX_VALUE).toShort() - WaveType.SAWTOOTH -> ((2 * (wavePhase / (2 * Math.PI) - floor(wavePhase / (2 * Math.PI) + 0.5))) * Short.MAX_VALUE).toInt().toShort() - WaveType.TRIANGLE -> ((2 * abs(2 * (wavePhase / (2 * Math.PI) - floor(wavePhase / (2 * Math.PI) + 0.5))) - 1) * Short.MAX_VALUE).toInt().toShort() - } - wavePhase += phaseChange - } - - context.dispatchAudio(buffer, audioTrack) - } - audioTrack.flush() - } -} From 0f002fdff01b1cb2a85190e81249ed94a6b1df13 Mon Sep 17 00:00:00 2001 From: Maciej Makowski <maciej.makowski@swmansion.com> Date: Fri, 12 Jul 2024 09:53:03 +0200 Subject: [PATCH 4/9] feat: integrate AudioContext class with OscillatorNode on native side --- android/CMakeLists.txt | 4 +- android/src/main/cpp/OnLoad.cpp | 6 +- .../{Oscillator.cpp => OscillatorNode.cpp} | 12 +-- .../cpp/{Oscillator.h => OscillatorNode.h} | 14 ++-- .../com/audiocontext/context/AudioContext.kt | 34 ++++++++ .../audiocontext/context/BaseAudioContext.kt | 15 ++++ .../nativemodules/AudioContextModule.kt | 9 +- .../nodes/AudioDestinationNode.kt | 14 ++++ .../java/com/audiocontext/nodes/AudioNode.kt | 25 ++++++ .../nodes/AudioScheduledSourceNode.kt | 8 ++ .../nodes/oscillator/OscillatorNode.kt | 83 +++++++++++++++++++ .../{ => nodes/oscillator}/WaveType.kt | 0 ...bject.cpp => OscillatorNodeHostObject.cpp} | 8 +- ...ostObject.h => OscillatorNodeHostObject.h} | 10 +-- example/src/App.tsx | 2 +- src/Oscillator.ts | 4 +- 16 files changed, 216 insertions(+), 32 deletions(-) rename android/src/main/cpp/{Oscillator.cpp => OscillatorNode.cpp} (63%) rename android/src/main/cpp/{Oscillator.h => OscillatorNode.h} (54%) create mode 100644 android/src/main/java/com/audiocontext/context/AudioContext.kt create mode 100644 android/src/main/java/com/audiocontext/context/BaseAudioContext.kt create mode 100644 android/src/main/java/com/audiocontext/nodes/AudioDestinationNode.kt create mode 100644 android/src/main/java/com/audiocontext/nodes/AudioNode.kt create mode 100644 android/src/main/java/com/audiocontext/nodes/AudioScheduledSourceNode.kt create mode 100644 android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt rename android/src/main/java/com/audiocontext/{ => nodes/oscillator}/WaveType.kt (100%) rename cpp/{OscillatorHostObject.cpp => OscillatorNodeHostObject.cpp} (76%) rename cpp/{OscillatorHostObject.h => OscillatorNodeHostObject.h} (65%) diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index 49122d66..2e1e6dd8 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -17,8 +17,8 @@ include_directories( add_library(react-native-audio-context SHARED src/main/cpp/OnLoad.cpp - src/main/cpp/Oscillator - ../cpp/OscillatorHostObject + src/main/cpp/OscillatorNode + ../cpp/OscillatorNodeHostObject ) find_package(ReactAndroid REQUIRED CONFIG) diff --git a/android/src/main/cpp/OnLoad.cpp b/android/src/main/cpp/OnLoad.cpp index 4ac29070..8c9184c3 100644 --- a/android/src/main/cpp/OnLoad.cpp +++ b/android/src/main/cpp/OnLoad.cpp @@ -1,12 +1,12 @@ #include <fbjni/fbjni.h> -#include "Oscillator.h" -#include "OscillatorHostObject.h" +#include "OscillatorNode.h" +#include "OscillatorNodeHostObject.h" using namespace audiocontext; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { return facebook::jni::initialize(vm, [] { - Oscillator::registerNatives(); + OscillatorNode::registerNatives(); }); } diff --git a/android/src/main/cpp/Oscillator.cpp b/android/src/main/cpp/OscillatorNode.cpp similarity index 63% rename from android/src/main/cpp/Oscillator.cpp rename to android/src/main/cpp/OscillatorNode.cpp index 12ff1a00..49877153 100644 --- a/android/src/main/cpp/Oscillator.cpp +++ b/android/src/main/cpp/OscillatorNode.cpp @@ -1,4 +1,4 @@ -#include "Oscillator.h" +#include "OscillatorNode.h" #include <fbjni/fbjni.h> #include <jsi/jsi.h> @@ -6,20 +6,20 @@ namespace audiocontext { using namespace facebook::jni; - Oscillator::Oscillator(jni::alias_ref<Oscillator::jhybridobject> &jThis, + OscillatorNode::OscillatorNode(jni::alias_ref<OscillatorNode::jhybridobject> &jThis, jlong jsContext): javaObject_(make_global(jThis)) { auto runtime = reinterpret_cast<jsi::Runtime *>(jsContext); - auto hostObject = std::make_shared<OscillatorHostObject>(this); + auto hostObject = std::make_shared<OscillatorNodeHostObject>(this); auto object = jsi::Object::createFromHostObject(*runtime, hostObject); - runtime->global().setProperty(*runtime, "__OscillatorProxy", std::move(object)); + runtime->global().setProperty(*runtime, "__OscillatorNodeProxy", std::move(object)); } - void Oscillator::start() { + void OscillatorNode::start() { static const auto method = javaClassStatic()->getMethod<void()>("start"); method(javaObject_.get()); } - void Oscillator::stop() { + void OscillatorNode::stop() { static const auto method = javaClassStatic()->getMethod<void()>("stop"); method(javaObject_.get()); } diff --git a/android/src/main/cpp/Oscillator.h b/android/src/main/cpp/OscillatorNode.h similarity index 54% rename from android/src/main/cpp/Oscillator.h rename to android/src/main/cpp/OscillatorNode.h index 529890ad..5c0d669f 100644 --- a/android/src/main/cpp/Oscillator.h +++ b/android/src/main/cpp/OscillatorNode.h @@ -4,25 +4,25 @@ #include <jsi/jsi.h> #include <react/jni/CxxModuleWrapper.h> #include <react/jni/JMessageQueueThread.h> -#include "OscillatorHostObject.h" +#include "OscillatorNodeHostObject.h" namespace audiocontext { using namespace facebook; using namespace facebook::jni; - class Oscillator : public jni::HybridClass<Oscillator> { + class OscillatorNode : public jni::HybridClass<OscillatorNode> { public: - static auto constexpr kJavaDescriptor = "Lcom/audiocontext/Oscillator;"; + static auto constexpr kJavaDescriptor = "Lcom/audiocontext/nodes/oscillator/OscillatorNode;"; - static jni::local_ref<Oscillator::jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis, jlong jsContext) + static jni::local_ref<OscillatorNode::jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis, jlong jsContext) { return makeCxxInstance(jThis, jsContext); } static void registerNatives() { registerHybrid({ - makeNativeMethod("initHybrid", Oscillator::initHybrid), + makeNativeMethod("initHybrid", OscillatorNode::initHybrid), }); } @@ -32,9 +32,9 @@ namespace audiocontext { private: friend HybridBase; - global_ref<Oscillator::javaobject> javaObject_; + global_ref<OscillatorNode::javaobject> javaObject_; - explicit Oscillator(jni::alias_ref<Oscillator::jhybridobject>& jThis, jlong jsContext); + explicit OscillatorNode(jni::alias_ref<OscillatorNode::jhybridobject>& jThis, jlong jsContext); }; } // namespace audiocontext 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..f9ec9f22 --- /dev/null +++ b/android/src/main/java/com/audiocontext/context/AudioContext.kt @@ -0,0 +1,34 @@ +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.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 fun addNode(node: AudioNode) { + sources.add(node) + } + + override fun createOscillator(): OscillatorNode { + val oscillatorNode = OscillatorNode(this, reactContext) + addNode(oscillatorNode) + return oscillatorNode + } + + override fun dispatchAudio(buffer: ShortArray, audioTrack: AudioTrack) { + val currentBuffer = buffer.clone() + + synchronized(sources) { + sources.forEach { source -> + source.process(currentBuffer, audioTrack) + } + } + } +} 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..29063b3f --- /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 + +interface BaseAudioContext { + val sampleRate: Int + val destination: AudioDestinationNode + val sources: List<AudioNode> + + fun createOscillator(): OscillatorNode + abstract fun dispatchAudio(buffer: ShortArray, audioTrack: AudioTrack) +} diff --git a/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt b/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt index 68c75f86..80e17099 100644 --- a/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt +++ b/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt @@ -1,17 +1,22 @@ package com.audiocontext.nativemodules -import com.audiocontext.Oscillator +import com.audiocontext.context.AudioContext +import com.audiocontext.nodes.oscillator.OscillatorNode 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) { + private val audioContext = AudioContext(reactContext) + private val destination = audioContext.destination + override fun getName(): String { return "AudioContextModule" } @ReactMethod(isBlockingSynchronousMethod = true) fun createOscillator() { - Oscillator(reactContext) + val oscillator = audioContext.createOscillator() + oscillator.connect(destination) } } 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..e014a3ab --- /dev/null +++ b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt @@ -0,0 +1,83 @@ +package com.audiocontext.nodes.oscillator + +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioTrack +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? + + 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 -> (sin(wavePhase) * Short.MAX_VALUE).toInt().toShort() + WaveType.SQUARE -> ((if (sin(wavePhase) >= 0) 1 else -1) * Short.MAX_VALUE).toShort() + WaveType.SAWTOOTH -> ((2 * (wavePhase / (2 * Math.PI) - floor(wavePhase / (2 * Math.PI) + 0.5))) * Short.MAX_VALUE).toInt().toShort() + WaveType.TRIANGLE -> ((2 * abs(2 * (wavePhase / (2 * Math.PI) - floor(wavePhase / (2 * Math.PI) + 0.5))) - 1) * Short.MAX_VALUE).toInt().toShort() + } + wavePhase += phaseChange + } + + context.dispatchAudio(buffer, audioTrack) + } + audioTrack.flush() + } +} diff --git a/android/src/main/java/com/audiocontext/WaveType.kt b/android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt similarity index 100% rename from android/src/main/java/com/audiocontext/WaveType.kt rename to android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt diff --git a/cpp/OscillatorHostObject.cpp b/cpp/OscillatorNodeHostObject.cpp similarity index 76% rename from cpp/OscillatorHostObject.cpp rename to cpp/OscillatorNodeHostObject.cpp index 80c14f5c..c3ed82b6 100644 --- a/cpp/OscillatorHostObject.cpp +++ b/cpp/OscillatorNodeHostObject.cpp @@ -1,16 +1,16 @@ -#include "OscillatorHostObject.h" +#include "OscillatorNodeHostObject.h" namespace audiocontext { using namespace facebook; - std::vector<jsi::PropNameID> OscillatorHostObject::getPropertyNames(jsi::Runtime& runtime) { + 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 OscillatorHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propNameId) { + jsi::Value OscillatorNodeHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propNameId) { auto propName = propNameId.utf8(runtime); if (propName == "start") { @@ -30,7 +30,7 @@ namespace audiocontext { throw std::runtime_error("Prop not yet implemented!"); } - void OscillatorHostObject::set(jsi::Runtime& runtime, const jsi::PropNameID& propNameId, const jsi::Value& value) { + void OscillatorNodeHostObject::set(jsi::Runtime& runtime, const jsi::PropNameID& propNameId, const jsi::Value& value) { auto propName = propNameId.utf8(runtime); throw std::runtime_error("Not yet implemented!"); diff --git a/cpp/OscillatorHostObject.h b/cpp/OscillatorNodeHostObject.h similarity index 65% rename from cpp/OscillatorHostObject.h rename to cpp/OscillatorNodeHostObject.h index 9a8d512e..40680352 100644 --- a/cpp/OscillatorHostObject.h +++ b/cpp/OscillatorNodeHostObject.h @@ -3,19 +3,19 @@ #include <jsi/jsi.h> #include <fbjni/fbjni.h> #include <fbjni/detail/Hybrid.h> -#include "Oscillator.h" +#include "OscillatorNode.h" namespace audiocontext { using namespace facebook; - class Oscillator; + class OscillatorNode; - class OscillatorHostObject : public jsi::HostObject { + class OscillatorNodeHostObject : public jsi::HostObject { private: - Oscillator* oscillator_; + OscillatorNode* oscillator_; public: - explicit OscillatorHostObject(Oscillator* oscillator) : oscillator_(oscillator) {} + 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; diff --git a/example/src/App.tsx b/example/src/App.tsx index 362335d9..14d5ba4f 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -5,7 +5,7 @@ import type { OscillatorWrapper } from 'react-native-audio-context'; const App = () => { AudioContext.createOscillator(); - const oscillator = global.__OscillatorProxy as OscillatorWrapper; + const oscillator = global.__OscillatorNodeProxy as OscillatorWrapper; const startOscillator = () => { oscillator.start(); diff --git a/src/Oscillator.ts b/src/Oscillator.ts index 1d72483f..d1eb4b21 100644 --- a/src/Oscillator.ts +++ b/src/Oscillator.ts @@ -5,7 +5,7 @@ export interface OscillatorWrapper { declare global { function nativeCallSyncHook(): unknown; - var __OscillatorProxy: OscillatorWrapper | undefined; + var __OscillatorNodeProxy: OscillatorWrapper | undefined; } -export const Oscillator = global.__OscillatorProxy as OscillatorWrapper; +export const Oscillator = global.__OscillatorNodeProxy as OscillatorWrapper; From 4992eff8332e3ce982fb24b53b39206b60284cf0 Mon Sep 17 00:00:00 2001 From: Maciej Makowski <maciej.makowski@swmansion.com> Date: Mon, 15 Jul 2024 11:47:35 +0200 Subject: [PATCH 5/9] feat: trying to implement createOscillaltor function which should be inject into js runtime --- android/CMakeLists.txt | 4 +- android/src/main/cpp/AudioContext.cpp | 67 +++++++++++++++ android/src/main/cpp/AudioContext.h | 42 ++++++++++ android/src/main/cpp/OnLoad.cpp | 3 +- android/src/main/cpp/OscillatorNode.cpp | 14 ++-- android/src/main/cpp/OscillatorNode.h | 3 + .../main/java/com/audiocontext/Oscillator.kt | 82 ------------------- .../com/audiocontext/context/AudioContext.kt | 23 +++++- .../audiocontext/context/BaseAudioContext.kt | 2 +- .../nativemodules/AudioContextModule.kt | 3 +- .../nodes/oscillator/OscillatorNode.kt | 4 + cpp/AudioContextHostObject.cpp | 23 ++++++ cpp/AudioContextHostObject.h | 24 ++++++ 13 files changed, 195 insertions(+), 99 deletions(-) create mode 100644 android/src/main/cpp/AudioContext.cpp create mode 100644 android/src/main/cpp/AudioContext.h delete mode 100644 android/src/main/java/com/audiocontext/Oscillator.kt create mode 100644 cpp/AudioContextHostObject.cpp create mode 100644 cpp/AudioContextHostObject.h diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index 2e1e6dd8..baa80886 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -17,8 +17,8 @@ include_directories( add_library(react-native-audio-context SHARED src/main/cpp/OnLoad.cpp - src/main/cpp/OscillatorNode - ../cpp/OscillatorNodeHostObject + src/main/cpp + ../cpp/AudioContextHostObject ) find_package(ReactAndroid REQUIRED CONFIG) diff --git a/android/src/main/cpp/AudioContext.cpp b/android/src/main/cpp/AudioContext.cpp new file mode 100644 index 00000000..4ea2defe --- /dev/null +++ b/android/src/main/cpp/AudioContext.cpp @@ -0,0 +1,67 @@ +#include "AudioContext.h" +#include "AudioContextHostObject.h" +#include "OscillatorNode.h" +#include <fbjni/fbjni.h> +#include <jsi/jsi.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); + auto createOscillatorFunction = jsi::Function::createFromHostFunction( + *runtime, + jsi::PropNameID::forAscii(*runtime, "createOscillator"), + 0, + [this](jsi::Runtime &runtime, const jsi::Value &, const jsi::Value *, size_t) { + return jsiCreateOscillator(runtime); + }); + object.setProperty(*runtime, "createOscillator", std::move(createOscillatorFunction)); + runtime->global().setProperty(*runtime, "__AudioContextProxy", std::move(object)); + } + + // this method is called from Java to create a new instance of oscillator + jni::local_ref<OscillatorNode::jhybriddata> AudioContext::createOscillator() { + auto method = javaClassStatic()->getMethod<jni::local_ref<OscillatorNode::jhybriddata>()>("createOscillator"); + return method(javaObject_.get()); + } + + // this method is called in order to get a reference to c++ hybrid object of oscillator + //and create a host object for it + jsi::Value AudioContext::jsiCreateOscillator(jsi::Runtime &runtime) { + auto oscillatorHybridData = createOscillator(); + auto oscillatorNode = oscillatorHybridData->cthis();//error detail::HybridData does not have a member named 'cthis' + //cthis() is a method of HybridClass class that returns a pointer to the c++ object + + //HybridClass from Hybrid.h + //template <typename T, typename Base = detail::BaseHybridClass> + //class HybridClass : public detail::HybridTraits<Base>::CxxBase { + // public: + // struct JavaPart + // : JavaClass<JavaPart, typename detail::HybridTraits<Base>::JavaBase> { + // // At this point, T is incomplete, and so we cannot access + // // T::kJavaDescriptor directly. jtype_traits support this escape hatch for + // // such a case. + // static constexpr const char* kJavaDescriptor = nullptr; + // static constexpr auto /* detail::SimpleFixedString<_> */ + // get_instantiated_java_descriptor(); + // static constexpr auto /* detail::SimpleFixedString<_> */ + // get_instantiated_base_name(); + // + // using HybridType = T; + // + // // This will reach into the java object and extract the C++ instance from + // // the mHybridData and return it. + // T* cthis() const; + // + // friend class HybridClass; + // friend T; + // }; + return oscillatorNode->createOscillatorNodeHostObject(runtime); + } + +} // namespace audiocontext diff --git a/android/src/main/cpp/AudioContext.h b/android/src/main/cpp/AudioContext.h new file mode 100644 index 00000000..8365b3be --- /dev/null +++ b/android/src/main/cpp/AudioContext.h @@ -0,0 +1,42 @@ +#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/nodes/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), + }); + } + + jni::local_ref<OscillatorNode::jhybriddata> createOscillator(); + jsi::Value jsiCreateOscillator(jsi::Runtime &runtime); + + 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 index 8c9184c3..93daacda 100644 --- a/android/src/main/cpp/OnLoad.cpp +++ b/android/src/main/cpp/OnLoad.cpp @@ -1,6 +1,6 @@ #include <fbjni/fbjni.h> #include "OscillatorNode.h" -#include "OscillatorNodeHostObject.h" +#include "AudioContext.h" using namespace audiocontext; @@ -8,5 +8,6 @@ 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 index 49877153..d4e0156e 100644 --- a/android/src/main/cpp/OscillatorNode.cpp +++ b/android/src/main/cpp/OscillatorNode.cpp @@ -6,22 +6,22 @@ namespace audiocontext { using namespace facebook::jni; - OscillatorNode::OscillatorNode(jni::alias_ref<OscillatorNode::jhybridobject> &jThis, - jlong jsContext): javaObject_(make_global(jThis)) { + 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); - auto object = jsi::Object::createFromHostObject(*runtime, hostObject); - runtime->global().setProperty(*runtime, "__OscillatorNodeProxy", std::move(object)); + return jsi::Object::createFromHostObject(*runtime, hostObject); } void OscillatorNode::start() { - static const auto method = javaClassStatic()->getMethod<void()>("start"); + static const auto method = javaClassLocal()->getMethod<void()>("start"); method(javaObject_.get()); } void OscillatorNode::stop() { - static const auto method = javaClassStatic()->getMethod<void()>("stop"); + static const auto method = javaClassLocal()->getMethod<void()>("stop"); method(javaObject_.get()); } - } // namespace audiocontext diff --git a/android/src/main/cpp/OscillatorNode.h b/android/src/main/cpp/OscillatorNode.h index 5c0d669f..1751421e 100644 --- a/android/src/main/cpp/OscillatorNode.h +++ b/android/src/main/cpp/OscillatorNode.h @@ -20,6 +20,8 @@ namespace audiocontext { return makeCxxInstance(jThis, jsContext); } + jsi::Object createOscillatorNodeHostObject(); + static void registerNatives() { registerHybrid({ makeNativeMethod("initHybrid", OscillatorNode::initHybrid), @@ -33,6 +35,7 @@ namespace audiocontext { friend HybridBase; global_ref<OscillatorNode::javaobject> javaObject_; + jlong jsContext; explicit OscillatorNode(jni::alias_ref<OscillatorNode::jhybridobject>& jThis, jlong jsContext); }; diff --git a/android/src/main/java/com/audiocontext/Oscillator.kt b/android/src/main/java/com/audiocontext/Oscillator.kt deleted file mode 100644 index 01469ca8..00000000 --- a/android/src/main/java/com/audiocontext/Oscillator.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.audiocontext - -import android.media.AudioFormat -import android.media.AudioManager -import android.media.AudioTrack -import com.audiocontext.nodes.oscillator.WaveType -import com.facebook.jni.HybridData -import com.facebook.react.bridge.ReactApplicationContext -import kotlin.math.abs -import kotlin.math.floor -import kotlin.math.sin - -class Oscillator(reactContext: ReactApplicationContext) { - val numberOfInputs: Int = 0 - 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( - 44100, - AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT) - this.audioTrack = AudioTrack( - AudioManager.STREAM_MUSIC, 44100, AudioFormat.CHANNEL_OUT_MONO, - AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM - ) - } - - external fun initHybrid(l: Long): HybridData? - - fun start() { - if(isPlaying) return - isPlaying = true - audioTrack.play() - playbackThread = Thread { generateSound() }.apply{ start()} - } - - 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) / 44100 - - for(i in buffer.indices) { - buffer[i] = when(waveType) { - WaveType.SINE -> (sin(wavePhase) * Short.MAX_VALUE).toInt().toShort() - WaveType.SQUARE -> ((if (sin(wavePhase) >= 0) 1 else -1) * Short.MAX_VALUE).toShort() - WaveType.SAWTOOTH -> ((2 * (wavePhase / (2 * Math.PI) - floor(wavePhase / (2 * Math.PI) + 0.5))) * Short.MAX_VALUE).toInt().toShort() - WaveType.TRIANGLE -> ((2 * abs(2 * (wavePhase / (2 * Math.PI) - floor(wavePhase / (2 * Math.PI) + 0.5))) - 1) * Short.MAX_VALUE).toInt().toShort() - } - wavePhase += phaseChange - } - - audioTrack.write(buffer, 0, buffer.size) - } - audioTrack.flush() - } -} diff --git a/android/src/main/java/com/audiocontext/context/AudioContext.kt b/android/src/main/java/com/audiocontext/context/AudioContext.kt index f9ec9f22..fd85f7a0 100644 --- a/android/src/main/java/com/audiocontext/context/AudioContext.kt +++ b/android/src/main/java/com/audiocontext/context/AudioContext.kt @@ -4,6 +4,7 @@ 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 @@ -12,14 +13,28 @@ class AudioContext(private val reactContext: ReactApplicationContext) : BaseAudi 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 oscillatorNode = OscillatorNode(this, reactContext) - addNode(oscillatorNode) - return oscillatorNode + fun createOscillator(): HybridData? { + val oscillator = OscillatorNode(this, reactContext) + addNode(oscillator) + return oscillator.getHybridData() } override fun dispatchAudio(buffer: ShortArray, audioTrack: AudioTrack) { diff --git a/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt b/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt index 29063b3f..7685323a 100644 --- a/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt +++ b/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt @@ -4,12 +4,12 @@ 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> - fun createOscillator(): OscillatorNode abstract fun dispatchAudio(buffer: ShortArray, audioTrack: AudioTrack) } diff --git a/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt b/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt index 80e17099..5d3cf64f 100644 --- a/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt +++ b/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt @@ -1,7 +1,6 @@ package com.audiocontext.nativemodules import com.audiocontext.context.AudioContext -import com.audiocontext.nodes.oscillator.OscillatorNode import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod @@ -17,6 +16,6 @@ class AudioContextModule(private val reactContext: ReactApplicationContext) : Re @ReactMethod(isBlockingSynchronousMethod = true) fun createOscillator() { val oscillator = audioContext.createOscillator() - oscillator.connect(destination) + //oscillator.connect(destination) } } diff --git a/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt index e014a3ab..a7ec86d2 100644 --- a/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt +++ b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt @@ -45,6 +45,10 @@ class OscillatorNode(context: BaseAudioContext, reactContext: ReactApplicationCo external fun initHybrid(l: Long): HybridData? + fun getHybridData(): HybridData? { + return mHybridData + } + override fun start() { if(isPlaying) return isPlaying = true diff --git a/cpp/AudioContextHostObject.cpp b/cpp/AudioContextHostObject.cpp new file mode 100644 index 00000000..ca9d6de7 --- /dev/null +++ b/cpp/AudioContextHostObject.cpp @@ -0,0 +1,23 @@ +#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::forAscii(runtime, "createOscillator")); + return propertyNames; + } + + jsi::Value AudioContextHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propNameId) { + auto propName = propNameId.utf8(runtime); + + throw std::runtime_error("Prop 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!"); + } +} diff --git a/cpp/AudioContextHostObject.h b/cpp/AudioContextHostObject.h new file mode 100644 index 00000000..c18aa910 --- /dev/null +++ b/cpp/AudioContextHostObject.h @@ -0,0 +1,24 @@ +#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; + }; +} // namespace audiocontext From ed55f6e7253924ce6d0b4316fce980e0b8ac0d6d Mon Sep 17 00:00:00 2001 From: Maciej Makowski <maciej.makowski@swmansion.com> Date: Tue, 16 Jul 2024 10:03:20 +0200 Subject: [PATCH 6/9] feat: correctly integrated oscillator with audiocontext --- android/CMakeLists.txt | 4 +- android/src/main/cpp/AudioContext.cpp | 51 +++---------------- android/src/main/cpp/AudioContext.h | 5 +- android/src/main/cpp/OscillatorNode.cpp | 30 ++++++++++- android/src/main/cpp/OscillatorNode.h | 9 +++- .../com/audiocontext/context/AudioContext.kt | 5 +- .../nativemodules/AudioContextModule.kt | 7 +-- .../nodes/oscillator/OscillatorNode.kt | 36 +++++++++++-- .../audiocontext/nodes/oscillator/WaveType.kt | 14 ++++- cpp/AudioContextHostObject.cpp | 10 +++- cpp/OscillatorNodeHostObject.cpp | 35 +++++++++++++ example/src/App.tsx | 11 ++-- src/AudioContext.ts | 8 --- src/Oscillator.ts | 11 ---- src/index.ts | 23 ++++++++- src/types.d.ts | 8 +++ src/types.ts | 4 -- 17 files changed, 175 insertions(+), 96 deletions(-) delete mode 100644 src/AudioContext.ts delete mode 100644 src/Oscillator.ts create mode 100644 src/types.d.ts delete mode 100644 src/types.ts diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index baa80886..f1c7b96d 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -17,8 +17,10 @@ include_directories( add_library(react-native-audio-context SHARED src/main/cpp/OnLoad.cpp - src/main/cpp + src/main/cpp/AudioContext + src/main/cpp/OscillatorNode ../cpp/AudioContextHostObject + ../cpp/OscillatorNodeHostObject ) find_package(ReactAndroid REQUIRED CONFIG) diff --git a/android/src/main/cpp/AudioContext.cpp b/android/src/main/cpp/AudioContext.cpp index 4ea2defe..731d04a2 100644 --- a/android/src/main/cpp/AudioContext.cpp +++ b/android/src/main/cpp/AudioContext.cpp @@ -3,6 +3,7 @@ #include "OscillatorNode.h" #include <fbjni/fbjni.h> #include <jsi/jsi.h> +#include <android/log.h> namespace audiocontext { @@ -13,55 +14,15 @@ namespace audiocontext { auto runtime = reinterpret_cast<jsi::Runtime *>(jsContext); auto hostObject = std::make_shared<AudioContextHostObject>(this); auto object = jsi::Object::createFromHostObject(*runtime, hostObject); - auto createOscillatorFunction = jsi::Function::createFromHostFunction( - *runtime, - jsi::PropNameID::forAscii(*runtime, "createOscillator"), - 0, - [this](jsi::Runtime &runtime, const jsi::Value &, const jsi::Value *, size_t) { - return jsiCreateOscillator(runtime); - }); - object.setProperty(*runtime, "createOscillator", std::move(createOscillatorFunction)); runtime->global().setProperty(*runtime, "__AudioContextProxy", std::move(object)); } - // this method is called from Java to create a new instance of oscillator - jni::local_ref<OscillatorNode::jhybriddata> AudioContext::createOscillator() { - auto method = javaClassStatic()->getMethod<jni::local_ref<OscillatorNode::jhybriddata>()>("createOscillator"); - return method(javaObject_.get()); - } - - // this method is called in order to get a reference to c++ hybrid object of oscillator - //and create a host object for it - jsi::Value AudioContext::jsiCreateOscillator(jsi::Runtime &runtime) { - auto oscillatorHybridData = createOscillator(); - auto oscillatorNode = oscillatorHybridData->cthis();//error detail::HybridData does not have a member named 'cthis' - //cthis() is a method of HybridClass class that returns a pointer to the c++ object + jsi::Object AudioContext::createOscillator() { + static const auto method = javaClassLocal()->getMethod<OscillatorNode()>("createOscillator"); + auto oscillator = method(javaObject_.get()); + auto oscillatorCppInstance = oscillator->cthis(); - //HybridClass from Hybrid.h - //template <typename T, typename Base = detail::BaseHybridClass> - //class HybridClass : public detail::HybridTraits<Base>::CxxBase { - // public: - // struct JavaPart - // : JavaClass<JavaPart, typename detail::HybridTraits<Base>::JavaBase> { - // // At this point, T is incomplete, and so we cannot access - // // T::kJavaDescriptor directly. jtype_traits support this escape hatch for - // // such a case. - // static constexpr const char* kJavaDescriptor = nullptr; - // static constexpr auto /* detail::SimpleFixedString<_> */ - // get_instantiated_java_descriptor(); - // static constexpr auto /* detail::SimpleFixedString<_> */ - // get_instantiated_base_name(); - // - // using HybridType = T; - // - // // This will reach into the java object and extract the C++ instance from - // // the mHybridData and return it. - // T* cthis() const; - // - // friend class HybridClass; - // friend T; - // }; - return oscillatorNode->createOscillatorNodeHostObject(runtime); + return oscillatorCppInstance->createOscillatorNodeHostObject(); } } // namespace audiocontext diff --git a/android/src/main/cpp/AudioContext.h b/android/src/main/cpp/AudioContext.h index 8365b3be..d50a1775 100644 --- a/android/src/main/cpp/AudioContext.h +++ b/android/src/main/cpp/AudioContext.h @@ -14,7 +14,7 @@ namespace audiocontext { class AudioContext : public jni::HybridClass<AudioContext> { public: - static auto constexpr kJavaDescriptor = "Lcom/audiocontext/nodes/context/AudioContext;"; + static auto constexpr kJavaDescriptor = "Lcom/audiocontext/context/AudioContext;"; static jni::local_ref<AudioContext::jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis, jlong jsContext) { @@ -27,8 +27,7 @@ namespace audiocontext { }); } - jni::local_ref<OscillatorNode::jhybriddata> createOscillator(); - jsi::Value jsiCreateOscillator(jsi::Runtime &runtime); + jsi::Object createOscillator(); private: friend HybridBase; diff --git a/android/src/main/cpp/OscillatorNode.cpp b/android/src/main/cpp/OscillatorNode.cpp index d4e0156e..ee3ce953 100644 --- a/android/src/main/cpp/OscillatorNode.cpp +++ b/android/src/main/cpp/OscillatorNode.cpp @@ -1,6 +1,7 @@ #include "OscillatorNode.h" #include <fbjni/fbjni.h> #include <jsi/jsi.h> +#include <android/log.h> namespace audiocontext { @@ -16,12 +17,37 @@ namespace audiocontext { } void OscillatorNode::start() { - static const auto method = javaClassLocal()->getMethod<void()>("start"); + static const auto method = javaClassStatic()->getMethod<void()>("start"); method(javaObject_.get()); } void OscillatorNode::stop() { - static const auto method = javaClassLocal()->getMethod<void()>("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); + } + + jni::local_ref<JString> OscillatorNode::getWaveType() { + static const auto method = javaClassStatic()->getMethod<jni::local_ref<JString>()>("getWaveType"); + return method(javaObject_.get()); + } + + 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 index 1751421e..05ddcb8a 100644 --- a/android/src/main/cpp/OscillatorNode.h +++ b/android/src/main/cpp/OscillatorNode.h @@ -20,8 +20,6 @@ namespace audiocontext { return makeCxxInstance(jThis, jsContext); } - jsi::Object createOscillatorNodeHostObject(); - static void registerNatives() { registerHybrid({ makeNativeMethod("initHybrid", OscillatorNode::initHybrid), @@ -30,6 +28,13 @@ namespace audiocontext { void start(); void stop(); + void setFrequency(jdouble frequency); + void setDetune(jdouble detune); + jni::local_ref<JString> getWaveType(); + jdouble getFrequency(); + jdouble getDetune(); + + jsi::Object createOscillatorNodeHostObject(); private: friend HybridBase; diff --git a/android/src/main/java/com/audiocontext/context/AudioContext.kt b/android/src/main/java/com/audiocontext/context/AudioContext.kt index fd85f7a0..45c4974f 100644 --- a/android/src/main/java/com/audiocontext/context/AudioContext.kt +++ b/android/src/main/java/com/audiocontext/context/AudioContext.kt @@ -31,10 +31,11 @@ class AudioContext(private val reactContext: ReactApplicationContext) : BaseAudi sources.add(node) } - fun createOscillator(): HybridData? { + fun createOscillator(): OscillatorNode { val oscillator = OscillatorNode(this, reactContext) + oscillator.connect(destination) addNode(oscillator) - return oscillator.getHybridData() + return oscillator } override fun dispatchAudio(buffer: ShortArray, audioTrack: AudioTrack) { diff --git a/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt b/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt index 5d3cf64f..97440cb7 100644 --- a/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt +++ b/android/src/main/java/com/audiocontext/nativemodules/AudioContextModule.kt @@ -6,16 +6,13 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod class AudioContextModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { - private val audioContext = AudioContext(reactContext) - private val destination = audioContext.destination override fun getName(): String { return "AudioContextModule" } @ReactMethod(isBlockingSynchronousMethod = true) - fun createOscillator() { - val oscillator = audioContext.createOscillator() - //oscillator.connect(destination) + fun initAudioContext() { + AudioContext(reactContext) } } diff --git a/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt index a7ec86d2..fb955d5b 100644 --- a/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt +++ b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt @@ -3,6 +3,7 @@ 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 @@ -33,7 +34,6 @@ class OscillatorNode(context: BaseAudioContext, reactContext: ReactApplicationCo init { mHybridData = initHybrid(reactContext.javaScriptContextHolder!!.get()) - val bufferSize = AudioTrack.getMinBufferSize( context.sampleRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT) @@ -45,19 +45,45 @@ class OscillatorNode(context: BaseAudioContext, reactContext: ReactApplicationCo external fun initHybrid(l: Long): HybridData? - fun getHybridData(): HybridData? { - return mHybridData + fun getWaveType(): WaveType { + return waveType + } + + fun getFrequency(): Double { + return frequency + } + + fun getDetune(): Double { + return detune + } + + fun setWaveType(waveType: String) { + this.waveType = WaveType.switchWaveType(waveType) + } + + fun setFrequency(frequency: Double) { + this.frequency = frequency + } + + fun setDetune(detune: Double) { + this.detune = detune } override fun start() { - if(isPlaying) return + if(isPlaying) { + return + } + isPlaying = true audioTrack.play() playbackThread = Thread { generateSound() }.apply{ start()} } override fun stop() { - if(!isPlaying) return + if(!isPlaying) { + return + } + isPlaying = false audioTrack.stop() playbackThread?.join() diff --git a/android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt b/android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt index ffd63ff2..06816c29 100644 --- a/android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt +++ b/android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt @@ -4,5 +4,17 @@ enum class WaveType { SINE, SQUARE, SAWTOOTH, - TRIANGLE + TRIANGLE; + + companion object { + fun switchWaveType(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 index ca9d6de7..1464daf2 100644 --- a/cpp/AudioContextHostObject.cpp +++ b/cpp/AudioContextHostObject.cpp @@ -5,14 +5,20 @@ namespace audiocontext { std::vector<jsi::PropNameID> AudioContextHostObject::getPropertyNames(jsi::Runtime& runtime) { std::vector<jsi::PropNameID> propertyNames; - propertyNames.push_back(jsi::PropNameID::forAscii(runtime, "createOscillator")); + 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); - throw std::runtime_error("Prop not yet implemented!"); + if(propName == "createOscillator") { + 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(); + }); + } + + throw std::runtime_error("Not yet implemented!"); } void AudioContextHostObject::set(jsi::Runtime& runtime, const jsi::PropNameID& propNameId, const jsi::Value& value) { diff --git a/cpp/OscillatorNodeHostObject.cpp b/cpp/OscillatorNodeHostObject.cpp index c3ed82b6..8dc3e15f 100644 --- a/cpp/OscillatorNodeHostObject.cpp +++ b/cpp/OscillatorNodeHostObject.cpp @@ -1,4 +1,5 @@ #include "OscillatorNodeHostObject.h" +#include <android/log.h> namespace audiocontext { using namespace facebook; @@ -27,12 +28,46 @@ namespace audiocontext { }); } + if (propName == "wave") { + 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 waveTypeJString = oscillator_->getWaveType(); + std::string waveTypeStr = waveTypeJString->toStdString(); + return jsi::String::createFromUtf8(rt, waveTypeStr); + }); + } + + if (propName == "frequency") { + 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 {frequency}; + }); + } + + if (propName == "detune") { + 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 {detune}; + }); + } + 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!"); } } diff --git a/example/src/App.tsx b/example/src/App.tsx index 14d5ba4f..d4eedc36 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,18 +1,23 @@ /* eslint-disable react/react-in-jsx-scope */ import { Button, StyleSheet, Text, View } from 'react-native'; import { AudioContext } from 'react-native-audio-context'; -import type { OscillatorWrapper } from 'react-native-audio-context'; const App = () => { - AudioContext.createOscillator(); - const oscillator = global.__OscillatorNodeProxy as OscillatorWrapper; + const audioContext = new AudioContext(); + + const oscillator = audioContext.createOscillator(); + const oscillator2 = audioContext.createOscillator(); + + oscillator.frequency = 200; const startOscillator = () => { oscillator.start(); + oscillator2.start(); }; const stopOscillator = () => { oscillator.stop(); + oscillator2.stop(); }; return ( diff --git a/src/AudioContext.ts b/src/AudioContext.ts deleted file mode 100644 index 038ddf73..00000000 --- a/src/AudioContext.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { NativeModules } from 'react-native'; -const { AudioContextModule } = NativeModules; - -interface AudioContextInterface { - createOscillator(): void; -} - -export const AudioContext = AudioContextModule as AudioContextInterface; diff --git a/src/Oscillator.ts b/src/Oscillator.ts deleted file mode 100644 index d1eb4b21..00000000 --- a/src/Oscillator.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface OscillatorWrapper { - start(): undefined; - stop(): undefined; -} - -declare global { - function nativeCallSyncHook(): unknown; - var __OscillatorNodeProxy: OscillatorWrapper | undefined; -} - -export const Oscillator = global.__OscillatorNodeProxy as OscillatorWrapper; diff --git a/src/index.ts b/src/index.ts index cfe45d34..eb542a18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,21 @@ -export * from './Oscillator'; -export * from './AudioContext'; +import { NativeModules } from 'react-native'; +import type { Float } from 'react-native/Libraries/Types/CodegenTypes'; +const { AudioContextModule } = NativeModules; + +interface Oscillator { + frequency: Float; + wave: 'sine' | 'square' | 'sawtooth' | 'triangle'; + detune: Float; + start: () => void; + stop: () => void; +} + +export class AudioContext { + constructor() { + AudioContextModule.initAudioContext(); + } + + createOscillator(): Oscillator { + return global.__AudioContextProxy.createOscillator(); + } +} diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 00000000..0c429ee3 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,8 @@ +export interface AudioContextWrapper { + createOscillator(): Oscillator; +} + +declare global { + function nativeCallSyncHook(): unknown; + var __AudioContextProxy: AudioContextWrapper; +} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 43a2d4fe..00000000 --- a/src/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface JSIExampleWrapper { - start(): void; - stop(): void; -} From f190c7e17116a4356c4c385dd960739d21fa6958 Mon Sep 17 00:00:00 2001 From: Maciej Makowski <maciej.makowski@swmansion.com> Date: Tue, 16 Jul 2024 11:32:49 +0200 Subject: [PATCH 7/9] fix: removed wave getters and setters, fix types in interface --- android/src/main/cpp/OscillatorNode.cpp | 5 ----- android/src/main/cpp/OscillatorNode.h | 1 - .../audiocontext/nodes/oscillator/OscillatorNode.kt | 8 -------- cpp/OscillatorNodeHostObject.cpp | 12 ++---------- example/src/App.tsx | 1 - src/index.ts | 5 ++--- 6 files changed, 4 insertions(+), 28 deletions(-) diff --git a/android/src/main/cpp/OscillatorNode.cpp b/android/src/main/cpp/OscillatorNode.cpp index ee3ce953..5e3899b3 100644 --- a/android/src/main/cpp/OscillatorNode.cpp +++ b/android/src/main/cpp/OscillatorNode.cpp @@ -36,11 +36,6 @@ namespace audiocontext { method(javaObject_.get(), detune); } - jni::local_ref<JString> OscillatorNode::getWaveType() { - static const auto method = javaClassStatic()->getMethod<jni::local_ref<JString>()>("getWaveType"); - return method(javaObject_.get()); - } - jdouble OscillatorNode::getFrequency() { static const auto method = javaClassLocal()->getMethod<jdouble()>("getFrequency"); return method(javaObject_.get()); diff --git a/android/src/main/cpp/OscillatorNode.h b/android/src/main/cpp/OscillatorNode.h index 05ddcb8a..8471ab17 100644 --- a/android/src/main/cpp/OscillatorNode.h +++ b/android/src/main/cpp/OscillatorNode.h @@ -30,7 +30,6 @@ namespace audiocontext { void stop(); void setFrequency(jdouble frequency); void setDetune(jdouble detune); - jni::local_ref<JString> getWaveType(); jdouble getFrequency(); jdouble getDetune(); diff --git a/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt index fb955d5b..0aa1b5c9 100644 --- a/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt +++ b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt @@ -45,10 +45,6 @@ class OscillatorNode(context: BaseAudioContext, reactContext: ReactApplicationCo external fun initHybrid(l: Long): HybridData? - fun getWaveType(): WaveType { - return waveType - } - fun getFrequency(): Double { return frequency } @@ -57,10 +53,6 @@ class OscillatorNode(context: BaseAudioContext, reactContext: ReactApplicationCo return detune } - fun setWaveType(waveType: String) { - this.waveType = WaveType.switchWaveType(waveType) - } - fun setFrequency(frequency: Double) { this.frequency = frequency } diff --git a/cpp/OscillatorNodeHostObject.cpp b/cpp/OscillatorNodeHostObject.cpp index 8dc3e15f..3673e3c4 100644 --- a/cpp/OscillatorNodeHostObject.cpp +++ b/cpp/OscillatorNodeHostObject.cpp @@ -28,25 +28,17 @@ namespace audiocontext { }); } - if (propName == "wave") { - 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 waveTypeJString = oscillator_->getWaveType(); - std::string waveTypeStr = waveTypeJString->toStdString(); - return jsi::String::createFromUtf8(rt, waveTypeStr); - }); - } - if (propName == "frequency") { 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 {frequency}; + return jsi::Value(frequency); }); } if (propName == "detune") { 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 {detune}; + return jsi::Value(detune); }); } diff --git a/example/src/App.tsx b/example/src/App.tsx index d4eedc36..137455ef 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -7,7 +7,6 @@ const App = () => { const oscillator = audioContext.createOscillator(); const oscillator2 = audioContext.createOscillator(); - oscillator.frequency = 200; const startOscillator = () => { diff --git a/src/index.ts b/src/index.ts index eb542a18..2fc0b680 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,10 @@ import { NativeModules } from 'react-native'; -import type { Float } from 'react-native/Libraries/Types/CodegenTypes'; const { AudioContextModule } = NativeModules; interface Oscillator { - frequency: Float; + frequency: number; wave: 'sine' | 'square' | 'sawtooth' | 'triangle'; - detune: Float; + detune: number; start: () => void; stop: () => void; } From 493b198a5d1fdd62a26d861d655eeafeba5202db Mon Sep 17 00:00:00 2001 From: Maciej Makowski <maciej.makowski@swmansion.com> Date: Tue, 16 Jul 2024 14:34:57 +0200 Subject: [PATCH 8/9] fix: refactoring --- .../com/audiocontext/context/AudioContext.kt | 2 +- .../audiocontext/context/BaseAudioContext.kt | 1 + .../nodes/oscillator/OscillatorNode.kt | 24 +++++++-- .../audiocontext/nodes/oscillator/WaveType.kt | 2 +- cpp/AudioContextHostObject.cpp | 12 +++-- cpp/AudioContextHostObject.h | 2 + cpp/OscillatorNodeHostObject.cpp | 51 +++++++++++++------ cpp/OscillatorNodeHostObject.h | 5 ++ example/src/App.tsx | 30 +++++++---- src/index.ts | 14 ++--- src/types.d.ts | 8 --- src/types.ts | 22 ++++++++ 12 files changed, 123 insertions(+), 50 deletions(-) delete mode 100644 src/types.d.ts create mode 100644 src/types.ts diff --git a/android/src/main/java/com/audiocontext/context/AudioContext.kt b/android/src/main/java/com/audiocontext/context/AudioContext.kt index 45c4974f..69d94227 100644 --- a/android/src/main/java/com/audiocontext/context/AudioContext.kt +++ b/android/src/main/java/com/audiocontext/context/AudioContext.kt @@ -31,7 +31,7 @@ class AudioContext(private val reactContext: ReactApplicationContext) : BaseAudi sources.add(node) } - fun createOscillator(): OscillatorNode { + override fun createOscillator(): OscillatorNode { val oscillator = OscillatorNode(this, reactContext) oscillator.connect(destination) addNode(oscillator) diff --git a/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt b/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt index 7685323a..27cf2eff 100644 --- a/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt +++ b/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt @@ -11,5 +11,6 @@ interface BaseAudioContext { val destination: AudioDestinationNode val sources: List<AudioNode> + abstract fun createOscillator(): OscillatorNode abstract fun dispatchAudio(buffer: ShortArray, audioTrack: AudioTrack) } diff --git a/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt index 0aa1b5c9..c09e4298 100644 --- a/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt +++ b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt @@ -90,10 +90,10 @@ class OscillatorNode(context: BaseAudioContext, reactContext: ReactApplicationCo for(i in buffer.indices) { buffer[i] = when(waveType) { - WaveType.SINE -> (sin(wavePhase) * Short.MAX_VALUE).toInt().toShort() - WaveType.SQUARE -> ((if (sin(wavePhase) >= 0) 1 else -1) * Short.MAX_VALUE).toShort() - WaveType.SAWTOOTH -> ((2 * (wavePhase / (2 * Math.PI) - floor(wavePhase / (2 * Math.PI) + 0.5))) * Short.MAX_VALUE).toInt().toShort() - WaveType.TRIANGLE -> ((2 * abs(2 * (wavePhase / (2 * Math.PI) - floor(wavePhase / (2 * Math.PI) + 0.5))) - 1) * Short.MAX_VALUE).toInt().toShort() + WaveType.SINE -> sineWaveBuffer(wavePhase) + WaveType.SQUARE -> squareWaveBuffer(wavePhase) + WaveType.SAWTOOTH -> sawtoothWaveBuffer(wavePhase) + WaveType.TRIANGLE -> triangleWaveBuffer(wavePhase) } wavePhase += phaseChange } @@ -102,4 +102,20 @@ class OscillatorNode(context: BaseAudioContext, reactContext: ReactApplicationCo } 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 index 06816c29..5d506dca 100644 --- a/android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt +++ b/android/src/main/java/com/audiocontext/nodes/oscillator/WaveType.kt @@ -7,7 +7,7 @@ enum class WaveType { TRIANGLE; companion object { - fun switchWaveType(type: String): WaveType { + fun fromString(type: String): WaveType { return when (type.uppercase()) { "SINE" -> SINE "SQUARE" -> SQUARE diff --git a/cpp/AudioContextHostObject.cpp b/cpp/AudioContextHostObject.cpp index 1464daf2..6a16841b 100644 --- a/cpp/AudioContextHostObject.cpp +++ b/cpp/AudioContextHostObject.cpp @@ -13,9 +13,7 @@ namespace audiocontext { auto propName = propNameId.utf8(runtime); if(propName == "createOscillator") { - 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(); - }); + return createOscillator(runtime, propNameId); } throw std::runtime_error("Not yet implemented!"); @@ -26,4 +24,12 @@ namespace audiocontext { 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 index c18aa910..af9e07fd 100644 --- a/cpp/AudioContextHostObject.h +++ b/cpp/AudioContextHostObject.h @@ -20,5 +20,7 @@ namespace 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/OscillatorNodeHostObject.cpp b/cpp/OscillatorNodeHostObject.cpp index 3673e3c4..9d387fcc 100644 --- a/cpp/OscillatorNodeHostObject.cpp +++ b/cpp/OscillatorNodeHostObject.cpp @@ -15,31 +15,19 @@ namespace audiocontext { auto propName = propNameId.utf8(runtime); if (propName == "start") { - 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(); - }); + return start(runtime, propNameId); } if (propName == "stop") { - 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(); - }); + return stop(runtime, propNameId); } if (propName == "frequency") { - 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); - }); + return frequency(runtime, propNameId); } if (propName == "detune") { - 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); - }); + return detune(runtime, propNameId); } throw std::runtime_error("Prop not yet implemented!"); @@ -62,4 +50,35 @@ namespace audiocontext { 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 index 40680352..9a0822f9 100644 --- a/cpp/OscillatorNodeHostObject.h +++ b/cpp/OscillatorNodeHostObject.h @@ -20,5 +20,10 @@ namespace 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 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 137455ef..d38cb456 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,22 +1,32 @@ /* eslint-disable react/react-in-jsx-scope */ import { Button, StyleSheet, Text, View } from 'react-native'; -import { AudioContext } from 'react-native-audio-context'; +import { useRef, useEffect } from 'react'; + +import { AudioContext, type Oscillator } from 'react-native-audio-context'; const App = () => { - const audioContext = new AudioContext(); + 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; - const oscillator = audioContext.createOscillator(); - const oscillator2 = audioContext.createOscillator(); - oscillator.frequency = 200; + return () => { + //TODO + }; + }, []); const startOscillator = () => { - oscillator.start(); - oscillator2.start(); + oscillatorRef.current?.start(); + //secondaryOscillatorRef.current?.start(); }; - const stopOscillator = () => { - oscillator.stop(); - oscillator2.stop(); + oscillatorRef.current?.stop(); + //secondaryOscillatorRef.current?.stop(); }; return ( diff --git a/src/index.ts b/src/index.ts index 2fc0b680..255b48fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,13 @@ import { NativeModules } from 'react-native'; const { AudioContextModule } = NativeModules; +import type { Oscillator, BaseAudioContext } from './types'; -interface Oscillator { - frequency: number; - wave: 'sine' | 'square' | 'sawtooth' | 'triangle'; - detune: number; - start: () => void; - stop: () => void; +declare global { + function nativeCallSyncHook(): unknown; + var __AudioContextProxy: BaseAudioContext; } -export class AudioContext { +export class AudioContext implements BaseAudioContext { constructor() { AudioContextModule.initAudioContext(); } @@ -18,3 +16,5 @@ export class AudioContext { return global.__AudioContextProxy.createOscillator(); } } + +export type { Oscillator }; diff --git a/src/types.d.ts b/src/types.d.ts deleted file mode 100644 index 0c429ee3..00000000 --- a/src/types.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface AudioContextWrapper { - createOscillator(): Oscillator; -} - -declare global { - function nativeCallSyncHook(): unknown; - var __AudioContextProxy: AudioContextWrapper; -} 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; +} From 423f33bacb9988570d0a520f212e02824bf37240 Mon Sep 17 00:00:00 2001 From: Maciej Makowski <maciej.makowski@swmansion.com> Date: Tue, 16 Jul 2024 14:41:19 +0200 Subject: [PATCH 9/9] fix: fixed audio processing flow logic --- .../main/java/com/audiocontext/context/AudioContext.kt | 10 ---------- .../java/com/audiocontext/context/BaseAudioContext.kt | 1 - .../audiocontext/nodes/oscillator/OscillatorNode.kt | 3 +-- example/src/App.tsx | 4 ++-- 4 files changed, 3 insertions(+), 15 deletions(-) diff --git a/android/src/main/java/com/audiocontext/context/AudioContext.kt b/android/src/main/java/com/audiocontext/context/AudioContext.kt index 69d94227..86f02201 100644 --- a/android/src/main/java/com/audiocontext/context/AudioContext.kt +++ b/android/src/main/java/com/audiocontext/context/AudioContext.kt @@ -37,14 +37,4 @@ class AudioContext(private val reactContext: ReactApplicationContext) : BaseAudi addNode(oscillator) return oscillator } - - override fun dispatchAudio(buffer: ShortArray, audioTrack: AudioTrack) { - val currentBuffer = buffer.clone() - - synchronized(sources) { - sources.forEach { source -> - source.process(currentBuffer, audioTrack) - } - } - } } diff --git a/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt b/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt index 27cf2eff..80175c5d 100644 --- a/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt +++ b/android/src/main/java/com/audiocontext/context/BaseAudioContext.kt @@ -12,5 +12,4 @@ interface BaseAudioContext { val sources: List<AudioNode> abstract fun createOscillator(): OscillatorNode - abstract fun dispatchAudio(buffer: ShortArray, audioTrack: AudioTrack) } diff --git a/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt index c09e4298..f05373a4 100644 --- a/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt +++ b/android/src/main/java/com/audiocontext/nodes/oscillator/OscillatorNode.kt @@ -97,8 +97,7 @@ class OscillatorNode(context: BaseAudioContext, reactContext: ReactApplicationCo } wavePhase += phaseChange } - - context.dispatchAudio(buffer, audioTrack) + process(buffer, audioTrack) } audioTrack.flush() } diff --git a/example/src/App.tsx b/example/src/App.tsx index d38cb456..e3c9ce43 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -22,11 +22,11 @@ const App = () => { const startOscillator = () => { oscillatorRef.current?.start(); - //secondaryOscillatorRef.current?.start(); + secondaryOscillatorRef.current?.start(); }; const stopOscillator = () => { oscillatorRef.current?.stop(); - //secondaryOscillatorRef.current?.stop(); + secondaryOscillatorRef.current?.stop(); }; return (