diff --git a/.claude/skills/build-compilation-dependencies/SKILL.md b/.claude/skills/build-compilation-dependencies/SKILL.md index b7fe5f518..d8868f273 100644 --- a/.claude/skills/build-compilation-dependencies/SKILL.md +++ b/.claude/skills/build-compilation-dependencies/SKILL.md @@ -84,15 +84,19 @@ Downloaded artifacts land in: ### Files - `android/build.gradle` — Gradle library config +- `android/fix-prefab.gradle` — prefab publication workaround (ported from react-native-worklets) - `android/CMakeLists.txt` — Android CMake root (SIMD detection, RN version flags, delegates to subdirectory) - `android/src/main/cpp/audioapi/CMakeLists.txt` — actual build target (sources, prebuilt libs, include paths) +- `common/cpp/audioapi/EXTENSION_API.md` — stable C++ extension contract for dependent native modules +- `common/cpp/audioapi/compatibility/StableAPI.h` — single public C++ compatibility header for extensions ### Key behaviors - Feature flags (`newArchEnabled`, `disableAudioapiFFmpeg`) are read from app's `gradle.properties` and forwarded to both CMake and Kotlin `BuildConfig` - DSP sources always compiled with `-O3` regardless of overall build type - Sources gathered with `GLOB_RECURSE CONFIGURE_DEPENDS` — CMake re-runs automatically when files are added/removed -- Worklets must be merged before the audio API CMake build starts (explicit Gradle task dependency) - 16KB page size alignment enabled for Android 15+ +- **Extension API (prefab)**: single public C++ header ``; prefab publishes transitive headers needed to compile it (`prepareAudioApiHeadersForPrefabs`); `fix-prefab.gradle` ensures the `.so` is in prefab metadata. Contract: `EXTENSION_API.md` +- CMake exposes `COMMON_CPP_DIR` and `ANDROID_CPP_DIR` as **PUBLIC** include dirs so prefab consumers resolve `` For full per-line analysis see [build-details.md](build-details.md#android-androidcmakeliststxt-root--detailed-analysis). diff --git a/.claude/skills/build-compilation-dependencies/build-details.md b/.claude/skills/build-compilation-dependencies/build-details.md index 970e99d92..ad6440149 100644 --- a/.claude/skills/build-compilation-dependencies/build-details.md +++ b/.claude/skills/build-compilation-dependencies/build-details.md @@ -42,12 +42,31 @@ packagingOptions { } ``` -### Worklets dependency ordering - -Worklets must be merged before the audio API's CMake build starts. Gradle task dependency is wired explicitly: +### Prefab extension API ```groovy -tasks.getByName("buildCMakeDebug").dependsOn(rnWorkletsProject.tasks.getByName("mergeDebugNativeLibs")) +buildFeatures { + prefab true + prefabPublishing true +} + +prefab { + "react-native-audio-api" { + headers audioApiPrefabHeadersDir.absolutePath + } +} +``` + +`prepareAudioApiHeadersForPrefabs` copies `audioapi/**/*.{h,hpp}` (include-only, no +excludes — same approach as react-native-worklets). `fix-prefab.gradle` makes +`prefab*ConfigurePackage` depend on `externalNativeBuild*` and touches dependents' +`prefab_config.json`. + +Consumers: + +```cmake +find_package(react-native-audio-api REQUIRED CONFIG) +target_link_libraries(my-ext react-native-audio-api::react-native-audio-api) ``` ### Minimum RN version enforcement diff --git a/.claude/skills/turbo-modules/SKILL.md b/.claude/skills/turbo-modules/SKILL.md index e777b3bf3..4a315a18b 100644 --- a/.claude/skills/turbo-modules/SKILL.md +++ b/.claude/skills/turbo-modules/SKILL.md @@ -229,6 +229,22 @@ The JSI injection itself (`AudioAPIModuleInstaller`) is identical for both archi --- +## Pure C++ TurboModule library (`react-native-audio-worklets`) + +Extension packages that expose a **C++-only** TurboModule (no JNI implementation) use a hybrid Android setup: + +| Piece | Role | +|---|---| +| `react-native.config.js` `cxxModule*` fields | Registers `NativeAudioWorkletsModule` in app autolinking (`autolinking_cxxModuleProvider`) | +| `android/build.gradle` + `com.facebook.react` | Runs Codegen → `react_codegen_rnaudioworklets` (JSI spec headers) | +| `AudioWorkletsPackage.kt` | Stub `BaseReactPackage` so Gradle autolinking includes the library project | +| Root `CMakeLists.txt` | Static `react-native-audio-worklets` target; linked into `libappmodules.so` via autolinking | +| `common/cpp/NativeAudioWorkletsModule.h` | Forwarding header — autolinking includes `` | + +iOS uses `NativeAudioWorkletsModuleProvider.mm` + `codegenConfig.ios.modulesProvider` instead. + +RN guide: https://reactnative.dev/docs/the-new-architecture/pure-cxx-modules + --- *Maintenance: see [maintenance.md](maintenance.md).* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b592ce00..3051bc4ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,13 +14,22 @@ jobs: name: Format check run: yarn format:check - lint: + lint-audio-api: uses: ./.github/workflows/ci-check.yml with: - name: Lint and typecheck + name: Lint and typecheck (react-native-audio-api) run: | - yarn lint - yarn typecheck + yarn workspace react-native-audio-api run lint + yarn workspace react-native-audio-api run typecheck + + lint-worklets: + uses: ./.github/workflows/ci-check.yml + with: + name: Lint and typecheck (react-native-audio-worklets) + run: | + yarn workspace react-native-audio-api build + yarn workspace react-native-audio-worklets run lint + yarn workspace react-native-audio-worklets run typecheck check-audio-enum-sync: uses: ./.github/workflows/ci-check.yml @@ -28,8 +37,14 @@ jobs: name: Check AudioEvent enum sync run: yarn check-audio-enum-sync - build-library: + build-audio-api: + uses: ./.github/workflows/ci-check.yml + with: + name: Build (react-native-audio-api) + run: yarn workspace react-native-audio-api build + + build-worklets: uses: ./.github/workflows/ci-check.yml with: - name: Build package - run: yarn build + name: Build (react-native-audio-worklets) + run: yarn workspace react-native-audio-worklets build diff --git a/.gitignore b/.gitignore index c8951115a..1508a5a71 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,7 @@ android/keystores/debug.keystore .turbo/ packages/react-native-audio-api/lib +packages/react-native-audio-worklets/lib react-native-audio-api*.tgz # Android diff --git a/apps/common-app/package.json b/apps/common-app/package.json index 1fb4bc19b..4188e26e5 100644 --- a/apps/common-app/package.json +++ b/apps/common-app/package.json @@ -14,13 +14,14 @@ "@shopify/react-native-skia": "2.6.2", "lucide-react-native": "^0.555.0", "react-native-audio-api": "workspace:*", + "react-native-audio-worklets": "workspace:*", "react-native-background-timer": "2.4.1", "react-native-gesture-handler": "2.31.1", - "react-native-reanimated": "4.3.0", + "react-native-reanimated": "4.5.1", "react-native-safe-area-context": "5.7.0", "react-native-screens": "4.24.0", "react-native-svg": "15.15.4", - "react-native-worklets": "0.8.1" + "react-native-worklets": "0.10.1" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/apps/common-app/src/examples/Worklets/Worklets.tsx b/apps/common-app/src/examples/Worklets/Worklets.tsx index 9e142df36..172f38487 100644 --- a/apps/common-app/src/examples/Worklets/Worklets.tsx +++ b/apps/common-app/src/examples/Worklets/Worklets.tsx @@ -2,216 +2,202 @@ import { useEffect, useRef, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { AudioContext, - AudioManager, - WorkletNode, - WorkletProcessingNode, - WorkletSourceNode, + OscillatorNode, } from 'react-native-audio-api'; +import { WorkletNode } from 'react-native-audio-worklets'; import Animated, { Extrapolation, interpolate, useAnimatedStyle, useSharedValue, withSpring, + type SharedValue, } from 'react-native-reanimated'; -import { Button, Container } from '../../components'; +import { Button, Container, Switch } from '../../components'; import { colors } from '../../styles'; +const HEAVY_JS_ITERATIONS = 4_000_000; + +function runHeavyJsWork(): number { + let acc = 0; + for (let i = 0; i < HEAVY_JS_ITERATIONS; i++) { + acc += Math.sin(i * 0.001) * Math.cos(i * 0.0013); + } + return acc; +} + +function VisualizerBar({ + amplitude, + index, +}: { + amplitude: SharedValue; + index: number; +}) { + const animatedStyle = useAnimatedStyle(() => { + const centerIndex = 2; + const distanceFromCenter = Math.abs(index - centerIndex); + + const height = interpolate( + amplitude.value, + [0, 1], + [10, 200], + Extrapolation.CLAMP + ); + const red = interpolate( + amplitude.value, + [0, 1], + [0, 255], + Extrapolation.CLAMP + ); + const green = interpolate( + amplitude.value, + [0, 1], + [255, 0], + Extrapolation.CLAMP + ); + const opacity = 1 - distanceFromCenter * 0.15; + + return { + height, + backgroundColor: `rgba(${Math.floor(red)}, ${Math.floor(green)}, 0, ${opacity})`, + }; + }); + + return ; +} + function Worklets() { - const SAMPLE_RATE = 44100; - const aCtxRef = useRef(null); + const audioContextRef = useRef(null); const workletNodeRef = useRef(null); - const workletProcessingNodeRef = useRef(null); - const workletSourceNodeRef = useRef(null); + const oscillatorRef = useRef(null); + const lfoRef = useRef(null); + const heavyWorkAccRef = useRef(0); + const jsWorkloadTimerRef = useRef | null>(null); + const [isPlaying, setIsPlaying] = useState(false); + const [heavyJsLoad, setHeavyJsLoad] = useState(false); const bar0 = useSharedValue(0); const bar1 = useSharedValue(0); - const bar2 = useSharedValue(0); // center bar + const bar2 = useSharedValue(0); const bar3 = useSharedValue(0); const bar4 = useSharedValue(0); useEffect(() => { - if (!aCtxRef.current) { - aCtxRef.current = new AudioContext({ sampleRate: SAMPLE_RATE }); + if (!audioContextRef.current) { + audioContextRef.current = new AudioContext(); } - AudioManager.setAudioSessionOptions({ - iosCategory: 'playAndRecord', - iosMode: 'spokenAudio', - iosOptions: ['defaultToSpeaker', 'allowBluetoothA2DP'], - }); - return () => { - aCtxRef.current?.close(); + audioContextRef.current?.close(); }; }, []); - const start = () => { - setIsPlaying(true); - const processingWorklet = ( - inputAudioData: Array, - outputAudioData: Array, - framesToProcess: number, - _currentTime: number - ) => { - 'worklet'; - const gain = 0.5; - for (let channel = 0; channel < inputAudioData.length; channel++) { - const inputChannelData = inputAudioData[channel]; - const outputChannelData = outputAudioData[channel]; - for (let i = 0; i < framesToProcess; i++) { - outputChannelData[i] = inputChannelData[i] * gain; - } + useEffect(() => { + if (!heavyJsLoad) { + if (jsWorkloadTimerRef.current != null) { + clearInterval(jsWorkloadTimerRef.current); + jsWorkloadTimerRef.current = null; } - }; + return; + } - const sourceWorklet = ( - audioData: Array, - framesToProcess: number, - currentTime: number, - _startOffset: number - ) => { - 'worklet'; - const frequency = 440; // A4 note - const baseAmplitude = 0.2; - - const modulationFreq = 2; // 2 Hz modulation - const modulationDepth = 0.8; - const amplitudeModulation = Math.sin( - 2 * Math.PI * modulationFreq * currentTime - ); - const dynamicAmplitude = - baseAmplitude * (1 + modulationDepth * amplitudeModulation); - - for (let channel = 0; channel < audioData.length; channel++) { - for (let sample = 0; sample < framesToProcess; sample++) { - // Calculate phase based on sample position and time - const phase = - 2 * Math.PI * frequency * (currentTime + sample / SAMPLE_RATE); - audioData[channel][sample] = dynamicAmplitude * Math.sin(phase); - } + jsWorkloadTimerRef.current = setInterval(() => { + heavyWorkAccRef.current += runHeavyJsWork(); + }, 0); + + return () => { + if (jsWorkloadTimerRef.current != null) { + clearInterval(jsWorkloadTimerRef.current); + jsWorkloadTimerRef.current = null; } }; - const worklet = ( - audioData: Array, - _inputChannelCount: number - ) => { - 'worklet'; - - // Calculates RMS amplitude - let sum = 0; - for (let i = 0; i < audioData[0].length; i++) { - sum += audioData[0][i] * audioData[0][i]; + }, [heavyJsLoad]); + + const start = () => { + const ctx = audioContextRef.current; + if (isPlaying || !ctx) { + return; + } + + workletNodeRef.current = new WorkletNode( + ctx, + (audioBuffers, numberOfChannels) => { + 'worklet'; + if (numberOfChannels < 1) { + return; + } + const buffer = audioBuffers[0]; + if (buffer == null) { + return; + } + const channel = new Float32Array(buffer); + let sum = 0; + for (let i = 0; i < channel.length; i++) { + sum += channel[i] * channel[i]; + } + const rms = Math.sqrt(sum / channel.length); + const scaledAmplitude = Math.min(rms * 4, 1); + + bar0.value = withSpring(bar1.value, { damping: 18, stiffness: 120 }); + bar1.value = withSpring(bar2.value, { damping: 18, stiffness: 120 }); + bar3.value = withSpring(bar2.value, { damping: 18, stiffness: 120 }); + bar4.value = withSpring(bar3.value, { damping: 18, stiffness: 120 }); + bar2.value = withSpring(scaledAmplitude, { + damping: 18, + stiffness: 120, + }); } - const rms = Math.sqrt(sum / audioData[0].length); + ); - // Increased scaling for better visualization - const scaledAmplitude = Math.min(rms * 25, 1); + const oscillator = ctx.createOscillator(); + oscillator.frequency.value = 440; - // console.log(`RMS: ${rms}, Scaled: ${scaledAmplitude}`); + const lfo = ctx.createOscillator(); + lfo.frequency.value = 2; + const lfoGain = ctx.createGain(); + lfoGain.gain.value = 0.18; + const amp = ctx.createGain(); + amp.gain.value = 0.25; - bar0.value = withSpring(bar1.value, { damping: 15, stiffness: 200 }); - bar1.value = withSpring(bar2.value, { damping: 15, stiffness: 200 }); - bar3.value = withSpring(bar2.value, { damping: 15, stiffness: 200 }); - bar4.value = withSpring(bar3.value, { damping: 15, stiffness: 200 }); - bar2.value = withSpring(scaledAmplitude, { damping: 15, stiffness: 200 }); - }; + lfo.connect(lfoGain); + lfoGain.connect(amp.gain); + oscillator.connect(amp); + amp.connect(workletNodeRef.current); + workletNodeRef.current.connect(ctx.destination); - if (!aCtxRef.current) { - aCtxRef.current = new AudioContext({ sampleRate: SAMPLE_RATE }); - } + oscillator.start(); + lfo.start(); - workletSourceNodeRef.current = aCtxRef.current.createWorkletSourceNode( - sourceWorklet, - 'AudioRuntime' - ); - workletNodeRef.current = aCtxRef.current.createWorkletNode( - worklet, - 256, - 1, - 'UIRuntime' - ); - workletProcessingNodeRef.current = - aCtxRef.current.createWorkletProcessingNode( - processingWorklet, - 'AudioRuntime' - ); - - // Connect nodes - workletSourceNodeRef.current.connect(workletProcessingNodeRef.current); - workletProcessingNodeRef.current.connect(workletNodeRef.current); - workletSourceNodeRef.current.connect(workletNodeRef.current); - workletNodeRef.current.connect(aCtxRef.current.destination); - - workletSourceNodeRef.current.start(); - if (aCtxRef.current.state === 'suspended') { - aCtxRef.current.resume(); + oscillatorRef.current = oscillator; + lfoRef.current = lfo; + + if (ctx.state === 'suspended') { + ctx.resume(); } + + setIsPlaying(true); }; const stop = () => { - console.log('Recording stopped'); - workletSourceNodeRef.current?.stop(); + oscillatorRef.current?.stop(); + lfoRef.current?.stop(); + workletNodeRef.current?.disconnect(); + + oscillatorRef.current = null; + lfoRef.current = null; + workletNodeRef.current = null; + bar0.value = withSpring(0, { damping: 20, stiffness: 100 }); bar1.value = withSpring(0, { damping: 20, stiffness: 100 }); bar2.value = withSpring(0, { damping: 20, stiffness: 100 }); bar3.value = withSpring(0, { damping: 20, stiffness: 100 }); bar4.value = withSpring(0, { damping: 20, stiffness: 100 }); + setIsPlaying(false); }; - const createBarStyle = (index: number) => { - return useAnimatedStyle(() => { - let amplitude = 0; - - switch (index) { - case 0: - amplitude = bar0.value; - break; - case 1: - amplitude = bar1.value; - break; - case 2: - amplitude = bar2.value; - break; - case 3: - amplitude = bar3.value; - break; - case 4: - amplitude = bar4.value; - break; - } - - const centerIndex = 2; - const distanceFromCenter = Math.abs(index - centerIndex); - - const height = interpolate( - amplitude, - [0, 1], - [10, 200], - Extrapolation.CLAMP - ); - - // Interpolate red component: 0 (quiet) -> 255 (loud) - const red = interpolate(amplitude, [0, 1], [0, 255], Extrapolation.CLAMP); - - // Interpolate green component: 255 (quiet) -> 0 (loud) - const green = interpolate( - amplitude, - [0, 1], - [255, 0], - Extrapolation.CLAMP - ); - - const opacity = 1 - distanceFromCenter * 0.15; - - return { - height, - backgroundColor: `rgba(${Math.floor(red)}, ${Math.floor(green)}, 0, ${opacity})`, - }; - }); - }; + const barAmplitudes = [bar0, bar1, bar2, bar3, bar4]; return ( @@ -219,16 +205,18 @@ function Worklets() { Audio Worklets Visualizer - Listen to the generated sine wave with dynamic animation + Oscillator → WorkletNode (RMS on UI runtime) → destination + + Heavy JS workload + + + - {Array.from({ length: 5 }, (_, index) => ( - + {barAmplitudes.map((amplitude, index) => ( + ))} @@ -250,14 +238,25 @@ const styles = StyleSheet.create({ }, subtitle: { fontSize: 14, - marginBottom: 30, + marginBottom: 24, textAlign: 'center', }, + toggleRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 16, + paddingHorizontal: 8, + }, + toggleLabel: { + color: colors.white, + fontSize: 14, + }, visualizer: { height: 250, justifyContent: 'flex-end', alignItems: 'center', - marginVertical: 30, + marginVertical: 16, borderRadius: 10, padding: 20, }, diff --git a/apps/fabric-example/babel.config.js b/apps/fabric-example/babel.config.js index da69d304d..0356993e0 100644 --- a/apps/fabric-example/babel.config.js +++ b/apps/fabric-example/babel.config.js @@ -3,7 +3,6 @@ module.exports = function (api) { return { presets: ['module:@react-native/babel-preset'], plugins: [ - 'react-native-worklets/plugin', [ 'module-resolver', { @@ -25,6 +24,7 @@ module.exports = function (api) { ], }, ], + 'react-native-worklets/plugin', ], }; }; diff --git a/apps/fabric-example/ios/Podfile.lock b/apps/fabric-example/ios/Podfile.lock index f4013d90d..26b95a6a6 100644 --- a/apps/fabric-example/ios/Podfile.lock +++ b/apps/fabric-example/ios/Podfile.lock @@ -1864,7 +1864,6 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - RNAudioAPI/audioapi (= 1.0.0) - - RNWorklets - Yoga - RNAudioAPI/audioapi (1.0.0): - hermes-engine @@ -1890,7 +1889,6 @@ PODS: - RNAudioAPI/audioapi/audioapi_dsp (= 1.0.0) - RNAudioAPI/audioapi/ios (= 1.0.0) - RNAudioAPI/audioapi/miniaudio_impl (= 1.0.0) - - RNWorklets - Yoga - RNAudioAPI/audioapi/audioapi_dsp (1.0.0): - hermes-engine @@ -1913,7 +1911,6 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNWorklets - Yoga - RNAudioAPI/audioapi/ios (1.0.0): - hermes-engine @@ -1936,7 +1933,6 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNWorklets - Yoga - RNAudioAPI/audioapi/miniaudio_impl (1.0.0): - hermes-engine @@ -1959,6 +1955,29 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies + - Yoga + - RNAudioWorklets (1.0.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNAudioAPI - RNWorklets - Yoga - RNGestureHandler (2.31.1): @@ -1983,7 +2002,34 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNReanimated (4.3.0): + - RNReanimated (4.5.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNReanimated/apple (= 4.5.1) + - RNReanimated/common (= 4.5.1) + - RNReanimated/view (= 4.5.1) + - RNWorklets + - Yoga + - RNReanimated/apple (4.5.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2005,11 +2051,9 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNReanimated/apple (= 4.3.0) - - RNReanimated/common (= 4.3.0) - RNWorklets - Yoga - - RNReanimated/apple (4.3.0): + - RNReanimated/common (4.5.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2033,7 +2077,7 @@ PODS: - ReactNativeDependencies - RNWorklets - Yoga - - RNReanimated/common (4.3.0): + - RNReanimated/view (4.5.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2149,7 +2193,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNWorklets (0.8.1): + - RNWorklets (0.10.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2171,10 +2215,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNWorklets/apple (= 0.8.1) - - RNWorklets/common (= 0.8.1) + - RNWorklets/apple (= 0.10.1) + - RNWorklets/common (= 0.10.1) - Yoga - - RNWorklets/apple (0.8.1): + - RNWorklets/apple (0.10.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2197,7 +2241,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNWorklets/common (0.8.1): + - RNWorklets/common (0.10.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2300,6 +2344,7 @@ DEPENDENCIES: - ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`) - ReactNativeDependencies (from `../../../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`) - RNAudioAPI (from `../../../node_modules/react-native-audio-api`) + - RNAudioWorklets (from `../../../node_modules/react-native-audio-worklets`) - RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`) - RNReanimated (from `../../../node_modules/react-native-reanimated`) - RNScreens (from `../../../node_modules/react-native-screens`) @@ -2461,6 +2506,8 @@ EXTERNAL SOURCES: :podspec: "../../../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec" RNAudioAPI: :path: "../../../node_modules/react-native-audio-api" + RNAudioWorklets: + :path: "../../../node_modules/react-native-audio-worklets" RNGestureHandler: :path: "../../../node_modules/react-native-gesture-handler" RNReanimated: @@ -2476,7 +2523,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: c00c20551d40126351a6783c47ce75f5b374851b - hermes-engine: 91023181d4bc5948b457de5314623fbfe4f8604e + hermes-engine: c399a2e224a0b13c589d76b4fc05e14bdd76fa88 RCTDeprecation: 3bb167081b134461cfeb875ff7ae1945f8635257 RCTRequired: 74839f55d5058a133a0bc4569b0afec750957f64 RCTSwiftUI: 87a316382f3eab4dd13d2a0d0fd2adcce917361a @@ -2485,7 +2532,7 @@ SPEC CHECKSUMS: React: 1b1536b9099195944034e65b1830f463caaa8390 React-callinvoker: 6dff6d17d1d6cc8fdf85468a649bafed473c65f5 React-Core: 00faa4d038298089a1d5a5b21dde8660c4f0820d - React-Core-prebuilt: a6d614de037caff7898424dfc22915ec792de921 + React-Core-prebuilt: ab26be1216323aea7c76f96ca450bffa7bcd4a72 React-CoreModules: a17807f849bfd86045b0b9a75ec8c19373b482f6 React-cxxreact: c7b53ace5827be54048288bce5c55f337c41e95f React-debug: e1f00fcd2cef58a2897471a6d76a4ef5f5f90c74 @@ -2549,13 +2596,14 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 5787b37b8e2e51dfeab697ec031cc7c4080dcea2 ReactCodegen: d07ee3c8db75b43d1cbe479ae6affebf9925c733 ReactCommon: fe2a3af8975e63efa60f95fca8c34dc85deee360 - ReactNativeDependencies: 4d5ce2683b6d74f7c686bf90a88c7d381295cf3c - RNAudioAPI: 555be5f8e643f733db95096a9c6902e43bddec23 + ReactNativeDependencies: 212738cc51e6c4cc34ee487890497d6f41979ec0 + RNAudioAPI: 6bba1527c091e2702e3d879de7564ef1ccf30a78 + RNAudioWorklets: febe470be646585d9b8b0de320a5fb8684cb012c RNGestureHandler: 187c5c7936abf427bc4d22d6c3b1ac80ad1f63c0 - RNReanimated: 64f4b3b33b48b19e0ba76a352571b52b1e931981 + RNReanimated: 3a204af3747842859392545d41d443b9c5e428a3 RNScreens: 01b065ded2dfe7987bcce770ff3a196be417ff41 RNSVG: 04044c3abcf177fd674a1a3d13097efa1adebcbe - RNWorklets: 533b896a04be761c26ec519014a6ccd0fac03781 + RNWorklets: 63cb8f15b2886288da888ac65cc21934c9ebc80a Yoga: e83c3121d079541e69f3c5c623faaaf933fb5812 PODFILE CHECKSUM: 435ccb12d8f72abbb2424d54f6ec603f419769fd diff --git a/package.json b/package.json index 65f06acc8..a02522d15 100644 --- a/package.json +++ b/package.json @@ -6,15 +6,16 @@ "workspaces": { "packages": [ "packages/react-native-audio-api", + "packages/react-native-audio-worklets", "apps/common-app", "apps/fabric-example" ] }, "scripts": { - "build": "yarn workspaces foreach -A -p run build", + "build": "yarn workspace react-native-audio-api run build && yarn workspace react-native-audio-worklets run build", "lint": "yarn workspaces foreach -A -p run lint", "format": "yarn workspaces foreach -A -p run format", - "format:check": "yarn workspace react-native-audio-api run format:check", + "format:check": "yarn workspace react-native-audio-api run format:check && yarn workspace react-native-audio-worklets run format:check", "clean": "del-cli packages/**/android/build apps/**/android/build apps/**/android/app/build apps/**/ios/build packages/**/lib node_modules apps/**/node_modules packages/**/node_modules", "typecheck": "yarn workspaces foreach -A -p run typecheck", "test": "yarn workspace react-native-audio-api run test", diff --git a/packages/react-native-audio-api/RNAudioAPI.podspec b/packages/react-native-audio-api/RNAudioAPI.podspec index 691a2d0cd..ebf5e2ea4 100644 --- a/packages/react-native-audio-api/RNAudioAPI.podspec +++ b/packages/react-native-audio-api/RNAudioAPI.podspec @@ -12,9 +12,6 @@ fabric_flags = $new_arch_enabled ? '-DRCT_NEW_ARCH_ENABLED' : '' version_flag = "-DAUDIOAPI_VERSION=#{package_json['version']}" ios_min_version = '14.0' -worklets_enabled = $audio_api_config[:worklets_enabled] -worklets_preprocessor_flag = worklets_enabled ? '-DRN_AUDIO_API_ENABLE_WORKLETS=1' : '' - ffmpeg_flag = $RN_AUDIO_API_FFMPEG_DISABLED ? '-DRN_AUDIO_API_FFMPEG_DISABLED=1' : '' static_external_libs_flag = $RN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED ? '-DRN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED=1 -DMA_NO_LIBOPUS=1 -DMA_NO_LIBVORBIS=1' : '' skip_ffmpeg_argument = $RN_AUDIO_API_FFMPEG_DISABLED ? 'skipffmpeg' : '' @@ -59,10 +56,6 @@ Pod::Spec.new do |s| end end - if worklets_enabled - s.dependency 'RNWorklets' - end - s.ios.frameworks = 'Accelerate', 'AVFoundation', 'MediaPlayer' s.prepare_command = <<-CMD @@ -141,16 +134,11 @@ Pod::Spec.new do |s| "\"$(PODS_TARGET_SRCROOT)/#{external_dir_relative}/include/vorbis\"" ]) .concat($RN_AUDIO_API_FFMPEG_DISABLED ? [] : ["\"$(PODS_TARGET_SRCROOT)/#{external_dir_relative}/include_ffmpeg\""]) - .concat(worklets_enabled ? [ - '"$(PODS_ROOT)/Headers/Public/RNWorklets"', - '"$(PODS_ROOT)/Headers/Private/ReactCodegen"', - '"$(PODS_ROOT)/../build/generated/ios/ReactCodegen"', - ] : []) .join(' '), "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", "GCC_PREPROCESSOR_DEFINITIONS" => '$(inherited) HAVE_ACCELERATE=1', "GCC_PREPROCESSOR_DEFINITIONS[config=Debug]" => '$(inherited) HAVE_ACCELERATE=1 DEBUG=1', - 'OTHER_CFLAGS' => "$(inherited) #{fabric_flags} #{version_flag} #{worklets_preprocessor_flag} #{ffmpeg_flag} #{static_external_libs_flag}", + 'OTHER_CFLAGS' => "$(inherited) #{fabric_flags} #{version_flag} #{ffmpeg_flag} #{static_external_libs_flag}", } s.xcconfig = { @@ -165,13 +153,6 @@ Pod::Spec.new do |s| "\"$(PODS_ROOT)/#{$audio_api_config[:dynamic_frameworks_audio_api_dir]}/ios\"", "\"$(PODS_ROOT)/#{$audio_api_config[:dynamic_frameworks_audio_api_dir]}/common/cpp\"", ] - .concat(worklets_enabled ? [ - '"$(PODS_ROOT)/Headers/Public/RNWorklets"', - '"$(PODS_ROOT)/Headers/Private/ReactCodegen"', - '"$(PODS_ROOT)/../build/generated/ios/ReactCodegen"', - "\"$(PODS_ROOT)/#{$audio_api_config[:dynamic_frameworks_worklets_dir]}/apple\"", - "\"$(PODS_ROOT)/#{$audio_api_config[:dynamic_frameworks_worklets_dir]}/Common/cpp\"" - ] : []) .join(' '), 'OTHER_LDFLAGS' => %W[ $(inherited) diff --git a/packages/react-native-audio-api/android/build.gradle b/packages/react-native-audio-api/android/build.gradle index 0033a2b24..8305f9c7d 100644 --- a/packages/react-native-audio-api/android/build.gradle +++ b/packages/react-native-audio-api/android/build.gradle @@ -74,39 +74,6 @@ def resolveReactNativeDirectory() { ) } -def resolveReactNativeWorkletsDirectory() { - def rnWorklets = rootProject.subprojects.find { it.name == 'react-native-worklets' } - if (rnWorklets != null) { - return rnWorklets.projectDir - } - - return null; -} - -def validateWorkletsVersion() { - def validationScript = file("${projectDir}/../scripts/validate-worklets-version.js") - if (!validationScript.exists()) { - logger.error("[AudioAPI] Worklets validation script not found at ${validationScript.absolutePath}") - return false - } - - try { - def process = ["node", validationScript.absolutePath].execute() - process.waitForProcessOutput(System.out, System.err) - def exitCode = process.exitValue() - - if (exitCode == 0) { - return true - } else { - logger.warn("[AudioAPI] Worklets version validation failed") - return false - } - } catch (Exception e) { - logger.error("[AudioAPI] Failed to validate worklets version: ${e.message}") - return false - } -} - def toPlatformFileString(String path) { if (Os.isFamily(Os.FAMILY_WINDOWS)) { path = path.replace(File.separatorChar, '/' as char) @@ -123,9 +90,35 @@ static def supportsNamespace() { return (major == 7 && minor >= 3) || major >= 8 } +def getAudioApiVersion() { + def inputFile = file("${projectDir.path}/../package.json") + def json = new groovy.json.JsonSlurper().parseText(inputFile.text) + def packageVersion = json.version + if (packageVersion == null) { + throw new GradleException("[AudioAPI] Cannot find version in package.json") + } + return packageVersion.toString() +} + def reactNativeRootDir = resolveReactNativeDirectory() -def reactNativeWorkletsRootDir = resolveReactNativeWorkletsDirectory() -def isWorkletsAvailable = reactNativeWorkletsRootDir != null && validateWorkletsVersion() +def audioApiPackageDir = project.projectDir.parentFile +def AUDIO_API_VERSION = getAudioApiVersion() + +// --- Prefab extension API (common/cpp/audioapi/EXTENSION_API.md) ---------------- +// Extension packages must include only and link +// react-native-audio-api::react-native-audio-api via find_package(). + +// Set version for prefab +version AUDIO_API_VERSION + +def audioApiCommonCppDir = file("$audioApiPackageDir/common/cpp") +def audioApiAndroidCppDir = file("$projectDir/src/main/cpp") + +// build/prefab-headers/audioapi/... — staged for the react-native-audio-api prefab. +def audioApiPrefabHeadersDir = layout.buildDirectory.dir("prefab-headers").get().asFile + +// Headers staged for the react-native-audio-api prefab (mirrors worklets: include-only). +def audioApiPrefabHeaderIncludes = ["audioapi/**/*.h", "audioapi/**/*.hpp"] def reactProperties = new Properties() file("$reactNativeRootDir/ReactAndroid/gradle.properties").withInputStream { reactProperties.load(it) } @@ -136,6 +129,8 @@ def IS_NEW_ARCHITECTURE_ENABLED = isNewArchitectureEnabled() def IS_RN_AUDIO_API_FFMPEG_DISABLED = isFFmpegDisabled() def IS_RN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED = isStaticExternalLibsDisabled() +apply from: "./fix-prefab.gradle" + android { sourceSets { main { @@ -162,7 +157,6 @@ android { consumerProguardFiles("proguard-rules.pro") buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() - buildConfigField "boolean", "RN_AUDIO_API_ENABLE_WORKLETS", "${isWorkletsAvailable}" buildConfigField "boolean", "RN_AUDIO_API_FFMPEG_DISABLED", isFFmpegDisabled().toString() buildConfigField "boolean", "RN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED", isStaticExternalLibsDisabled().toString() @@ -175,16 +169,12 @@ android { "-DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}", "-DANDROID_TOOLCHAIN=clang", "-DREACT_NATIVE_DIR=${toPlatformFileString(reactNativeRootDir.path)}", - "-DRN_AUDIO_API_WORKLETS_ENABLED=${isWorkletsAvailable}", "-DIS_NEW_ARCHITECTURE_ENABLED=${IS_NEW_ARCHITECTURE_ENABLED}", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON", "-DRN_AUDIO_API_FFMPEG_DISABLED=${IS_RN_AUDIO_API_FFMPEG_DISABLED}", "-DRN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED=${IS_RN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED}" ] - if (isWorkletsAvailable) { - cmakeArgs << "-DREACT_NATIVE_WORKLETS_DIR=${toPlatformFileString(reactNativeWorkletsRootDir.path)}" - } arguments = cmakeArgs } } @@ -230,6 +220,14 @@ android { buildFeatures { buildConfig true prefab true + prefabPublishing true + } + + // Publish compatibility/StableAPI.h (+ transitive headers) for extension packages. + prefab { + "react-native-audio-api" { + headers audioApiPrefabHeadersDir.absolutePath + } } buildTypes { @@ -290,24 +288,8 @@ dependencies { implementation 'com.facebook.fbjni:fbjni:0.6.0' implementation 'com.google.oboe:oboe:1.9.3' implementation 'androidx.media:media:1.7.0' - - if (isWorkletsAvailable) { - implementation(rootProject.subprojects.find { it.name == 'react-native-worklets' }) - } } -if (isWorkletsAvailable) { - // Ensure worklets is built before react-native-audio-api - def rnWorkletsProject = rootProject.subprojects.find { it.name == 'react-native-worklets' } - evaluationDependsOn(rnWorkletsProject.path) - - afterEvaluate { - tasks.getByName("buildCMakeDebug").dependsOn(rnWorkletsProject.tasks.getByName("mergeDebugNativeLibs")) - tasks.getByName("buildCMakeRelWithDebInfo").dependsOn(rnWorkletsProject.tasks.getByName("mergeReleaseNativeLibs")) - } -} - - def assertMinimalReactNativeVersion = task assertMinimalReactNativeVersionTask { // If you change the minimal React Native version remember to update Compatibility Table in docs def minimalReactNativeVersion = 76 @@ -328,10 +310,21 @@ task downloadPrebuiltBinaries(type: Exec) { environment 'DISABLE_AUDIOAPI_STATIC_EXTERNAL_LIBS', isStaticExternalLibsDisabled() ? '1' : '0' } +tasks.register('prepareAudioApiHeadersForPrefabs', Copy) { + from(audioApiCommonCppDir) { + include(audioApiPrefabHeaderIncludes) + } + from(audioApiAndroidCppDir) { + include(audioApiPrefabHeaderIncludes) + } + into(audioApiPrefabHeadersDir) +} + // Make preBuild depend on the download task tasks.preBuild { dependsOn assertMinimalReactNativeVersion dependsOn downloadPrebuiltBinaries + dependsOn prepareAudioApiHeadersForPrefabs } task cleanCmakeCache() { diff --git a/packages/react-native-audio-api/android/fix-prefab.gradle b/packages/react-native-audio-api/android/fix-prefab.gradle new file mode 100644 index 000000000..3788e7e7b --- /dev/null +++ b/packages/react-native-audio-api/android/fix-prefab.gradle @@ -0,0 +1,61 @@ +// Ensures prefab publications include the built .so, not just headers. +// Ported from react-native-worklets/android/fix-prefab.gradle.kts. +// +// 1. Makes prefab*ConfigurePackage tasks depend on externalNativeBuild* so the +// native library exists before prefab metadata is generated. +// 2. Touches dependent projects' prefab_config.json so their CMake glue is +// regenerated with this library's .so in the link line. + +tasks.configureEach { task -> + def matcher = (task.name =~ /^prefab(.+)ConfigurePackage$/) + if (matcher.matches()) { + def variantName = matcher.group(1) + task.outputs.upToDateWhen { false } + task.dependsOn("externalNativeBuild${variantName}") + } +} + +afterEvaluate { + def abis = reactNativeArchitectures() + + rootProject.allprojects.each { proj -> + if (proj == rootProject) { + return + } + + def dependsOnThisLib = proj.configurations.any { config -> + config.dependencies.any { dep -> + (dep.group == project.group && dep.name == project.name) || + dep.name == project.name + } + } + if (!dependsOnThisLib && proj != project) { + return + } + + if (!proj.plugins.hasPlugin('com.android.application') && + !proj.plugins.hasPlugin('com.android.library')) { + return + } + + // Touch prefab_config.json in dependent native builds so AGP re-runs the + // prefab CLI and emits a *Config.cmake that links libreact-native-audio-api.so. + // See ExternalNativeJsonGenerator.kt in Android Gradle Plugin. + def cxxDir = new File(proj.projectDir, '.cxx') + if (!cxxDir.exists()) { + return + } + + cxxDir.eachDir { variantDir -> + def randomDirs = variantDir.listFiles({ File f -> f.isDirectory() } as FileFilter) + abis.each { abi -> + randomDirs?.each { randomDir -> + def prefabFile = new File(randomDir, "${abi}/prefab_config.json") + if (prefabFile.exists()) { + prefabFile.setLastModified(System.currentTimeMillis()) + } + } + } + } + } +} diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/CMakeLists.txt b/packages/react-native-audio-api/android/src/main/cpp/audioapi/CMakeLists.txt index accaba6ff..46642e88b 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/CMakeLists.txt +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/CMakeLists.txt @@ -73,24 +73,27 @@ else() set(BUILD_TYPE "release") endif() +# PUBLIC: extension packages consume `` via the published prefab. target_include_directories( react-native-audio-api + PUBLIC + "${COMMON_CPP_DIR}" + "${ANDROID_CPP_DIR}" PRIVATE - "${COMMON_CPP_DIR}" - "${ANDROID_CPP_DIR}" - "${INCLUDE_DIR}" - "${FFMPEG_INCLUDE_DIR}" - "${REACT_NATIVE_DIR}/ReactCommon" - "${REACT_NATIVE_DIR}/ReactAndroid/src/main/jni/react/turbomodule" - "${REACT_NATIVE_DIR}/ReactCommon/callinvoker" + "${INCLUDE_DIR}" + "${FFMPEG_INCLUDE_DIR}" + "${REACT_NATIVE_DIR}/ReactCommon" + "${REACT_NATIVE_DIR}/ReactCommon/jsiexecutor" + "${REACT_NATIVE_DIR}/ReactAndroid/src/main/jni/react/turbomodule" + "${REACT_NATIVE_DIR}/ReactCommon/callinvoker" ) if(NOT RN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED) target_include_directories( react-native-audio-api PRIVATE - "${INCLUDE_DIR}/opus" - "${INCLUDE_DIR}/vorbis" + "${INCLUDE_DIR}/opus" + "${INCLUDE_DIR}/vorbis" ) endif() @@ -102,67 +105,10 @@ set(LINK_LIBRARIES oboe::oboe ) -set(INCLUDE_LIBRARIES - "${COMMON_CPP_DIR}" - "${ANDROID_CPP_DIR}" - "${INCLUDE_DIR}" - "${REACT_NATIVE_DIR}/ReactCommon" - "${REACT_NATIVE_DIR}/ReactCommon/jsiexecutor" - "${REACT_NATIVE_DIR}/ReactAndroid/src/main/jni/react/turbomodule" - "${REACT_NATIVE_DIR}/ReactCommon/callinvoker" -) - -if(NOT RN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED) - list(APPEND INCLUDE_LIBRARIES - "${INCLUDE_DIR}/opus" - "${INCLUDE_DIR}/vorbis" - ) -endif() - -if(RN_AUDIO_API_WORKLETS_ENABLED) - list(APPEND INCLUDE_LIBRARIES - "${REACT_NATIVE_WORKLETS_DIR}/../Common/cpp" - "${REACT_NATIVE_WORKLETS_DIR}/src/main/cpp" - ) - - find_package(react-native-worklets CONFIG QUIET) - if(react-native-worklets_FOUND) - list(APPEND LINK_LIBRARIES react-native-worklets::worklets) - else() - # Fallback for environments without prefab publication (older worklets) - add_library(worklets SHARED IMPORTED) - set_target_properties( - worklets - PROPERTIES - IMPORTED_LOCATION - "${REACT_NATIVE_WORKLETS_DIR}/build/intermediates/cmake/${BUILD_TYPE}/obj/${ANDROID_ABI}/libworklets.so" - ) - list(APPEND LINK_LIBRARIES worklets) - endif() -endif() - -target_include_directories( - react-native-audio-api - PRIVATE - ${INCLUDE_LIBRARIES} -) - set(RN_VERSION_LINK_LIBRARIES ReactAndroid::reactnative ) -if(RN_AUDIO_API_WORKLETS_ENABLED) - target_compile_definitions( - react-native-audio-api - PRIVATE RN_AUDIO_API_ENABLE_WORKLETS=1 - ) -else() - target_compile_definitions( - react-native-audio-api - PRIVATE RN_AUDIO_API_ENABLE_WORKLETS=0 - ) -endif() - if (RN_AUDIO_API_FFMPEG_DISABLED) target_compile_definitions( react-native-audio-api diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.cpp index d8d9005c6..943c42a5c 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.cpp @@ -9,38 +9,20 @@ using namespace facebook::jni; AudioAPIModule::AudioAPIModule( jni::alias_ref &jThis, -#if RN_AUDIO_API_ENABLE_WORKLETS - std::weak_ptr weakWorkletsModuleProxy, -#endif jsi::Runtime *jsiRuntime, const std::shared_ptr &jsCallInvoker) - : javaPart_(make_global(jThis)), - jsiRuntime_(jsiRuntime), -#if RN_AUDIO_API_ENABLE_WORKLETS - weakWorkletsModuleProxy_(weakWorkletsModuleProxy), -#endif - jsCallInvoker_(jsCallInvoker) { + : javaPart_(make_global(jThis)), jsiRuntime_(jsiRuntime), jsCallInvoker_(jsCallInvoker) { audioEventHandlerRegistry_ = std::make_shared(jsiRuntime, jsCallInvoker); } jni::local_ref AudioAPIModule::initHybrid( jni::alias_ref jThis, - jni::alias_ref jWorkletsModule, jlong jsContext, jni::alias_ref jsCallInvokerHolder) { auto jsCallInvoker = jsCallInvokerHolder->cthis()->getCallInvoker(); auto rnRuntime = reinterpret_cast(jsContext); -#if RN_AUDIO_API_ENABLE_WORKLETS - if (jWorkletsModule) { - auto castedModule = jni::static_ref_cast(jWorkletsModule); - auto workletsModuleProxy = castedModule->cthis()->getWorkletsModuleProxy(); - return makeCxxInstance(jThis, workletsModuleProxy, rnRuntime, jsCallInvoker); - } - throw std::runtime_error("Worklets module is required but not provided from Java/Kotlin side"); -#else return makeCxxInstance(jThis, rnRuntime, jsCallInvoker); -#endif } void AudioAPIModule::registerNatives() { @@ -57,13 +39,8 @@ void AudioAPIModule::injectJSIBindings() { // cache app directory paths on the attached thread NativeFileInfo::warmCache(); -#if RN_AUDIO_API_ENABLE_WORKLETS - auto uiWorkletRuntime = weakWorkletsModuleProxy_.lock()->getUIWorkletRuntime(); -#else - auto uiWorkletRuntime = nullptr; -#endif AudioAPIModuleInstaller::injectJSIBindings( - jsiRuntime_, jsCallInvoker_, audioEventHandlerRegistry_, uiWorkletRuntime); + jsiRuntime_, jsCallInvoker_, audioEventHandlerRegistry_); } void AudioAPIModule::invokeHandlerWithEventNameAndEventBody( diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.h index 9df1882e0..b0719a4db 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/AudioAPIModule.h @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -10,7 +9,6 @@ namespace audioapi { using namespace facebook; -using namespace worklets; class AudioAPIModule : public jni::HybridClass { public: @@ -18,8 +16,6 @@ class AudioAPIModule : public jni::HybridClass { static jni::local_ref initHybrid( jni::alias_ref jThis, - jni::alias_ref - jWorkletsModule, // it will be null if RN_AUDIO_API_ENABLE_WORKLETS is false jlong jsContext, jni::alias_ref jsCallInvokerHolder); @@ -35,24 +31,13 @@ class AudioAPIModule : public jni::HybridClass { jni::global_ref javaPart_; jsi::Runtime *jsiRuntime_; -#if RN_AUDIO_API_ENABLE_WORKLETS - std::weak_ptr weakWorkletsModuleProxy_; -#endif std::shared_ptr jsCallInvoker_; std::shared_ptr audioEventHandlerRegistry_; -#if RN_AUDIO_API_ENABLE_WORKLETS explicit AudioAPIModule( jni::alias_ref &jThis, - std::weak_ptr weakWorkletsModuleProxy, jsi::Runtime *jsiRuntime, const std::shared_ptr &jsCallInvoker); -#else - explicit AudioAPIModule( - jni::alias_ref &jThis, - jsi::Runtime *jsiRuntime, - const std::shared_ptr &jsCallInvoker); -#endif }; } // namespace audioapi diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt index 30865917a..301854ce1 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt @@ -39,7 +39,6 @@ class AudioAPIModule( @OptIn(markerClass = [FrameworkAPI::class]) private external fun initHybrid( - workletsModule: Any?, jsContext: Long, callInvoker: CallInvokerHolderImpl, ): HybridData @@ -68,16 +67,7 @@ class AudioAPIModule( val jsContext = context.javaScriptContextHolder!!.get() val jsCallInvokerHolder = context.jsCallInvokerHolder as CallInvokerHolderImpl - var workletsModule: Any? = null - if (BuildConfig.RN_AUDIO_API_ENABLE_WORKLETS) { - try { - workletsModule = context.getNativeModule("WorkletsModule") - } catch (_: Exception) { - throw RuntimeException("WorkletsModule not found - make sure react-native-worklets is properly installed") - } - } - - mHybridData = initHybrid(workletsModule, jsContext, jsCallInvokerHolder) + mHybridData = initHybrid(jsContext, jsCallInvokerHolder) MediaSessionManager.initialize(WeakReference(this), reactContext) NativeFileInfo.initialize(reactContext) injectJSIBindings() diff --git a/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h b/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h index 44be8101b..762c4ce20 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h @@ -15,8 +15,6 @@ #include #include -#include - #include namespace audioapi { @@ -27,14 +25,13 @@ class AudioAPIModuleInstaller { static void injectJSIBindings( jsi::Runtime *jsiRuntime, const std::shared_ptr &jsCallInvoker, - const std::shared_ptr &audioEventHandlerRegistry, - std::shared_ptr uiRuntime = nullptr) { - auto createAudioContext = getCreateAudioContextFunction( - jsiRuntime, jsCallInvoker, audioEventHandlerRegistry, uiRuntime); + const std::shared_ptr &audioEventHandlerRegistry) { + auto createAudioContext = + getCreateAudioContextFunction(jsiRuntime, jsCallInvoker, audioEventHandlerRegistry); auto createAudioRecorder = getCreateAudioRecorderFunction(jsiRuntime, jsCallInvoker, audioEventHandlerRegistry); - auto createOfflineAudioContext = getCreateOfflineAudioContextFunction( - jsiRuntime, jsCallInvoker, audioEventHandlerRegistry, uiRuntime); + auto createOfflineAudioContext = + getCreateOfflineAudioContextFunction(jsiRuntime, jsCallInvoker, audioEventHandlerRegistry); auto createAudioBuffer = getCreateAudioBufferFunction(jsiRuntime); auto createAudioDecoder = getCreateAudioDecoderFunction(jsiRuntime, jsCallInvoker); auto createAudioFileUtils = getCreateAudioFileUtilsFunction(jsiRuntime, jsCallInvoker); @@ -59,30 +56,20 @@ class AudioAPIModuleInstaller { static jsi::Function getCreateAudioContextFunction( jsi::Runtime *jsiRuntime, const std::shared_ptr &jsCallInvoker, - const std::shared_ptr &audioEventHandlerRegistry, - const std::weak_ptr &uiRuntime) { + const std::shared_ptr &audioEventHandlerRegistry) { return jsi::Function::createFromHostFunction( *jsiRuntime, jsi::PropNameID::forAscii(*jsiRuntime, "createAudioContext"), 1, - [jsCallInvoker, audioEventHandlerRegistry, uiRuntime]( + [jsCallInvoker, audioEventHandlerRegistry]( jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value { auto sampleRate = static_cast(args[0].getNumber()); -#if RN_AUDIO_API_ENABLE_WORKLETS - auto runtimeRegistry = RuntimeRegistry{.uiRuntime = uiRuntime}; - if (count > 1 && args[1].isObject()) { - runtimeRegistry.audioRuntime = worklets::extractWorkletRuntime(runtime, args[1]); - } -#else - auto runtimeRegistry = RuntimeRegistry{}; -#endif - auto audioContextHostObject = std::make_shared( - sampleRate, audioEventHandlerRegistry, runtimeRegistry, &runtime, jsCallInvoker); + sampleRate, audioEventHandlerRegistry, &runtime, jsCallInvoker); return jsi::Object::createFromHostObject(runtime, audioContextHostObject); }); @@ -91,13 +78,12 @@ class AudioAPIModuleInstaller { static jsi::Function getCreateOfflineAudioContextFunction( jsi::Runtime *jsiRuntime, const std::shared_ptr &jsCallInvoker, - const std::shared_ptr &audioEventHandlerRegistry, - const std::weak_ptr &uiRuntime) { + const std::shared_ptr &audioEventHandlerRegistry) { return jsi::Function::createFromHostFunction( *jsiRuntime, jsi::PropNameID::forAscii(*jsiRuntime, "createOfflineAudioContext"), 3, - [jsCallInvoker, audioEventHandlerRegistry, uiRuntime]( + [jsCallInvoker, audioEventHandlerRegistry]( jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, @@ -106,21 +92,11 @@ class AudioAPIModuleInstaller { auto length = static_cast(args[1].getNumber()); auto sampleRate = static_cast(args[2].getNumber()); -#if RN_AUDIO_API_ENABLE_WORKLETS - auto runtimeRegistry = RuntimeRegistry{.uiRuntime = uiRuntime}; - if (count > 3 && args[3].isObject()) { - runtimeRegistry.audioRuntime = worklets::extractWorkletRuntime(runtime, args[3]); - } -#else - auto runtimeRegistry = RuntimeRegistry{}; -#endif - auto audioContextHostObject = std::make_shared( numberOfChannels, length, sampleRate, audioEventHandlerRegistry, - runtimeRegistry, &runtime, jsCallInvoker); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp index 0ece298cf..1fdc3c820 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp @@ -12,11 +12,10 @@ namespace audioapi { AudioContextHostObject::AudioContextHostObject( float sampleRate, const std::shared_ptr &audioEventHandlerRegistry, - const RuntimeRegistry &runtimeRegistry, jsi::Runtime *runtime, const std::shared_ptr &callInvoker) : BaseAudioContextHostObject( - std::make_shared(sampleRate, audioEventHandlerRegistry, runtimeRegistry), + std::make_shared(sampleRate, audioEventHandlerRegistry), runtime, callInvoker) { addFunctions( diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h index ce114618d..99104aa08 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -17,7 +16,6 @@ class AudioContextHostObject : public BaseAudioContextHostObject { explicit AudioContextHostObject( float sampleRate, const std::shared_ptr &audioEventHandlerRegistry, - const RuntimeRegistry &runtimeRegistry, jsi::Runtime *runtime, const std::shared_ptr &callInvoker); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp index 671645e33..8c3986785 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp @@ -9,8 +9,6 @@ #include #include #include -#include -#include #include #include #include @@ -19,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -46,9 +43,6 @@ BaseAudioContextHostObject::BaseAudioContextHostObject( JSI_EXPORT_PROPERTY_GETTER(BaseAudioContextHostObject, currentTime)); addFunctions( - JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createWorkletSourceNode), - JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createWorkletNode), - JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createWorkletProcessingNode), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createRecorderAdapter), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createOscillator), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createStreamer), @@ -90,68 +84,6 @@ JSI_PROPERTY_GETTER_IMPL(BaseAudioContextHostObject, currentTime) { return {context_->getCurrentTime()}; } -JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createWorkletSourceNode) { -#if RN_AUDIO_API_ENABLE_WORKLETS - auto shareableWorklet = - worklets::extractSerializableOrThrow(runtime, args[0]); - auto shouldUseUiRuntime = args[1].getBool(); - std::weak_ptr workletRuntime = shouldUseUiRuntime - ? context_->getRuntimeRegistry().uiRuntime - : context_->getRuntimeRegistry().audioRuntime; - - auto workletSourceNodeHostObject = std::make_shared( - context_->getGraph(), context_, workletRuntime, shareableWorklet, shouldUseUiRuntime); - return jsi::Object::createFromHostObject(runtime, workletSourceNodeHostObject); -#endif - return jsi::Value::undefined(); -} - -JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createWorkletNode) { -#if RN_AUDIO_API_ENABLE_WORKLETS - auto shareableWorklet = - worklets::extractSerializableOrThrow(runtime, args[0]); - auto shouldUseUiRuntime = args[1].getBool(); - std::weak_ptr workletRuntime = shouldUseUiRuntime - ? context_->getRuntimeRegistry().uiRuntime - : context_->getRuntimeRegistry().audioRuntime; - auto bufferLength = static_cast(args[2].getNumber()); - auto inputChannelCount = static_cast(args[3].getNumber()); - - auto workletNodeHostObject = std::make_shared( - context_->getGraph(), - context_, - workletRuntime, - shareableWorklet, - shouldUseUiRuntime, - bufferLength, - inputChannelCount); - auto jsiObject = jsi::Object::createFromHostObject(runtime, workletNodeHostObject); - jsiObject.setExternalMemoryPressure( - runtime, - sizeof(float) * bufferLength * inputChannelCount); // rough estimate of underlying buffer - return jsiObject; -#endif - return jsi::Value::undefined(); -} - -JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createWorkletProcessingNode) { -#if RN_AUDIO_API_ENABLE_WORKLETS - auto shareableWorklet = - worklets::extractSerializableOrThrow(runtime, args[0]); - auto shouldUseUiRuntime = args[1].getBool(); - std::weak_ptr workletRuntime = shouldUseUiRuntime - ? context_->getRuntimeRegistry().uiRuntime - : context_->getRuntimeRegistry().audioRuntime; - - auto workletProcessingNodeHostObject = std::make_shared( - context_->getGraph(), context_, workletRuntime, shareableWorklet, shouldUseUiRuntime); - auto object = jsi::Object::createFromHostObject(runtime, workletProcessingNodeHostObject); - object.setExternalMemoryPressure(runtime, workletProcessingNodeHostObject->getMemoryPressure()); - return object; -#endif - return jsi::Value::undefined(); -} - JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createRecorderAdapter) { auto recorderAdapterHostObject = std::make_shared(context_); auto object = jsi::Object::createFromHostObject(runtime, recorderAdapterHostObject); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h index b8d597209..499c7333d 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h @@ -27,9 +27,6 @@ class BaseAudioContextHostObject : public HostObject { JSI_PROPERTY_GETTER_DECL(sampleRate); JSI_PROPERTY_GETTER_DECL(currentTime); - JSI_HOST_FUNCTION_DECL(createWorkletSourceNode); - JSI_HOST_FUNCTION_DECL(createWorkletNode); - JSI_HOST_FUNCTION_DECL(createWorkletProcessingNode); JSI_HOST_FUNCTION_DECL(createRecorderAdapter); JSI_HOST_FUNCTION_DECL(createOscillator); JSI_HOST_FUNCTION_DECL(createStreamer); @@ -47,6 +44,12 @@ class BaseAudioContextHostObject : public HostObject { JSI_HOST_FUNCTION_DECL(createWaveShaper); JSI_HOST_FUNCTION_DECL(createDelay); + /// @brief Access the underlying C++ audio context. + /// @return The underlying C++ audio context. + [[nodiscard]] const std::shared_ptr &getContext() const { + return context_; + } + protected: std::shared_ptr context_; std::shared_ptr promiseVendor_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/OfflineAudioContextHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/OfflineAudioContextHostObject.cpp index 189f88f93..5644864f4 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/OfflineAudioContextHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/OfflineAudioContextHostObject.cpp @@ -12,7 +12,6 @@ OfflineAudioContextHostObject::OfflineAudioContextHostObject( size_t length, float sampleRate, const std::shared_ptr &audioEventHandlerRegistry, - const RuntimeRegistry &runtimeRegistry, jsi::Runtime *runtime, const std::shared_ptr &callInvoker) : BaseAudioContextHostObject( @@ -20,8 +19,7 @@ OfflineAudioContextHostObject::OfflineAudioContextHostObject( numberOfChannels, length, sampleRate, - audioEventHandlerRegistry, - runtimeRegistry), + audioEventHandlerRegistry), runtime, callInvoker) { addFunctions( diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/OfflineAudioContextHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/OfflineAudioContextHostObject.h index 588a41ebe..a2997ee74 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/OfflineAudioContextHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/OfflineAudioContextHostObject.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -19,7 +18,6 @@ class OfflineAudioContextHostObject : public BaseAudioContextHostObject { size_t length, float sampleRate, const std::shared_ptr &audioEventHandlerRegistry, - const RuntimeRegistry &runtimeRegistry, jsi::Runtime *runtime, const std::shared_ptr &callInvoker); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WorkletNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WorkletNodeHostObject.h deleted file mode 100644 index e3d16ef1a..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WorkletNodeHostObject.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -namespace audioapi { -using namespace facebook; - -class WorkletNodeHostObject : public AudioNodeHostObject { - public: - explicit WorkletNodeHostObject( - const std::shared_ptr &graph, - const std::shared_ptr &context, - std::weak_ptr workletRuntime, - const std::shared_ptr &shareableWorklet, - bool shouldLockRuntime, - size_t bufferLength, - size_t inputChannelCount) - : AudioNodeHostObject( - graph, - std::make_unique( - context, - bufferLength, - inputChannelCount, - WorkletsRunner(workletRuntime, shareableWorklet, shouldLockRuntime))) {} -}; -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WorkletProcessingNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WorkletProcessingNodeHostObject.h deleted file mode 100644 index cdad2dea9..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WorkletProcessingNodeHostObject.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -namespace audioapi { -using namespace facebook; - -class WorkletProcessingNodeHostObject : public AudioNodeHostObject { - public: - explicit WorkletProcessingNodeHostObject( - const std::shared_ptr &graph, - const std::shared_ptr &context, - std::weak_ptr workletRuntime, - const std::shared_ptr &shareableWorklet, - bool shouldLockRuntime) - : AudioNodeHostObject( - graph, - std::make_unique( - context, - WorkletsRunner(workletRuntime, shareableWorklet, shouldLockRuntime))) {} - - [[nodiscard]] size_t getMemoryPressure() const override { - // 2 input + 2 output AudioArrayBuffer (RQ floats) wrapped as JS typed arrays. - // The worklet closure/captures live in JS and aren't tracked here. - return AudioNodeHostObject::getMemoryPressure() + 4 * RENDER_QUANTUM_SIZE * sizeof(float); - } -}; -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/WorkletSourceNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/WorkletSourceNodeHostObject.h deleted file mode 100644 index a9ae0e484..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/WorkletSourceNodeHostObject.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -namespace audioapi { -using namespace facebook; - -class WorkletSourceNodeHostObject : public AudioScheduledSourceNodeHostObject { - public: - explicit WorkletSourceNodeHostObject( - const std::shared_ptr &graph, - const std::shared_ptr &context, - std::weak_ptr workletRuntime, - const std::shared_ptr &shareableWorklet, - bool shouldLockRuntime, - const AudioScheduledSourceNodeOptions &options = AudioScheduledSourceNodeOptions()) - : AudioScheduledSourceNodeHostObject( - graph, - std::make_unique( - context, - WorkletsRunner(workletRuntime, shareableWorklet, shouldLockRuntime)), - options) {} -}; -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/compatibility/EXTENSION_API.md b/packages/react-native-audio-api/common/cpp/audioapi/compatibility/EXTENSION_API.md new file mode 100644 index 000000000..657643706 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/compatibility/EXTENSION_API.md @@ -0,0 +1,152 @@ +# react-native-audio-api — C++ Extension API + +Extension packages integrate with react-native-audio-api in C++ through a **single** +public header: + +```cpp +#include +``` + +Do **not** include any other `audioapi/...` headers. Types and helpers needed for +extensions are pulled in by `StableAPI.h`. Everything else is internal. + +--- + +## Android (Prefab) + +| Item | Value | +|---|---| +| Gradle module | `:react-native-audio-api` | +| Prefab package | `react-native-audio-api` | +| CMake `find_package` | `find_package(react-native-audio-api REQUIRED CONFIG)` | +| Link target | `react-native-audio-api::react-native-audio-api` | +| Shared library | `libreact-native-audio-api.so` | +| **Only public include** | `` | + +### Example `CMakeLists.txt` + +```cmake +find_package(react-native-audio-api REQUIRED CONFIG) + +target_link_libraries(my-extension + react-native-audio-api::react-native-audio-api + ReactAndroid::jsi + # ... +) +``` + +Prefab ships `audioapi/**/*.{h,hpp}` headers (include-only, same pattern as +react-native-worklets). Extension code must still include only ``. + +--- + +## iOS (CocoaPods) + +| Item | Value | +|---|---| +| Pod | `RNAudioAPI` | +| Podspec dependency | `s.dependency 'RNAudioAPI'` | +| npm peer dependency | `react-native-audio-api` (in extension `package.json`) | +| **Only public include** | `` | + +### Example `*.podspec` + +Extension packages ship their own pod (e.g. `RNAudioWorklets`) and **must** depend on +`RNAudioAPI` so CocoaPods links the native library and exposes public headers: + +```ruby +Pod::Spec.new do |s| + s.name = "MyAudioExtension" + # ... + + s.source_files = "common/cpp/myextension/**/*.{cpp,h}" + s.header_dir = "myextension" + s.header_mappings_dir = "common/cpp/myextension" + + s.dependency 'RNAudioAPI' + s.dependency 'React-jsi' + + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => [ + '"$(PODS_ROOT)/Headers/Public/RNAudioAPI"', + # React / Folly paths (see react-native-audio-worklets/RNAudioWorklets.podspec)... + ].join(' '), + "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", + } + + install_modules_dependencies(s) +end +``` + +Reference: `packages/react-native-audio-worklets/RNAudioWorklets.podspec`. + +### `pod_target_xcconfig` (header search paths) + +`s.dependency 'RNAudioAPI'` usually adds the RNAudioAPI header search paths +automatically. You may still set this explicitly so `` is +obvious in the podspec: + +```ruby +"HEADER_SEARCH_PATHS" => [ + '"$(PODS_ROOT)/Headers/Public/RNAudioAPI"', + # React / Folly paths... +].join(' ') +``` + +--- + +## API available through `StableAPI.h` + +| Type / header (transitive) | Purpose | +|---|---| +| `audioapi::BaseAudioContextHostObject` | Resolve `AudioContext` from a JSI object via `getContext()` | +| `audioapi::AudioNodeHostObject` | Base HostObject for custom nodes exposed to JS | +| `audioapi::BaseAudioContext` | Context handle; `getGraph()`, scheduling, sample rate | +| `audioapi::AudioNode` | Base class for custom `AudioNode` implementations | +| `audioapi::utils::graph::Graph` | Audio graph owned by the context | +| `RENDER_QUANTUM_SIZE` etc. (via `core/utils/Constants.h`) | Frame-size and engine constants | +| `audioapi::AudioBuffer` | Multi-channel float buffer | +| `audioapi::AudioArrayBuffer` | Single-channel view for JSI handoff | + +--- + +## Integration pattern + +1. **Separate package** with its own pod / pure C++ TurboModule / CMake target. +2. **Declare dependencies:** + - **iOS:** `s.dependency 'RNAudioAPI'` in your podspec (see example above). + - **Android:** `find_package(react-native-audio-api)` + link + `react-native-audio-api::react-native-audio-api` in CMake. + - **npm:** `react-native-audio-api` as a `peerDependency` in `package.json`. +3. **`#include `** — the only audio-api C++ include. +4. **Install JSI bindings** from your own TurboModule (do not modify + `AudioAPIModuleInstaller`). +5. **Resolve the context** from JS: + + ```cpp + auto host = contextObject.getHostObject(runtime); + auto context = host->getContext(); + auto graph = context->getGraph(); + ``` + +6. **Build a custom node** by subclassing `audioapi::AudioNode` and wrapping it in a + HostObject that extends `audioapi::AudioNodeHostObject`, then connect it through the + graph like built-in nodes. + +Reference implementation: `packages/react-native-audio-worklets`. + +--- + +## Explicitly not supported + +- Any `#include` other than `` +- `audioapi/AudioAPIModuleInstaller.h` — core global install; not for extensions +- `audioapi/libs/**`, `audioapi/dsp/**`, `audioapi/external/**` +- Concrete built-in node HostObjects — reference only, not a stable base API + +--- + +## Versioning + +Declare a `react-native-audio-api` **peer dependency** in `package.json` with a +semver range matching the API you target. diff --git a/packages/react-native-audio-api/common/cpp/audioapi/compatibility/StableAPI.h b/packages/react-native-audio-api/common/cpp/audioapi/compatibility/StableAPI.h new file mode 100644 index 000000000..83348d4f5 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/compatibility/StableAPI.h @@ -0,0 +1,17 @@ +#pragma once + +/** + * Stable C++ compatibility API for react-native-audio-api. + * + * Extension packages must include **only** this header — do not include other + * `audioapi/...` headers directly. Symbols not reachable through this file are + * internal and may change without notice. + */ + +#include +#include +#include +#include +#include +#include +#include diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp index b6f726a4c..ad35c1bfe 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp @@ -14,10 +14,8 @@ namespace audioapi { AudioContext::AudioContext( float sampleRate, - const std::shared_ptr &audioEventHandlerRegistry, - const RuntimeRegistry &runtimeRegistry) - : BaseAudioContext(sampleRate, audioEventHandlerRegistry, runtimeRegistry), - isInitialized_(false) {} + const std::shared_ptr &audioEventHandlerRegistry) + : BaseAudioContext(sampleRate, audioEventHandlerRegistry), isInitialized_(false) {} AudioContext::~AudioContext() { if (getState() != ContextState::CLOSED) { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h index 27ddecfea..2dbe5c312 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -22,8 +21,7 @@ class AudioContext : public BaseAudioContext { public: explicit AudioContext( float sampleRate, - const std::shared_ptr &audioEventHandlerRegistry, - const RuntimeRegistry &runtimeRegistry); + const std::shared_ptr &audioEventHandlerRegistry); ~AudioContext() override; DELETE_COPY_AND_MOVE(AudioContext); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp index 9e62f92b2..117856b41 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -15,12 +14,10 @@ namespace audioapi { BaseAudioContext::BaseAudioContext( float sampleRate, - const std::shared_ptr &audioEventHandlerRegistry, - const RuntimeRegistry &runtimeRegistry) + const std::shared_ptr &audioEventHandlerRegistry) : state_(ContextState::SUSPENDED), sampleRate_(sampleRate), audioEventHandlerRegistry_(audioEventHandlerRegistry), - runtimeRegistry_(runtimeRegistry), audioEventScheduler_(AUDIO_SCHEDULER_CAPACITY), gcAudioEventScheduler_(GC_AUDIO_SCHEDULER_CAPACITY), disposer_( @@ -100,10 +97,6 @@ std::shared_ptr BaseAudioContext::getAudioEventHandl return audioEventHandlerRegistry_; } -const RuntimeRegistry &BaseAudioContext::getRuntimeRegistry() const { - return runtimeRegistry_; -} - utils::DisposerImpl *BaseAudioContext::getDisposer() const { return disposer_.get(); } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h index 8c489637a..8773f0adc 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h @@ -5,7 +5,6 @@ #include #include #include -#include #include #include @@ -30,8 +29,7 @@ class BaseAudioContext : public std::enable_shared_from_this { public: explicit BaseAudioContext( float sampleRate, - const std::shared_ptr &audioEventHandlerRegistry, - const RuntimeRegistry &runtimeRegistry); + const std::shared_ptr &audioEventHandlerRegistry); virtual ~BaseAudioContext() = default; DELETE_COPY_AND_MOVE(BaseAudioContext); @@ -53,7 +51,6 @@ class BaseAudioContext : public std::enable_shared_from_this { std::shared_ptr getBasicWaveForm(OscillatorType type); std::shared_ptr getGraph() const; std::shared_ptr getAudioEventHandlerRegistry() const; - const RuntimeRegistry &getRuntimeRegistry() const; utils::DisposerImpl *getDisposer() const; /// @brief Assigns the audio destination node to the context. @@ -124,7 +121,6 @@ class BaseAudioContext : public std::enable_shared_from_this { private: std::atomic sampleRate_; std::shared_ptr audioEventHandlerRegistry_; - RuntimeRegistry runtimeRegistry_; std::shared_ptr cachedSineWave_ = nullptr; std::shared_ptr cachedSquareWave_ = nullptr; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.cpp index ccc66c7f1..eff37345d 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.cpp @@ -19,9 +19,8 @@ OfflineAudioContext::OfflineAudioContext( int numberOfChannels, size_t length, float sampleRate, - const std::shared_ptr &audioEventHandlerRegistry, - const RuntimeRegistry &runtimeRegistry) - : BaseAudioContext(sampleRate, audioEventHandlerRegistry, runtimeRegistry), + const std::shared_ptr &audioEventHandlerRegistry) + : BaseAudioContext(sampleRate, audioEventHandlerRegistry), length_(length), currentSampleFrame_(0), audioBuffer_( diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.h b/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.h index d7872f2b7..542173de1 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -19,8 +18,7 @@ class OfflineAudioContext : public BaseAudioContext { int numberOfChannels, size_t length, float sampleRate, - const std::shared_ptr &audioEventHandlerRegistry, - const RuntimeRegistry &runtimeRegistry); + const std::shared_ptr &audioEventHandlerRegistry); ~OfflineAudioContext() override = default; DELETE_COPY_AND_MOVE(OfflineAudioContext); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletNode.cpp deleted file mode 100644 index 7c034abbd..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletNode.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include -#include -#include -#include - -namespace audioapi { - -WorkletNode::WorkletNode( - const std::shared_ptr &context, - size_t bufferLength, - size_t inputChannelCount, - WorkletsRunner &&workletRunner) - : AudioNode(context), - workletRunner_(std::move(workletRunner)), - buffer_( - std::make_shared(bufferLength, inputChannelCount, context->getSampleRate())), - bufferLength_(bufferLength), - inputChannelCount_(inputChannelCount), - curBuffIndex_(0) {} - -void WorkletNode::processNode(int framesToProcess) { - int processed = 0; - size_t channelCount_ = - std::min(inputChannelCount_, static_cast(audioBuffer_->getNumberOfChannels())); - while (processed < framesToProcess) { - size_t framesToWorkletInvoke = bufferLength_ - curBuffIndex_; - size_t needsToProcess = framesToProcess - processed; - size_t shouldProcess = std::min(framesToWorkletInvoke, needsToProcess); - - /// here we copy - /// to [curBuffIndex_, curBuffIndex_ + shouldProcess] - /// from [processed, processed + shouldProcess] - buffer_->copy(*audioBuffer_, processed, curBuffIndex_, shouldProcess); - - processed += static_cast(shouldProcess); - curBuffIndex_ += shouldProcess; - - /// If we filled the entire buffer, we need to execute the worklet - if (curBuffIndex_ != bufferLength_) { - continue; - } - // Reset buffer index, channel buffers and execute worklet - curBuffIndex_ = 0; - workletRunner_.executeOnRuntimeSync([this, channelCount_](jsi::Runtime &uiRuntimeRaw) { - /// Arguments preparation - auto jsArray = jsi::Array(uiRuntimeRaw, channelCount_); - for (size_t ch = 0; ch < channelCount_; ch++) { - auto sharedAudioArray = std::make_shared(bufferLength_); - sharedAudioArray->copy(*buffer_->getChannel(ch)); - auto sharedAudioArraySize = sharedAudioArray->size(); - auto arrayBuffer = jsi::ArrayBuffer(uiRuntimeRaw, std::move(sharedAudioArray)); - arrayBuffer.setExternalMemoryPressure(uiRuntimeRaw, sharedAudioArraySize); - jsArray.setValueAtIndex(uiRuntimeRaw, ch, std::move(arrayBuffer)); - } - - buffer_->zero(); - - /// Call the worklet - workletRunner_.callUnsafe( - std::move(jsArray), jsi::Value(uiRuntimeRaw, static_cast(channelCount_))); - - return jsi::Value::undefined(); - }); - } -} - -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletNode.h deleted file mode 100644 index df0bc61b6..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletNode.h +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -namespace audioapi { - -#if RN_AUDIO_API_TEST -class WorkletNode : public AudioNode { - public: - explicit WorkletNode( - std::shared_ptr context, - size_t bufferLength, - size_t inputChannelCount, - WorkletsRunner &&workletRunner) - : AudioNode(context) {} - - protected: - void processNode(int framesToProcess) override {} -}; -#else - -using namespace facebook; - -class WorkletNode : public AudioNode { - public: - explicit WorkletNode( - const std::shared_ptr &context, - size_t bufferLength, - size_t inputChannelCount, - WorkletsRunner &&workletRunner); - DELETE_COPY_AND_MOVE(WorkletNode); - ~WorkletNode() override = default; - - protected: - void processNode(int framesToProcess) override; - - private: - WorkletsRunner workletRunner_; - std::shared_ptr buffer_; - - /// @brief Length of the byte buffer that will be passed to the AudioArrayBuffer - size_t bufferLength_; - size_t inputChannelCount_; - size_t curBuffIndex_; -}; - -#endif // RN_AUDIO_API_TEST - -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletProcessingNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletProcessingNode.cpp deleted file mode 100644 index 317973277..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletProcessingNode.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include -#include -#include -#include -#include - -namespace audioapi { - -WorkletProcessingNode::WorkletProcessingNode( - const std::shared_ptr &context, - WorkletsRunner &&workletRunner) - : AudioNode(context), workletRunner_(std::move(workletRunner)) { - // Pre-allocate buffers for max 128 frames and 2 channels (stereo) - size_t maxChannelCount = 2; - inputBuffsHandles_.resize(maxChannelCount); - outputBuffsHandles_.resize(maxChannelCount); - - for (size_t i = 0; i < maxChannelCount; ++i) { - inputBuffsHandles_[i] = std::make_shared(RENDER_QUANTUM_SIZE); - outputBuffsHandles_[i] = std::make_shared(RENDER_QUANTUM_SIZE); - } -} - -void WorkletProcessingNode::processNode(int framesToProcess) { - size_t channelCount = std::min( - static_cast(2), // Fixed to stereo for now - audioBuffer_->getNumberOfChannels()); - - // Copy input data to pre-allocated input buffers - for (size_t ch = 0; ch < channelCount; ch++) { - inputBuffsHandles_[ch]->copy(*audioBuffer_->getChannel(ch), 0, 0, framesToProcess); - } - - // Execute the worklet - auto result = workletRunner_.executeOnRuntimeSync( - [this, channelCount, framesToProcess](jsi::Runtime &rt) -> jsi::Value { - auto inputJsArray = jsi::Array(rt, channelCount); - auto outputJsArray = jsi::Array(rt, channelCount); - - for (size_t ch = 0; ch < channelCount; ch++) { - // Create input array buffer - auto inputArrayBuffer = jsi::ArrayBuffer(rt, inputBuffsHandles_[ch]); - inputJsArray.setValueAtIndex(rt, ch, inputArrayBuffer); - - // Create output array buffer - auto outputArrayBuffer = jsi::ArrayBuffer(rt, outputBuffsHandles_[ch]); - outputJsArray.setValueAtIndex(rt, ch, outputArrayBuffer); - } - - // We call unsafely here because we are already on the runtime thread - // and the runtime is locked by executeOnRuntimeSync (if - // shouldLockRuntime is true) - double time = 0.0f; - if (std::shared_ptr context = context_.lock()) { - time = context->getCurrentTime(); - } - return workletRunner_.callUnsafe( - inputJsArray, - outputJsArray, - jsi::Value(rt, static_cast(framesToProcess)), - jsi::Value(rt, time)); - }); - - // Copy processed output data back to the processing buffer or zero on failure - for (size_t ch = 0; ch < channelCount; ch++) { - auto *channelData = audioBuffer_->getChannel(ch); - - if (result.has_value()) { - // Copy processed output data - channelData->copy(*outputBuffsHandles_[ch], 0, 0, framesToProcess); - } else { - // Zero the output on worklet execution failure - channelData->zero(0, framesToProcess); - } - } -} - -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletProcessingNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletProcessingNode.h deleted file mode 100644 index 6e917c7c9..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletProcessingNode.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace audioapi { - -#if RN_AUDIO_API_TEST -class WorkletProcessingNode : public AudioNode { - public: - explicit WorkletProcessingNode( - std::shared_ptr context, - WorkletsRunner &&workletRunner) - : AudioNode(context) {} - - protected: - void processNode(int framesToProcess) override {} -}; -#else - -using namespace facebook; - -class WorkletProcessingNode : public AudioNode { - public: - explicit WorkletProcessingNode( - const std::shared_ptr &context, - WorkletsRunner &&workletRunner); - - protected: - void processNode(int framesToProcess) override; - - private: - WorkletsRunner workletRunner_; - std::vector> inputBuffsHandles_; - std::vector> outputBuffsHandles_; -}; - -#endif // RN_AUDIO_API_TEST - -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/WorkletSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/WorkletSourceNode.cpp deleted file mode 100644 index c650487aa..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/WorkletSourceNode.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include -#include -#include -#include - -namespace audioapi { - -WorkletSourceNode::WorkletSourceNode( - const std::shared_ptr &context, - WorkletsRunner &&workletRunner) - : AudioScheduledSourceNode(context), workletRunner_(std::move(workletRunner)) { - // Prepare buffers for audio processing - size_t outputChannelCount = this->getChannelCount(); - outputBuffsHandles_.resize(outputChannelCount); - for (size_t i = 0; i < outputChannelCount; ++i) { - outputBuffsHandles_[i] = std::make_shared(RENDER_QUANTUM_SIZE); - } -} - -void WorkletSourceNode::processNode(int framesToProcess) { - if (isUnscheduled() || isFinished()) { - audioBuffer_->zero(); - return; - } - - size_t startOffset = 0; - size_t nonSilentFramesToProcess = framesToProcess; - - std::shared_ptr context = context_.lock(); - if (context == nullptr) { - audioBuffer_->zero(); - return; - } - updatePlaybackInfo( - audioBuffer_, - framesToProcess, - startOffset, - nonSilentFramesToProcess, - context->getSampleRate(), - context->getCurrentSampleFrame()); - - if (!isPlaying() && !isStopScheduled() || nonSilentFramesToProcess == 0) { - audioBuffer_->zero(); - return; - } - - size_t outputChannelCount = audioBuffer_->getNumberOfChannels(); - - auto result = workletRunner_.executeOnRuntimeSync( - [this, nonSilentFramesToProcess, startOffset, time = context->getCurrentTime()]( - jsi::Runtime &rt) { - auto jsiArray = jsi::Array(rt, this->outputBuffsHandles_.size()); - for (size_t i = 0; i < this->outputBuffsHandles_.size(); ++i) { - auto arrayBuffer = jsi::ArrayBuffer(rt, this->outputBuffsHandles_[i]); - jsiArray.setValueAtIndex(rt, i, arrayBuffer); - } - - // We call unsafely here because we are already on the runtime thread - // and the runtime is locked by executeOnRuntimeSync (if - // shouldLockRuntime is true) - return workletRunner_.callUnsafe( - jsiArray, - jsi::Value(rt, static_cast(nonSilentFramesToProcess)), - jsi::Value(rt, time), - jsi::Value(rt, static_cast(startOffset))); - }); - - // If the worklet execution failed, zero the output - // It might happen if the runtime is not available - if (!result.has_value()) { - audioBuffer_->zero(); - return; - } - - // Copy the processed data back to the AudioBuffer - for (size_t i = 0; i < outputChannelCount; ++i) { - audioBuffer_->getChannel(i)->copy( - *outputBuffsHandles_[i], 0, startOffset, nonSilentFramesToProcess); - } - - handleStopScheduled(); -} - -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/WorkletSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/WorkletSourceNode.h deleted file mode 100644 index c22d28ad2..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/WorkletSourceNode.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace audioapi { - -#if RN_AUDIO_API_TEST -class WorkletSourceNode : public AudioScheduledSourceNode { - public: - explicit WorkletSourceNode( - std::shared_ptr context, - WorkletsRunner &&workletRunner) - : AudioScheduledSourceNode(context) {} - - protected: - void processNode(int framesToProcess) override {} -}; -#else - -class WorkletSourceNode : public AudioScheduledSourceNode { - public: - explicit WorkletSourceNode( - const std::shared_ptr &context, - WorkletsRunner &&workletRunner); - - protected: - void processNode(int framesToProcess) override; - - private: - WorkletsRunner workletRunner_; - std::vector> outputBuffsHandles_; -}; -#endif // RN_AUDIO_API_TEST - -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/SafeIncludes.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/SafeIncludes.h deleted file mode 100644 index 6569e6dc5..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/SafeIncludes.h +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once - -#include - -#include -#include - -#if ANDROID -#include -#endif - -#ifndef RN_AUDIO_API_TEST -#define RN_AUDIO_API_TEST 0 -#endif - -#if RN_AUDIO_API_ENABLE_WORKLETS -#include -#include -#include -#if ANDROID -#include -#endif -#else - -#define RN_AUDIO_API_WORKLETS_DISABLED_ERROR \ - std::runtime_error( \ - "Worklets are disabled. Please install react-native-worklets or check if you have supported version to enable these features."); - -/// @brief Dummy implementation of worklets for non-worklet builds they should do nothing and mock necessary methods -/// @note It helps to reduce compile time branching across codebase -/// @note If you need to base some c++ implementation on if the worklets are enabled use `#if RN_AUDIO_API_ENABLE_WORKLETS` -namespace worklets { - -using namespace facebook; -class MessageQueueThread {}; -class WorkletsModuleProxy {}; -class WorkletRuntime { - public: - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init) -- dummy type, members unused - explicit WorkletRuntime( - uint64_t, - const std::shared_ptr &, - const std::string &, - const bool) { - throw RN_AUDIO_API_WORKLETS_DISABLED_ERROR - } - [[nodiscard]] jsi::Runtime &getJSIRuntime() const { - throw RN_AUDIO_API_WORKLETS_DISABLED_ERROR - } - [[nodiscard]] jsi::Value executeSync(jsi::Runtime &rt, const jsi::Value &worklet) const { - throw RN_AUDIO_API_WORKLETS_DISABLED_ERROR - } - [[nodiscard]] jsi::Value executeSync(std::function &&job) const { - // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved) - throw RN_AUDIO_API_WORKLETS_DISABLED_ERROR} jsi::Value - executeSync(const std::function &job) const { - throw RN_AUDIO_API_WORKLETS_DISABLED_ERROR - } -}; -class SerializableWorklet { - public: - SerializableWorklet(jsi::Runtime *, const jsi::Object &){ - throw RN_AUDIO_API_WORKLETS_DISABLED_ERROR} jsi::Value toJSValue(jsi::Runtime &rt) { - throw RN_AUDIO_API_WORKLETS_DISABLED_ERROR - } -}; -} // namespace worklets - -#undef RN_AUDIO_API_WORKLETS_DISABLED_ERROR - -#endif - -/// @brief Struct to hold references to different runtimes used in the AudioAPI -/// @note it is used to pass them around and avoid creating multiple instances of the same runtime -struct - RuntimeRegistry { // NOLINT(cppcoreguidelines-pro-type-member-init) -- weak_ptr/shared_ptr default-init - std::weak_ptr uiRuntime; - std::shared_ptr audioRuntime; - -#if ANDROID - ~RuntimeRegistry() { - facebook::jni::ThreadScope::WithClassLoader([this]() { - uiRuntime.reset(); - audioRuntime.reset(); - }); - } -#endif -}; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp deleted file mode 100644 index 3e4300c50..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp +++ /dev/null @@ -1,80 +0,0 @@ -#include -#include -#include - -namespace audioapi { - -WorkletsRunner::WorkletsRunner( - std::weak_ptr weakRuntime, - const std::shared_ptr &shareableWorklet, - bool shouldLockRuntime) - : weakRuntime_(std::move(weakRuntime)), shouldLockRuntime(shouldLockRuntime) { - auto strongRuntime = weakRuntime_.lock(); - if (strongRuntime == nullptr) { - return; - } -#if RN_AUDIO_API_ENABLE_WORKLETS - unsafeRuntimePtr = &strongRuntime->getJSIRuntime(); - strongRuntime->executeSync([this, shareableWorklet](jsi::Runtime &rt) -> jsi::Value { - /// Placement new to avoid dynamic memory allocation - new (reinterpret_cast(&unsafeWorklet)) - jsi::Function(shareableWorklet->toJSValue(*unsafeRuntimePtr) - .asObject(*unsafeRuntimePtr) - .asFunction(*unsafeRuntimePtr)); - return jsi::Value::undefined(); - }); - workletInitialized = true; -#else - unsafeRuntimePtr = nullptr; - workletInitialized = false; -#endif -} - -WorkletsRunner::WorkletsRunner(WorkletsRunner &&other) - : weakRuntime_(std::move(other.weakRuntime_)), - unsafeRuntimePtr(other.unsafeRuntimePtr), - workletInitialized(other.workletInitialized), - shouldLockRuntime(other.shouldLockRuntime) { - if (workletInitialized) { - std::memcpy(&unsafeWorklet, &other.unsafeWorklet, sizeof(unsafeWorklet)); - other.workletInitialized = false; - other.unsafeRuntimePtr = nullptr; - } -} - -WorkletsRunner::~WorkletsRunner() { - if (!workletInitialized) { - return; - } - auto strongRuntime = weakRuntime_.lock(); - if (strongRuntime == nullptr) { - // We cannot safely destroy the worklet without a valid runtime - return; - } - reinterpret_cast(&unsafeWorklet)->~Function(); - workletInitialized = false; -} - -std::optional WorkletsRunner::executeOnRuntimeGuarded( - const std::function &&job) const noexcept(noexcept(job)) { - auto strongRuntime = weakRuntime_.lock(); - if (strongRuntime == nullptr) { - return std::nullopt; - } -#if RN_AUDIO_API_ENABLE_WORKLETS - return strongRuntime->executeSync(std::move(job)); -#else - return std::nullopt; -#endif -} - -std::optional WorkletsRunner::executeOnRuntimeUnsafe( - const std::function &&job) const noexcept(noexcept(job)) { -#if RN_AUDIO_API_ENABLE_WORKLETS - return job(*unsafeRuntimePtr); -#else - return std::nullopt; -#endif -} - -}; // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.h deleted file mode 100644 index ed366ddca..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.h +++ /dev/null @@ -1,90 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include - -namespace audioapi { -using namespace facebook; - -/* -* # How to extract worklet from JavaScript argument -* -* To extract a shareable worklet from a JavaScript argument, use the following code: -* -* ```cpp -* auto worklet = worklets::extractSerializableWorkletFromArg(runtime, args[0]); -* ``` -* -* This will return a shared pointer to the extracted worklet, or throw an error if the argument is invalid. -*/ - -class WorkletsRunner { - public: - explicit WorkletsRunner( - std::weak_ptr weakRuntime, - const std::shared_ptr &shareableWorklet, - bool shouldLockRuntime = true); - WorkletsRunner(WorkletsRunner &&); - ~WorkletsRunner(); - - /// @brief Call the worklet function with the given arguments. - /// @tparam ...Args - /// @param ...args - /// @return The result of the worklet function call. - /// @note This method is unsafe and should be used with caution. It assumes that the runtime and worklet are valid and runtime is locked. - template - inline jsi::Value callUnsafe(Args &&...args) { - return getUnsafeWorklet().call(*unsafeRuntimePtr, std::forward(args)...); - } - - /// @brief Call the worklet function with the given arguments. - /// @tparam ...Args - /// @param ...args - /// @return The result of the worklet function call. - /// @note This method is safe and will check if the runtime is available before calling the worklet. If the runtime is not available, it will return nullopt. - template - inline std::optional call(Args &&...args) { - return executeOnRuntimeGuarded([this, args...](jsi::Runtime &rt) -> jsi::Value { - return callUnsafe(std::forward(args)...); - }); - } - - /// @brief Execute a job on the UI runtime safely. - /// @param job - /// @return nullopt if the runtime is not available or the result of the job execution - /// @note Execution is synchronous and will be guarded if shouldLockRuntime is true. - inline std::optional executeOnRuntimeSync( - const std::function &&job) const noexcept(noexcept(job)) { - if (shouldLockRuntime) - return executeOnRuntimeGuarded(std::move(job)); - else - return executeOnRuntimeUnsafe(std::move(job)); - } - - private: - std::weak_ptr weakRuntime_; - jsi::Runtime *unsafeRuntimePtr = nullptr; - - /// @note We want to avoid automatic destruction as - /// when runtime is destroyed, underlying pointer will be invalid - char unsafeWorklet[sizeof(jsi::Function)]; - bool workletInitialized = false; - bool shouldLockRuntime = true; - - inline jsi::Function &getUnsafeWorklet() { - return *reinterpret_cast(&unsafeWorklet); - } - - std::optional executeOnRuntimeGuarded( - const std::function &&job) const noexcept(noexcept(job)); - - std::optional executeOnRuntimeUnsafe( - const std::function &&job) const noexcept(noexcept(job)); -}; - -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/clangd/CMakeLists.txt b/packages/react-native-audio-api/common/cpp/clangd/CMakeLists.txt index f8a3da551..d2c329b85 100644 --- a/packages/react-native-audio-api/common/cpp/clangd/CMakeLists.txt +++ b/packages/react-native-audio-api/common/cpp/clangd/CMakeLists.txt @@ -50,19 +50,6 @@ target_include_directories( "/opt/homebrew/Cellar/googletest/1.17.0/include" ) -add_library(worklets SHARED IMPORTED) -set_target_properties( - worklets - PROPERTIES - IMPORTED_LOCATION - "${REACT_NATIVE_WORKLETS_DIR}/build/intermediates/cmake/${BUILD_TYPE}/obj/${ANDROID_ABI}/libworklets.so" -) -list(APPEND INCLUDE_LIBRARIES - "${REACT_NATIVE_WORKLETS_DIR}/../Common/cpp" - "${REACT_NATIVE_WORKLETS_DIR}/src/main/cpp" -) -list(APPEND LINK_LIBRARIES worklets) - target_include_directories(rnaudioapi_cursor PUBLIC ${INCLUDE_LIBRARIES}) target_link_libraries(rnaudioapi_cursor ${LINK_LIBRARIES}) diff --git a/packages/react-native-audio-api/common/cpp/test/CMakeLists.txt b/packages/react-native-audio-api/common/cpp/test/CMakeLists.txt index 45f64822a..4ac9fd1df 100644 --- a/packages/react-native-audio-api/common/cpp/test/CMakeLists.txt +++ b/packages/react-native-audio-api/common/cpp/test/CMakeLists.txt @@ -37,8 +37,6 @@ file(GLOB_RECURSE RNAUDIOAPI_SRC # exclude HostObjects from tests list(FILTER RNAUDIOAPI_SRC EXCLUDE REGEX ".*/audioapi/HostObjects/.*\\.cpp$") -# exclude worklet nodes -list(FILTER RNAUDIOAPI_SRC EXCLUDE REGEX ".*/Worklet.*Node\\.cpp$") list(REMOVE_ITEM RNAUDIOAPI_SRC "${REACT_NATIVE_AUDIO_API_DIR}/common/cpp/audioapi/core/AudioContext.cpp" @@ -89,7 +87,6 @@ add_executable( ${test_src} ) -add_compile_definitions(RN_AUDIO_API_ENABLE_WORKLETS=0) add_compile_definitions(RN_AUDIO_API_TEST=1) add_compile_definitions(RN_AUDIO_API_FFMPEG_DISABLED=1) add_compile_definitions(RN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED=1) diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/AudioParamTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/AudioParamTest.cpp index 5062cded2..b1c29471e 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/AudioParamTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/AudioParamTest.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -17,8 +16,7 @@ class AudioParamTest : public ::testing::Test { void SetUp() override { eventRegistry = std::make_shared(); - context = std::make_shared( - 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); + context = std::make_shared(2, 5 * sampleRate, sampleRate, eventRegistry); } }; diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/effects/DelayTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/effects/DelayTest.cpp index 389a860a6..fe055851a 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/effects/DelayTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/effects/DelayTest.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -24,8 +23,7 @@ class DelayTest : public ::testing::Test { void SetUp() override { eventRegistry = std::make_shared(); - context = std::make_shared( - 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); + context = std::make_shared(2, 5 * sampleRate, sampleRate, eventRegistry); destination = std::make_shared(context); context->initialize(destination.get()); } diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/effects/GainTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/effects/GainTest.cpp index 3ddac4a25..6f3a6d43c 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/effects/GainTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/effects/GainTest.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -22,8 +21,7 @@ class GainTest : public ::testing::Test { void SetUp() override { eventRegistry = std::make_shared(); - context = std::make_shared( - 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); + context = std::make_shared(2, 5 * sampleRate, sampleRate, eventRegistry); destination = std::make_shared(context); context->initialize(destination.get()); } diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/effects/IIRFilterTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/effects/IIRFilterTest.cpp index 1b400f71a..269b0f779 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/effects/IIRFilterTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/effects/IIRFilterTest.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -23,8 +22,7 @@ class IIRFilterTest : public ::testing::Test { void SetUp() override { eventRegistry = std::make_shared(); - context = std::make_shared( - 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); + context = std::make_shared(2, 5 * sampleRate, sampleRate, eventRegistry); } static std::complex diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/effects/StereoPannerTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/effects/StereoPannerTest.cpp index dff1d86d5..4bf8bda72 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/effects/StereoPannerTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/effects/StereoPannerTest.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -22,8 +21,7 @@ class StereoPannerTest : public ::testing::Test { void SetUp() override { eventRegistry = std::make_shared(); - context = std::make_shared( - 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); + context = std::make_shared(2, 5 * sampleRate, sampleRate, eventRegistry); destination = std::make_shared(context); context->initialize(destination.get()); } diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/effects/TailProcessingTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/effects/TailProcessingTest.cpp index 24f872759..ece8e9e3d 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/effects/TailProcessingTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/effects/TailProcessingTest.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -75,8 +74,7 @@ class TailProcessingTest : public ::testing::Test { void SetUp() override { eventRegistry = std::make_shared(); - context = std::make_shared( - 2, 5 * kSampleRate, kSampleRate, eventRegistry, RuntimeRegistry{}); + context = std::make_shared(2, 5 * kSampleRate, kSampleRate, eventRegistry); destination = std::make_shared(context); context->initialize(destination.get()); } diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/effects/WaveShaperNodeTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/effects/WaveShaperNodeTest.cpp index 3666c93b9..f549004a3 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/effects/WaveShaperNodeTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/effects/WaveShaperNodeTest.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -21,8 +20,7 @@ class WaveShaperNodeTest : public ::testing::Test { void SetUp() override { eventRegistry = std::make_shared(); - context = std::make_shared( - 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); + context = std::make_shared(2, 5 * sampleRate, sampleRate, eventRegistry); } }; diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/effects/biquad/BiquadFilterTest.h b/packages/react-native-audio-api/common/cpp/test/src/core/effects/biquad/BiquadFilterTest.h index 17fe99703..dd6cf0b1f 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/effects/biquad/BiquadFilterTest.h +++ b/packages/react-native-audio-api/common/cpp/test/src/core/effects/biquad/BiquadFilterTest.h @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -19,8 +18,7 @@ class BiquadFilterTest : public ::testing::Test { void SetUp() override { eventRegistry = std::make_shared(); - context = std::make_shared( - 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); + context = std::make_shared(2, 5 * sampleRate, sampleRate, eventRegistry); } void expectCoefficientsNear( diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/sources/AudioBufferQueueSourceTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/sources/AudioBufferQueueSourceTest.cpp index f61605c0f..354cf24db 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/sources/AudioBufferQueueSourceTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/sources/AudioBufferQueueSourceTest.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -61,8 +60,7 @@ class AudioBufferQueueSourceTest : public ::testing::Test { void SetUp() override { eventRegistry = std::make_shared(); - context = std::make_shared( - 1, 5 * SAMPLE_RATE, SAMPLE_RATE, eventRegistry, RuntimeRegistry{}); + context = std::make_shared(1, 5 * SAMPLE_RATE, SAMPLE_RATE, eventRegistry); destination = std::make_shared(context); context->initialize(destination.get()); } @@ -162,8 +160,8 @@ TEST_F(AudioBufferQueueSourceTest, StreamingEnqueueKeepsPaceAndContinuity) { /// POSITIONS must still land every 1000 output frames if consumption pace is /// exactly 1x, and every other sample must stay silent. TEST_F(AudioBufferQueueSourceTest, FullGraphRenderPreservesPaceAndSilence) { - auto stereoContext = std::make_shared( - 2, 5 * SAMPLE_RATE, SAMPLE_RATE, eventRegistry, RuntimeRegistry{}); + auto stereoContext = + std::make_shared(2, 5 * SAMPLE_RATE, SAMPLE_RATE, eventRegistry); auto graph = stereoContext->getGraph(); diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/sources/AudioScheduledSourceTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/sources/AudioScheduledSourceTest.cpp index e47203fa7..2cb41e0e9 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/sources/AudioScheduledSourceTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/sources/AudioScheduledSourceTest.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -24,8 +23,7 @@ class AudioScheduledSourceTest : public ::testing::Test { void SetUp() override { eventRegistry = std::make_shared(); - context = std::make_shared( - 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); + context = std::make_shared(2, 5 * sampleRate, sampleRate, eventRegistry); destination = std::make_shared(context); context->initialize(destination.get()); } diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/sources/ConstantSourceTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/sources/ConstantSourceTest.cpp index c1ce53543..9ad111781 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/sources/ConstantSourceTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/sources/ConstantSourceTest.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -22,8 +21,7 @@ class ConstantSourceTest : public ::testing::Test { void SetUp() override { eventRegistry = std::make_shared(); - context = std::make_shared( - 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); + context = std::make_shared(2, 5 * sampleRate, sampleRate, eventRegistry); destination = std::make_shared(context); context->initialize(destination.get()); } diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/sources/MediaElementAudioSourceNodeTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/sources/MediaElementAudioSourceNodeTest.cpp index e359bcfe8..62b871e15 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/sources/MediaElementAudioSourceNodeTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/sources/MediaElementAudioSourceNodeTest.cpp @@ -75,8 +75,7 @@ class MediaElementAudioSourceNodeTest : public ::testing::Test { void SetUp() override { eventRegistry = std::make_shared(); - context = std::make_shared( - 2, 5 * kSampleRate, kSampleRate, eventRegistry, RuntimeRegistry{}); + context = std::make_shared(2, 5 * kSampleRate, kSampleRate, eventRegistry); destination = std::make_shared(context); context->initialize(destination.get()); } diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/sources/OscillatorTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/sources/OscillatorTest.cpp index d7e2f9feb..4afefa130 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/sources/OscillatorTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/sources/OscillatorTest.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -20,8 +19,7 @@ class OscillatorTest : public ::testing::Test { void SetUp() override { eventRegistry = std::make_shared(); - context = std::make_shared( - 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); + context = std::make_shared(2, 5 * sampleRate, sampleRate, eventRegistry); } }; diff --git a/packages/react-native-audio-api/common/cpp/test/src/graph/TestGraphUtils.h b/packages/react-native-audio-api/common/cpp/test/src/graph/TestGraphUtils.h index ca3f42d53..3783f1b0c 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/graph/TestGraphUtils.h +++ b/packages/react-native-audio-api/common/cpp/test/src/graph/TestGraphUtils.h @@ -30,8 +30,7 @@ namespace audioapi::utils::graph { inline std::shared_ptr getGraphTestContext() { static std::shared_ptr context = [] { auto eventRegistry = std::make_shared(); - auto ctx = - std::make_shared(2, 1024, 44100.0f, eventRegistry, RuntimeRegistry{}); + auto ctx = std::make_shared(2, 1024, 44100.0f, eventRegistry); return ctx; }(); return context; diff --git a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm index 33da72ef3..12206d697 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm @@ -1,16 +1,6 @@ #import #import -#import -#if RN_AUDIO_API_ENABLE_WORKLETS -// Forward-declare the single method used from WorkletsModule instead of importing -// , which transitively includes the codegen-generated -// that is not visible to this compile unit in all build setups -// (e.g. prebuilt RNWorklets in EAS builds). -@interface WorkletsModule : NSObject -- (std::shared_ptr)getWorkletsModuleProxy; -@end -#endif #ifdef RCT_NEW_ARCH_ENABLED #import #endif // RCT_NEW_ARCH_ENABLED @@ -24,7 +14,6 @@ @interface WorkletsModule : NSObject using namespace audioapi; using namespace facebook::react; -using namespace worklets; @interface RCTBridge (JSIRuntime) - (void *)runtime; @@ -41,7 +30,6 @@ - (void)_tryAndHandleError:(dispatch_block_t)block; @implementation AudioAPIModule { std::shared_ptr _eventHandler; - std::weak_ptr weakWorkletsModuleProxy_; } - (void)handleSessionDeactivation @@ -93,33 +81,7 @@ - (dispatch_queue_t)methodQueue _eventHandler = std::make_shared(jsiRuntime, jsCallInvoker); -#if RN_AUDIO_API_ENABLE_WORKLETS - WorkletsModule *workletsModule = [_moduleRegistry moduleForName:"WorkletsModule"]; - - if (!workletsModule) { - NSLog(@"WorkletsModule not found in module registry"); - } - - auto workletsModuleProxy = [workletsModule getWorkletsModuleProxy]; - - if (!workletsModuleProxy) { - NSLog(@"WorkletsModuleProxy not available"); - } - - weakWorkletsModuleProxy_ = workletsModuleProxy; - - auto uiWorkletRuntime = workletsModuleProxy->getUIWorkletRuntime(); - - if (!uiWorkletRuntime) { - NSLog(@"UI Worklet Runtime not available"); - } - - // Get the actual JSI Runtime reference - audioapi::AudioAPIModuleInstaller::injectJSIBindings( - jsiRuntime, jsCallInvoker, _eventHandler, uiWorkletRuntime); -#else audioapi::AudioAPIModuleInstaller::injectJSIBindings(jsiRuntime, jsCallInvoker, _eventHandler); -#endif NSLog(@"Successfully installed JSI bindings for react-native-audio-api!"); return @YES; diff --git a/packages/react-native-audio-api/package.json b/packages/react-native-audio-api/package.json index f2186bf33..31ed083fe 100644 --- a/packages/react-native-audio-api/package.json +++ b/packages/react-native-audio-api/package.json @@ -49,7 +49,6 @@ "!common/cpp/test", "scripts/setup-rn-audio-api-web.js", "scripts/rnaa_utils.rb", - "scripts/validate-worklets-version.js", "scripts/download-prebuilt-binaries.sh", "app.plugin.js" ], @@ -116,13 +115,7 @@ }, "peerDependencies": { "react": "*", - "react-native": "*", - "react-native-worklets": ">= 0.6.0" - }, - "peerDependenciesMeta": { - "react-native-worklets": { - "optional": true - } + "react-native": "*" }, "devDependencies": { "@babel/cli": "^7.20.0", diff --git a/packages/react-native-audio-api/scripts/rnaa_utils.rb b/packages/react-native-audio-api/scripts/rnaa_utils.rb index b291ddd93..9b2d28a77 100644 --- a/packages/react-native-audio-api/scripts/rnaa_utils.rb +++ b/packages/react-native-audio-api/scripts/rnaa_utils.rb @@ -1,12 +1,3 @@ -def check_if_worklets_enabled() - validate_worklets_script = File.expand_path(File.join(__dir__, 'validate-worklets-version.js')) - unless system("node \"#{validate_worklets_script}\"") - # If the validation script fails, we assume worklets are not present or have an incompatible version - return false - end - true -end - def try_to_parse_react_native_package_json(node_modules_dir) react_native_package_json_path = File.join(node_modules_dir, 'react-native/package.json') if !File.exist?(react_native_package_json_path) @@ -17,14 +8,10 @@ def try_to_parse_react_native_package_json(node_modules_dir) def find_audio_api_config() result = { - :worklets_enabled => nil, :react_native_common_dir => nil, - :dynamic_frameworks_audio_api_dir => nil, - :dynamic_frameworks_worklets_dir => nil + :dynamic_frameworks_audio_api_dir => nil } - result[:worklets_enabled] = check_if_worklets_enabled() - react_native_node_modules_dir = File.join(File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native/package.json')"`), '..') react_native_json = try_to_parse_react_native_package_json(react_native_node_modules_dir) @@ -47,12 +34,5 @@ def find_audio_api_config() react_native_audio_api_dir_relative = Pathname.new(react_native_audio_api_dir_absolute).relative_path_from(pods_root).to_s result[:dynamic_frameworks_audio_api_dir] = react_native_audio_api_dir_relative - if result[:worklets_enabled] == true - react_native_worklets_node_modules_dir = File.join(File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native-worklets/package.json')"`), '..') - react_native_worklets_dir_absolute = File.join(react_native_worklets_node_modules_dir, 'react-native-worklets') - react_native_worklets_dir_relative = Pathname.new(react_native_worklets_dir_absolute).relative_path_from(pods_root).to_s - result[:dynamic_frameworks_worklets_dir] = react_native_worklets_dir_relative - end - return result end diff --git a/packages/react-native-audio-api/scripts/validate-worklets-version.js b/packages/react-native-audio-api/scripts/validate-worklets-version.js deleted file mode 100644 index 9d2b0d99b..000000000 --- a/packages/react-native-audio-api/scripts/validate-worklets-version.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -const semverGte = require('semver/functions/gte'); - -const MIN_WORKLETS_VERSION = '0.6.0'; - - -/** - * Checks if the installed version of react-native-worklets is compatible with react-native-audio-api. - * @returns {boolean} True if the version is compatible, false otherwise. - */ -function validateVersion() { - let workletsVersion; - try { - const { version } = require('react-native-worklets/package.json'); - workletsVersion = version; - } catch (e) { - return false; - } - - return semverGte(workletsVersion, MIN_WORKLETS_VERSION); -} - -if (!validateVersion()) { - console.warn( - '[RNAudioApi] Incompatible version of react-native-worklets detected. Please install a compatible version if you want to use worklet nodes in react-native-audio-api.' - ); - process.exit(1); -} diff --git a/packages/react-native-audio-api/src/Audio/controls/AudioControls.tsx b/packages/react-native-audio-api/src/Audio/controls/AudioControls.tsx index 95c9d311e..07043d25f 100644 --- a/packages/react-native-audio-api/src/Audio/controls/AudioControls.tsx +++ b/packages/react-native-audio-api/src/Audio/controls/AudioControls.tsx @@ -59,10 +59,12 @@ const AudioControls: React.FC = () => { (x: number) => { progressTrackAnim.expand(); const d = durationRef.current; - progressTrackRef.current?.measureInWindow((_left, _y, width, _h) => { - progressMetricsWidth.value = width; - setScrubTime(timeFromLocationX(x, width, d)); - }); + progressTrackRef.current?.measureInWindow( + (_left: number, _y: number, width: number, _h: number) => { + progressMetricsWidth.value = width; + setScrubTime(timeFromLocationX(x, width, d)); + } + ); }, [progressTrackAnim, progressMetricsWidth, setScrubTime, progressTrackRef] ); @@ -147,9 +149,11 @@ const AudioControls: React.FC = () => { }, [playbackState, pause, play]); const onProgressTrackLayout = useCallback(() => { - progressTrackRef.current?.measureInWindow((_left, _y, width, _h) => { - progressMetricsWidth.value = width; - }); + progressTrackRef.current?.measureInWindow( + (_left: number, _y: number, width: number, _h: number) => { + progressMetricsWidth.value = width; + } + ); }, [progressMetricsWidth, progressTrackRef]); if (!ready) { diff --git a/packages/react-native-audio-api/src/AudioAPIModule/AudioAPIModule.ts b/packages/react-native-audio-api/src/AudioAPIModule/AudioAPIModule.ts index 22e00dbf3..c2e3f2e6c 100644 --- a/packages/react-native-audio-api/src/AudioAPIModule/AudioAPIModule.ts +++ b/packages/react-native-audio-api/src/AudioAPIModule/AudioAPIModule.ts @@ -1,25 +1,8 @@ import { NativeAudioAPIModule } from '../specs'; -import type { - WorkletRuntime, - IAudioAPIModule, - IWorkletsModule, -} from './ModuleInterfaces'; import { AudioApiError } from '../errors'; -import semverGte from 'semver/functions/gte'; - -class AudioAPIModule implements IAudioAPIModule { - #workletsModule_: IWorkletsModule | null = null; - #canUseWorklets_ = false; - #workletsVersion = 'unknown'; - #workletsAvailable_ = false; - private static readonly MIN_WORKLETS_VERSION = '0.6.0'; +class AudioAPIModule { constructor() { - // Important! Verify and import worklets first - // otherwise the native module installation might crash - // if react-native-worklets is not imported before audio-api - this.#verifyWorklets(); - if (this.#verifyInstallation()) { return; } @@ -33,29 +16,6 @@ class AudioAPIModule implements IAudioAPIModule { NativeAudioAPIModule.install(); } - #verifyWorklets(): boolean { - try { - const workletsPackage = require('react-native-worklets'); - const workletsPackageJson = require('react-native-worklets/package.json'); - this.#workletsVersion = workletsPackageJson.version; - this.#workletsAvailable_ = true; - - this.#canUseWorklets_ = semverGte( - this.#workletsVersion, - AudioAPIModule.MIN_WORKLETS_VERSION - ); - - if (this.#canUseWorklets_) { - this.#workletsModule_ = workletsPackage; - } - - return this.#canUseWorklets_; - } catch { - this.#canUseWorklets_ = false; - return false; - } - } - #verifyInstallation(): boolean { return ( globalThis.createAudioContext != null && @@ -67,53 +27,6 @@ class AudioAPIModule implements IAudioAPIModule { globalThis.AudioEventEmitter != null ); } - - get workletsModule(): IWorkletsModule | null { - return this.#workletsModule_; - } - - /** - * Indicates whether react-native-worklets are installed in matching version, - * for usage with react-native-audio-api. - */ - get canUseWorklets(): boolean { - return this.#canUseWorklets_; - } - - /** Returns the installed worklets version or 'unknown'. */ - get workletsVersion(): string { - return this.#workletsVersion; - } - - /** - * Indicates whether react-native-worklets are installed, regardless of - * version support. Useful for asserting compatibility. - */ - get areWorkletsAvailable(): boolean { - return this.#workletsAvailable_; - } - - /** - * Indicates whether the installed react-native-worklets version is supported. - * Useful for asserting compatibility. - */ - get isWorkletsVersionSupported(): boolean { - // Note: if areWorkletsAvailable is true, canUseWorklets is equivalent to version check - return this.#canUseWorklets_; - } - - /** Returns the range of supported worklets versions. */ - get supportedWorkletsVersion(): string[] { - return [`>=${AudioAPIModule.MIN_WORKLETS_VERSION}`]; - } - - public createAudioRuntime(): WorkletRuntime | null { - if (!this.#canUseWorklets_) { - return null; - } - - return this.#workletsModule_!.createWorkletRuntime('AudioWorkletRuntime'); - } } export default new AudioAPIModule(); diff --git a/packages/react-native-audio-api/src/AudioAPIModule/AudioAPIModule.web.ts b/packages/react-native-audio-api/src/AudioAPIModule/AudioAPIModule.web.ts index 34e2e2044..b32d02a4d 100644 --- a/packages/react-native-audio-api/src/AudioAPIModule/AudioAPIModule.web.ts +++ b/packages/react-native-audio-api/src/AudioAPIModule/AudioAPIModule.web.ts @@ -1,18 +1,3 @@ -import type { IAudioAPIModule, IWorkletsModule } from './ModuleInterfaces'; - -const mockGetter = (value: T) => value; - -class AudioAPIModule implements IAudioAPIModule { - public supportedWorkletsVersion = []; - workletsModule = mockGetter(null as IWorkletsModule | null); - canUseWorklets = mockGetter(false); - workletsVersion = mockGetter('unknown'); - areWorkletsAvailable = mockGetter(false); - isWorkletsVersionSupported = mockGetter(false); - - createAudioRuntime() { - return null; - } -} +class AudioAPIModule {} export default new AudioAPIModule(); diff --git a/packages/react-native-audio-api/src/AudioAPIModule/ModuleInterfaces.ts b/packages/react-native-audio-api/src/AudioAPIModule/ModuleInterfaces.ts deleted file mode 100644 index 48dafbb51..000000000 --- a/packages/react-native-audio-api/src/AudioAPIModule/ModuleInterfaces.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { ShareableWorkletCallback } from '../jsi-interfaces'; - -// Hint: Copied from react-native-worklets -// Doesn't really matter what is inside, just need a unique type -export interface WorkletRuntime { - __hostObjectWorkletRuntime: never; - readonly name: string; -} - -export interface IWorkletsModule { - makeShareableCloneRecursive: ( - workletCallback: ShareableWorkletCallback - ) => ShareableWorkletCallback; - - createWorkletRuntime: (runtimeName: string) => WorkletRuntime; -} - -export interface IAudioAPIModule { - get workletsModule(): IWorkletsModule | null; - get canUseWorklets(): boolean; - get workletsVersion(): string; - get areWorkletsAvailable(): boolean; - get isWorkletsVersionSupported(): boolean; - createAudioRuntime(): WorkletRuntime | null; -} diff --git a/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts b/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts index b5503d244..655cde36b 100644 --- a/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts +++ b/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts @@ -10,17 +10,11 @@ import type { /* eslint-disable no-var */ declare global { - var createAudioContext: ( - sampleRate: number, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - audioWorkletRuntime: any - ) => IAudioContext; + var createAudioContext: (sampleRate: number) => IAudioContext; var createOfflineAudioContext: ( numberOfChannels: number, length: number, - sampleRate: number, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - audioWorkletRuntime: any + sampleRate: number ) => IOfflineAudioContext; var createAudioRecorder: () => IAudioRecorder; diff --git a/packages/react-native-audio-api/src/api.ts b/packages/react-native-audio-api/src/api.ts index a415c5cdb..ca1cc5204 100644 --- a/packages/react-native-audio-api/src/api.ts +++ b/packages/react-native-audio-api/src/api.ts @@ -26,9 +26,6 @@ export { default as PeriodicWave } from './core/PeriodicWave'; export { default as StereoPannerNode } from './core/StereoPannerNode'; export { default as StreamerNode } from './core/StreamerNode'; export { default as WaveShaperNode } from './core/WaveShaperNode'; -export { default as WorkletNode } from './core/WorkletNode'; -export { default as WorkletProcessingNode } from './core/WorkletProcessingNode'; -export { default as WorkletSourceNode } from './core/WorkletSourceNode'; export * from './errors'; export * from './system/types'; diff --git a/packages/react-native-audio-api/src/core/AudioContext.ts b/packages/react-native-audio-api/src/core/AudioContext.ts index dd3fe69f5..852d14326 100644 --- a/packages/react-native-audio-api/src/core/AudioContext.ts +++ b/packages/react-native-audio-api/src/core/AudioContext.ts @@ -1,4 +1,3 @@ -import AudioAPIModule from '../AudioAPIModule'; import { NotSupportedError } from '../errors'; import { AudioTagHandle } from '../Audio/types'; import { IAudioContext } from '../jsi-interfaces'; @@ -19,12 +18,9 @@ export default class AudioContext extends BaseAudioContext { ); } - const audioRuntime = AudioAPIModule.createAudioRuntime(); - super( globalThis.createAudioContext( - options?.sampleRate || AudioManager.getDevicePreferredSampleRate(), - audioRuntime + options?.sampleRate || AudioManager.getDevicePreferredSampleRate() ) ); } diff --git a/packages/react-native-audio-api/src/core/BaseAudioContext.ts b/packages/react-native-audio-api/src/core/BaseAudioContext.ts index 73555006d..92a6af463 100644 --- a/packages/react-native-audio-api/src/core/BaseAudioContext.ts +++ b/packages/react-native-audio-api/src/core/BaseAudioContext.ts @@ -4,8 +4,7 @@ import { NotSupportedError, } from '../errors'; import { IBaseAudioContext } from '../jsi-interfaces'; -import { AudioWorkletRuntime, ContextState, DecodeDataInput } from '../types'; -import { assertWorkletsEnabled } from '../utils'; +import { ContextState, DecodeDataInput } from '../types'; import AnalyserNode from './AnalyserNode'; import AudioBuffer from './AudioBuffer'; import AudioBufferQueueSourceNode from './AudioBufferQueueSourceNode'; @@ -23,9 +22,6 @@ import PeriodicWave from './PeriodicWave'; import StereoPannerNode from './StereoPannerNode'; import StreamerNode from './StreamerNode'; import WaveShaperNode from './WaveShaperNode'; -import WorkletNode from './WorkletNode'; -import WorkletProcessingNode from './WorkletProcessingNode'; -import WorkletSourceNode from './WorkletSourceNode'; export default class BaseAudioContext { readonly destination: AudioDestinationNode; @@ -67,60 +63,6 @@ export default class BaseAudioContext { ); } - createWorkletNode( - callback: (audioData: Array, channelCount: number) => void, - bufferLength: number, - inputChannelCount: number, - workletRuntime: AudioWorkletRuntime = 'AudioRuntime' - ): WorkletNode { - if (inputChannelCount < 1 || inputChannelCount > 32) { - throw new NotSupportedError( - `The number of input channels provided (${inputChannelCount}) can not be less than 1 or greater than 32` - ); - } - - if (bufferLength < 1) { - throw new NotSupportedError( - `The buffer length provided (${bufferLength}) can not be less than 1` - ); - } - - assertWorkletsEnabled(); - return new WorkletNode( - this, - workletRuntime, - callback, - bufferLength, - inputChannelCount - ); - } - - createWorkletProcessingNode( - callback: ( - inputData: Array, - outputData: Array, - framesToProcess: number, - currentTime: number - ) => void, - workletRuntime: AudioWorkletRuntime = 'AudioRuntime' - ): WorkletProcessingNode { - assertWorkletsEnabled(); - return new WorkletProcessingNode(this, workletRuntime, callback); - } - - createWorkletSourceNode( - callback: ( - audioData: Array, - framesToProcess: number, - currentTime: number, - startOffset: number - ) => void, - workletRuntime: AudioWorkletRuntime = 'AudioRuntime' - ): WorkletSourceNode { - assertWorkletsEnabled(); - return new WorkletSourceNode(this, workletRuntime, callback); - } - createOscillator(): OscillatorNode { return new OscillatorNode(this); } diff --git a/packages/react-native-audio-api/src/core/OfflineAudioContext.ts b/packages/react-native-audio-api/src/core/OfflineAudioContext.ts index 519f8588f..8ac418090 100644 --- a/packages/react-native-audio-api/src/core/OfflineAudioContext.ts +++ b/packages/react-native-audio-api/src/core/OfflineAudioContext.ts @@ -1,4 +1,3 @@ -import AudioAPIModule from '../AudioAPIModule'; import { InvalidStateError, NotSupportedError } from '../errors'; import { IOfflineAudioContext } from '../jsi-interfaces'; import { OfflineAudioContextOptions } from '../types'; @@ -17,16 +16,13 @@ export default class OfflineAudioContext extends BaseAudioContext { arg1?: number, arg2?: number ) { - const audioRuntime = AudioAPIModule.createAudioRuntime(); - if (typeof arg0 === 'object') { const { numberOfChannels, length, sampleRate } = arg0; super( globalThis.createOfflineAudioContext( numberOfChannels, length, - sampleRate, - audioRuntime + sampleRate ) ); @@ -36,9 +32,7 @@ export default class OfflineAudioContext extends BaseAudioContext { typeof arg1 === 'number' && typeof arg2 === 'number' ) { - super( - globalThis.createOfflineAudioContext(arg0, arg1, arg2, audioRuntime) - ); + super(globalThis.createOfflineAudioContext(arg0, arg1, arg2)); this.duration = arg1 / arg2; } else { throw new NotSupportedError('Invalid constructor arguments'); diff --git a/packages/react-native-audio-api/src/core/WorkletNode.ts b/packages/react-native-audio-api/src/core/WorkletNode.ts deleted file mode 100644 index 984fb42e2..000000000 --- a/packages/react-native-audio-api/src/core/WorkletNode.ts +++ /dev/null @@ -1,32 +0,0 @@ -import AudioNode from './AudioNode'; -import type BaseAudioContext from './BaseAudioContext'; -import { AudioWorkletRuntime } from '../types'; -import AudioAPIModule from '../AudioAPIModule'; - -export default class WorkletNode extends AudioNode { - constructor( - context: BaseAudioContext, - runtime: AudioWorkletRuntime, - callback: (audioData: Array, channelCount: number) => void, - bufferLength: number, - inputChannelCount: number - ) { - const shareableWorklet = - AudioAPIModule.workletsModule!.makeShareableCloneRecursive( - (audioBuffers: Array, channelCount: number) => { - 'worklet'; - const floatAudioData: Array = audioBuffers.map( - (buffer) => new Float32Array(buffer) - ); - callback(floatAudioData, channelCount); - } - ); - const node = context.context.createWorkletNode( - shareableWorklet, - runtime === 'UIRuntime', - bufferLength, - inputChannelCount - ); - super(context, node); - } -} diff --git a/packages/react-native-audio-api/src/core/WorkletProcessingNode.ts b/packages/react-native-audio-api/src/core/WorkletProcessingNode.ts deleted file mode 100644 index c27219bdd..000000000 --- a/packages/react-native-audio-api/src/core/WorkletProcessingNode.ts +++ /dev/null @@ -1,41 +0,0 @@ -import AudioNode from './AudioNode'; -import type BaseAudioContext from './BaseAudioContext'; -import { AudioWorkletRuntime } from '../types'; -import AudioAPIModule from '../AudioAPIModule'; - -export default class WorkletProcessingNode extends AudioNode { - constructor( - context: BaseAudioContext, - runtime: AudioWorkletRuntime, - callback: ( - inputData: Array, - outputData: Array, - framesToProcess: number, - currentTime: number - ) => void - ) { - const shareableWorklet = - AudioAPIModule.workletsModule!.makeShareableCloneRecursive( - ( - inputBuffers: Array, - outputBuffers: Array, - framesToProcess: number, - currentTime: number - ) => { - 'worklet'; - const inputData: Array = inputBuffers.map( - (buffer) => new Float32Array(buffer, 0, framesToProcess) - ); - const outputData: Array = outputBuffers.map( - (buffer) => new Float32Array(buffer, 0, framesToProcess) - ); - callback(inputData, outputData, framesToProcess, currentTime); - } - ); - const node = context.context.createWorkletProcessingNode( - shareableWorklet, - runtime === 'UIRuntime' - ); - super(context, node); - } -} diff --git a/packages/react-native-audio-api/src/core/WorkletSourceNode.ts b/packages/react-native-audio-api/src/core/WorkletSourceNode.ts deleted file mode 100644 index f05d52bef..000000000 --- a/packages/react-native-audio-api/src/core/WorkletSourceNode.ts +++ /dev/null @@ -1,38 +0,0 @@ -import AudioScheduledSourceNode from './AudioScheduledSourceNode'; -import type BaseAudioContext from './BaseAudioContext'; -import { AudioWorkletRuntime } from '../types'; -import AudioAPIModule from '../AudioAPIModule'; - -export default class WorkletSourceNode extends AudioScheduledSourceNode { - constructor( - context: BaseAudioContext, - runtime: AudioWorkletRuntime, - callback: ( - audioData: Array, - framesToProcess: number, - currentTime: number, - startOffset: number - ) => void - ) { - const shareableWorklet = - AudioAPIModule.workletsModule!.makeShareableCloneRecursive( - ( - audioBuffers: Array, - framesToProcess: number, - currentTime: number, - startOffset: number - ) => { - 'worklet'; - const floatAudioData: Array = audioBuffers.map( - (buffer) => new Float32Array(buffer) - ); - callback(floatAudioData, framesToProcess, currentTime, startOffset); - } - ); - const node = context.context.createWorkletSourceNode( - shareableWorklet, - runtime === 'UIRuntime' - ); - super(context, node); - } -} diff --git a/packages/react-native-audio-api/src/jsi-interfaces.ts b/packages/react-native-audio-api/src/jsi-interfaces.ts index 470fa9e72..7f52313f4 100644 --- a/packages/react-native-audio-api/src/jsi-interfaces.ts +++ b/packages/react-native-audio-api/src/jsi-interfaces.ts @@ -29,30 +29,6 @@ import type { // IMPORTANT: use only IClass, because it is a part of contract between cpp host object and js layer -export type WorkletNodeCallback = ( - audioData: Array, - channelCount: number -) => void; - -export type WorkletSourceNodeCallback = ( - audioData: Array, - framesToProcess: number, - currentTime: number, - startOffset: number -) => void; - -export type WorkletProcessingNodeCallback = ( - inputData: Array, - outputData: Array, - framesToProcess: number, - currentTime: number -) => void; - -export type ShareableWorkletCallback = - | WorkletNodeCallback - | WorkletSourceNodeCallback - | WorkletProcessingNodeCallback; - export interface IBaseAudioContext { readonly destination: IAudioDestinationNode; readonly state: ContextState; @@ -61,20 +37,6 @@ export interface IBaseAudioContext { readonly decoder: IAudioDecoder; createRecorderAdapter(): IRecorderAdapterNode; - createWorkletSourceNode( - shareableWorklet: ShareableWorkletCallback, - shouldUseUiRuntime: boolean - ): IWorkletSourceNode; - createWorkletNode( - shareableWorklet: ShareableWorkletCallback, - shouldUseUiRuntime: boolean, - bufferLength: number, - inputChannelCount: number - ): IWorkletNode; - createWorkletProcessingNode( - shareableWorklet: ShareableWorkletCallback, - shouldUseUiRuntime: boolean - ): IWorkletProcessingNode; createOscillator(oscillatorOptions: OscillatorOptions): IOscillatorNode; createConstantSource( constantSourceOptions: ConstantSourceOptions @@ -318,12 +280,6 @@ export interface IAnalyserNode extends IAudioNode { export interface IRecorderAdapterNode extends IAudioNode {} -export interface IWorkletNode extends IAudioNode {} - -export interface IWorkletSourceNode extends IAudioScheduledSourceNode {} - -export interface IWorkletProcessingNode extends IAudioNode {} - export interface IWaveShaperNode extends IAudioNode { readonly curve: Float32Array | null; oversample: OverSampleType; diff --git a/packages/react-native-audio-api/src/mock/index.ts b/packages/react-native-audio-api/src/mock/index.ts index 31d895892..e85576c35 100644 --- a/packages/react-native-audio-api/src/mock/index.ts +++ b/packages/react-native-audio-api/src/mock/index.ts @@ -3,7 +3,6 @@ import { AudioRecorderCallbackOptions, AudioRecorderFileOptions, AudioRecorderStartOptions, - AudioWorkletRuntime, BiquadFilterType, ChannelCountMode, ChannelInterpretation, @@ -483,48 +482,6 @@ class MediaElementAudioSourceNodeMock extends AudioNodeMock { } } -class WorkletNodeMock extends AudioNodeMock { - constructor( - context: BaseAudioContextMock, - _runtime: AudioWorkletRuntime, - _callback: (audioData: Array, channelCount: number) => void, - _bufferLength: number, - _inputChannelCount: number - ) { - super(context, {}); - } -} - -class WorkletProcessingNodeMock extends AudioNodeMock { - constructor( - context: BaseAudioContextMock, - _runtime: AudioWorkletRuntime, - _callback: ( - inputData: Array, - outputData: Array, - framesToProcess: number, - currentTime: number - ) => void - ) { - super(context, {}); - } -} - -class WorkletSourceNodeMock extends AudioScheduledSourceNodeMock { - constructor( - context: BaseAudioContextMock, - _runtime: AudioWorkletRuntime, - _callback: ( - audioData: Array, - framesToProcess: number, - currentTime: number, - startOffset: number - ) => void - ) { - super(context, {}); - } -} - class PeriodicWaveMock { constructor(_context: BaseAudioContextMock, _options?: PeriodicWaveOptions) {} } @@ -654,29 +611,6 @@ class BaseAudioContextMock { createStreamer(options: StreamerOptions): StreamerNodeMock { return new StreamerNodeMock(this, options); } - - createWorkletNode( - _shareableWorklet: Record, - _runOnUI: boolean, - _bufferLength: number, - _inputChannelCount: number - ): WorkletNodeMock { - return new WorkletNodeMock(this, 'AudioRuntime', noop, 0, 0); - } - - createWorkletProcessingNode( - _shareableWorklet: Record, - _runOnUI: boolean - ): WorkletProcessingNodeMock { - return new WorkletProcessingNodeMock(this, 'AudioRuntime', noop); - } - - createWorkletSourceNode( - _shareableWorklet: Record, - _runOnUI: boolean - ): WorkletSourceNodeMock { - return new WorkletSourceNodeMock(this, 'AudioRuntime', noop); - } } class AudioContextMock extends BaseAudioContextMock { @@ -1075,9 +1009,6 @@ export const RecorderAdapterNode = RecorderAdapterNodeMock; export const StereoPannerNode = StereoPannerNodeMock; export const StreamerNode = StreamerNodeMock; export const WaveShaperNode = WaveShaperNodeMock; -export const WorkletNode = WorkletNodeMock; -export const WorkletProcessingNode = WorkletProcessingNodeMock; -export const WorkletSourceNode = WorkletSourceNodeMock; export const PeriodicWave = PeriodicWaveMock; export const AudioManager = AudioManagerMock; @@ -1129,9 +1060,6 @@ export type RecorderAdapterNode = RecorderAdapterNodeMock; export type StereoPannerNode = StereoPannerNodeMock; export type StreamerNode = StreamerNodeMock; export type WaveShaperNode = WaveShaperNodeMock; -export type WorkletNode = WorkletNodeMock; -export type WorkletProcessingNode = WorkletProcessingNodeMock; -export type WorkletSourceNode = WorkletSourceNodeMock; export type PeriodicWave = PeriodicWaveMock; // Export types and enums @@ -1140,7 +1068,6 @@ export { AudioRecorderCallbackOptions, AudioRecorderFileOptions, AudioRecorderStartOptions, - AudioWorkletRuntime, BiquadFilterType, ChannelCountMode, ChannelInterpretation, @@ -1192,9 +1119,6 @@ export default { StereoPannerNode: StereoPannerNodeMock, StreamerNode: StreamerNodeMock, WaveShaperNode: WaveShaperNodeMock, - WorkletNode: WorkletNodeMock, - WorkletProcessingNode: WorkletProcessingNodeMock, - WorkletSourceNode: WorkletSourceNodeMock, PeriodicWave: PeriodicWaveMock, // Functions diff --git a/packages/react-native-audio-api/src/types.ts b/packages/react-native-audio-api/src/types.ts index 9eba289a4..94f57e44a 100644 --- a/packages/react-native-audio-api/src/types.ts +++ b/packages/react-native-audio-api/src/types.ts @@ -38,8 +38,6 @@ export type BiquadFilterType = export type ContextState = 'running' | 'closed' | 'suspended'; -export type AudioWorkletRuntime = 'AudioRuntime' | 'UIRuntime'; - export type OscillatorType = | 'sine' | 'square' diff --git a/packages/react-native-audio-api/src/utils/index.ts b/packages/react-native-audio-api/src/utils/index.ts index 766b20f99..b47242519 100644 --- a/packages/react-native-audio-api/src/utils/index.ts +++ b/packages/react-native-audio-api/src/utils/index.ts @@ -1,23 +1,5 @@ -import AudioAPIModule from '../AudioAPIModule'; -import { AudioApiError } from '../errors'; - export { isFfmpegEnabled } from './flags'; -export function assertWorkletsEnabled() { - if (!AudioAPIModule.areWorkletsAvailable) { - throw new AudioApiError( - '[react-native-audio-api]: Worklets are not available. Please install react-native-worklets to use this feature.' - ); - } - - if (!AudioAPIModule.isWorkletsVersionSupported) { - throw new AudioApiError( - `[react-native-audio-api]: Worklets version ${AudioAPIModule.workletsVersion} is not supported. - Please install react-native-worklets of one of the following versions: [${AudioAPIModule.supportedWorkletsVersion.join(', ')}] to use this feature.` - ); - } -} - export function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } diff --git a/packages/react-native-audio-api/tests/integration.test.ts b/packages/react-native-audio-api/tests/integration.test.ts index 8909a1ea3..069e7a68d 100644 --- a/packages/react-native-audio-api/tests/integration.test.ts +++ b/packages/react-native-audio-api/tests/integration.test.ts @@ -137,56 +137,6 @@ describe('Mock Integration Tests', () => { }); }); - describe('Worklet Processing', () => { - it('should create and use worklet nodes for custom processing', () => { - const context = new MockAPI.AudioContext(); - - const processingCallback = jest.fn( - (inputData, outputData, framesToProcess) => { - // Mock audio processing logic - for (let channel = 0; channel < outputData.length; channel++) { - for (let i = 0; i < framesToProcess; i++) { - outputData[channel][i] = inputData[channel][i] * 0.5; // Simple gain - } - } - } - ); - - const workletNode = new MockAPI.WorkletProcessingNode( - context, - 'AudioRuntime', - processingCallback - ); - - expect(workletNode).toBeInstanceOf(MockAPI.WorkletProcessingNode); - expect(workletNode.context).toBe(context); - }); - - it('should create worklet source node for custom audio generation', () => { - const context = new MockAPI.AudioContext(); - - const sourceCallback = jest.fn( - (audioData, framesToProcess, currentTime) => { - // Mock audio generation logic - for (let channel = 0; channel < audioData.length; channel++) { - for (let i = 0; i < framesToProcess; i++) { - audioData[channel][i] = Math.sin(currentTime + i); // Simple sine wave - } - } - } - ); - - const workletSource = new MockAPI.WorkletSourceNode( - context, - 'UIRuntime', - sourceCallback - ); - - expect(workletSource).toBeInstanceOf(MockAPI.WorkletSourceNode); - expect(workletSource.context).toBe(context); - }); - }); - describe('Buffer Queue Management', () => { it('should manage audio buffer queue for seamless playback', () => { const context = new MockAPI.AudioContext(); diff --git a/packages/react-native-audio-api/tests/mock.test.ts b/packages/react-native-audio-api/tests/mock.test.ts index c8834bc14..3db5e835f 100644 --- a/packages/react-native-audio-api/tests/mock.test.ts +++ b/packages/react-native-audio-api/tests/mock.test.ts @@ -211,36 +211,6 @@ describe('React Native Audio API Mocks', () => { }); }); - describe('WorkletNodes', () => { - it('should create WorkletNode', () => { - const workletNode = new MockAPI.WorkletNode( - context, - 'AudioRuntime', - () => {}, - 1024, - 2 - ); - expect(workletNode).toBeInstanceOf(MockAPI.WorkletNode); - }); - - it('should create WorkletProcessingNode', () => { - const processingNode = new MockAPI.WorkletProcessingNode( - context, - 'AudioRuntime', - () => {} - ); - expect(processingNode).toBeInstanceOf(MockAPI.WorkletProcessingNode); - }); - - it('should create WorkletSourceNode', () => { - const sourceNode = new MockAPI.WorkletSourceNode( - context, - 'AudioRuntime', - () => {} - ); - expect(sourceNode).toBeInstanceOf(MockAPI.WorkletSourceNode); - }); - }); }); describe('AudioRecorder', () => { diff --git a/packages/react-native-audio-worklets/.clang-format b/packages/react-native-audio-worklets/.clang-format new file mode 100644 index 000000000..01ea226d1 --- /dev/null +++ b/packages/react-native-audio-worklets/.clang-format @@ -0,0 +1,83 @@ +--- +ColumnLimit: 100 +AccessModifierOffset: -1 +AlignAfterOpenBracket: false +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: DontAlign +AlignOperands: false +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortCompoundRequirementOnASingleLine: true +AllowShortEnumsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Inline +AllowShortLoopsOnASingleLine: false +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: true +BinPackArguments: false +BinPackParameters: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: false +CommentPragmas: '^ IWYU pragma:' +PackConstructorInitializers: NextLine +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ForEachMacros: [FOR_EACH_RANGE, FOR_EACH] +IncludeCategories: + - Regex: '^<.*\.h(pp)?>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IndentCaseLabels: true +IndentWidth: 2 +IndentWrappedFunctionNames: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Right +ReflowComments: false +RequiresExpressionIndentation: OuterScope +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: Never +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInSquareBrackets: false +UseTab: Never +--- +Language: Cpp +BreakAfterOpenBracketBracedList: true +BreakAfterOpenBracketFunction: true +Standard: c++20 +--- +Language: ObjC +BreakBeforeBraces: WebKit +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true +BreakAfterOpenBracketBracedList: true +BreakAfterOpenBracketFunction: true diff --git a/packages/react-native-audio-worklets/.clang-tidy b/packages/react-native-audio-worklets/.clang-tidy new file mode 100644 index 000000000..775152a31 --- /dev/null +++ b/packages/react-native-audio-worklets/.clang-tidy @@ -0,0 +1,30 @@ +# Full clang-tidy configuration for react-native-audio-worklets C++. +# See: https://clang.llvm.org/extra/clang-tidy/checks/list.html +# Tweak checks in .clangd (Diagnostics.ClangTidy.Remove) to disable noisy ones. + +Checks: '-*, + bugprone-*, + -bugprone-easily-swappable-parameters, + modernize-*, + -modernize-use-trailing-return-type, + performance-*, + readability-*, + -readability-uppercase-literal-suffix, + -readability-math-missing-parentheses, + -readability-isolate-declaration, + -readability-identifier-length, + cppcoreguidelines-*, + -cppcoreguidelines-non-private-member-variables-in-classes, + -cppcoreguidelines-pro-bounds-avoid-unchecked-container-access, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-type-union-access, + -cppcoreguidelines-pro-type-reinterpret-cast, + -cppcoreguidelines-avoid-do-while, + -cppcoreguidelines-pro-bounds-constant-array-index, + -cppcoreguidelines-init-variables, + -cppcoreguidelines-pro-type-vararg, + concurrency-*' + +HeaderFilterRegex: '.*/(audioworklets|common/cpp)/.*' + +FormatStyle: file diff --git a/packages/react-native-audio-worklets/.clangd b/packages/react-native-audio-worklets/.clangd new file mode 100644 index 000000000..e735c6397 --- /dev/null +++ b/packages/react-native-audio-worklets/.clangd @@ -0,0 +1,11 @@ +Documentation: + CommentFormat: Doxygen + +CompileFlags: + Add: + - -std=c++20 + - -Wall + +Diagnostics: + ClangTidy: + FastCheckFilter: Loose diff --git a/packages/react-native-audio-worklets/CMakeLists.txt b/packages/react-native-audio-worklets/CMakeLists.txt new file mode 100644 index 000000000..12f8140e6 --- /dev/null +++ b/packages/react-native-audio-worklets/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.13) +project(react-native-audio-worklets) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_VERBOSE_MAKEFILE ON) + +file(GLOB_RECURSE AUDIO_WORKLETS_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/common/cpp/audioworklets/*.cpp") + +add_library(react-native-audio-worklets STATIC ${AUDIO_WORKLETS_SOURCES}) + +target_compile_definitions(react-native-audio-worklets PRIVATE RCT_NEW_ARCH_ENABLED) + +target_include_directories(react-native-audio-worklets + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/common/cpp" +) + +find_package(ReactAndroid REQUIRED CONFIG) +find_package(fbjni REQUIRED CONFIG) + +find_package(react-native-worklets REQUIRED CONFIG) +find_package(react-native-audio-api REQUIRED CONFIG) + +target_link_libraries(react-native-audio-worklets + fbjni::fbjni + ReactAndroid::jsi + ReactAndroid::reactnative + react_codegen_rnaudioworklets + react-native-worklets::worklets + react-native-audio-api::react-native-audio-api + android + log +) diff --git a/packages/react-native-audio-worklets/RNAudioWorklets.podspec b/packages/react-native-audio-worklets/RNAudioWorklets.podspec new file mode 100644 index 000000000..34e600226 --- /dev/null +++ b/packages/react-native-audio-worklets/RNAudioWorklets.podspec @@ -0,0 +1,54 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +if ENV['RCT_NEW_ARCH_ENABLED'] != '1' + raise "[RNAudioWorklets] requires React Native New Architecture. Set ENV['RCT_NEW_ARCH_ENABLED'] = '1' in your Podfile." +end + +fabric_flags = '-DRCT_NEW_ARCH_ENABLED' +version_flag = "-DAUDIO_WORKLETS_VERSION=#{package['version']}" +ios_min_version = '14.0' + +Pod::Spec.new do |s| + s.name = "RNAudioWorklets" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => ios_min_version } + s.source = { :git => "https://github.com/software-mansion/react-native-audio-api.git", :tag => "#{s.version}" } + + s.source_files = "common/cpp/audioworklets/**/*.{cpp,h}", "ios/audioworklets/**/*.{mm,h}" + s.header_dir = "audioworklets" + s.header_mappings_dir = "common/cpp/audioworklets" + + s.dependency 'RNAudioAPI' + s.dependency 'RNWorklets' + s.dependency 'React-jsi' + + s.pod_target_xcconfig = { + "USE_HEADERMAP" => "YES", + "DEFINES_MODULE" => "YES", + "HEADER_SEARCH_PATHS" => [ + '"$(PODS_TARGET_SRCROOT)/ReactCommon"', + '"$(PODS_TARGET_SRCROOT)"', + '"$(PODS_ROOT)/RCT-Folly"', + '"$(PODS_ROOT)/boost"', + '"$(PODS_ROOT)/boost-for-react-native"', + '"$(PODS_ROOT)/DoubleConversion"', + '"$(PODS_ROOT)/Headers/Private/React-Core"', + '"$(PODS_ROOT)/Headers/Private/Yoga"', + '"$(PODS_ROOT)/Headers/Public/RNAudioAPI"', + '"$(PODS_ROOT)/Headers/Public/RNWorklets"', + '"$(PODS_ROOT)/Headers/Private/ReactCodegen"', + '"$(PODS_ROOT)/../build/generated/ios/ReactCodegen"', + ].join(' '), + "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", + "OTHER_CFLAGS" => "$(inherited) #{fabric_flags} #{version_flag}", + } + + install_modules_dependencies(s) +end diff --git a/packages/react-native-audio-worklets/android/build.gradle b/packages/react-native-audio-worklets/android/build.gradle new file mode 100644 index 000000000..cf04cc9b6 --- /dev/null +++ b/packages/react-native-audio-worklets/android/build.gradle @@ -0,0 +1,132 @@ +import com.android.Version +import org.apache.tools.ant.taskdefs.condition.Os + +buildscript { + ext.getExtOrDefault = { name -> + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["AudioWorklets_" + name] + } + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:8.7.2" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}" + } +} + +def getExtOrDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["AudioWorklets_" + name] +} + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["AudioWorklets_" + name]).toInteger() +} + +def isNewArchitectureEnabled() { + return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" +} + +if (!isNewArchitectureEnabled()) { + throw new GradleException( + "[react-native-audio-worklets] requires React Native New Architecture. " + + "Set newArchEnabled=true in your app's android/gradle.properties." + ) +} + +def safeAppExtGet(prop, fallback) { + def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') } + return appProject?.ext?.has(prop) ? appProject.ext.get(prop) : fallback +} + +def resolveReactNativeDirectory() { + def reactNativeLocation = safeAppExtGet("REACT_NATIVE_NODE_MODULES_DIR", null) + + if (reactNativeLocation != null) { + return file(reactNativeLocation) + } + + def reactNativePackage = file(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()) + if (reactNativePackage.exists()) { + return reactNativePackage.parentFile + } + + throw new GradleException( + "[AudioWorklets] Unable to resolve react-native location in node_modules." + ) +} + +def supportsNamespace() { + def parsed = Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.') + def major = parsed[0].toInteger() + def minor = parsed[1].toInteger() + + return (major == 7 && minor >= 3) || major >= 8 +} + +apply plugin: "com.android.library" +apply plugin: "org.jetbrains.kotlin.android" + +apply plugin: "com.facebook.react" + +android { + if (supportsNamespace()) { + namespace "com.swmansion.audioworklets" + + sourceSets { + main { + manifest.srcFile "src/main/AndroidManifestNew.xml" + } + } + } + + ndkVersion getExtOrDefault("ndkVersion") + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + } + + buildTypes { + release { + minifyEnabled false + } + } + + lintOptions { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + sourceSets { + main { + java.srcDirs += ["${project.buildDir}/generated/source/codegen/java"] + } + } +} + +repositories { + mavenCentral() + google() +} + +dependencies { + implementation "com.facebook.react:react-android" +} + +react { + jsRootDir = file("../src/") + libraryName = "rnaudioworklets" + codegenJavaPackageName = "com.swmansion.audioworklets" +} diff --git a/packages/react-native-audio-worklets/android/gradle.properties b/packages/react-native-audio-worklets/android/gradle.properties new file mode 100644 index 000000000..eede26377 --- /dev/null +++ b/packages/react-native-audio-worklets/android/gradle.properties @@ -0,0 +1,5 @@ +AudioWorklets_kotlinVersion=2.1.20 +AudioWorklets_minSdkVersion=24 +AudioWorklets_targetSdkVersion=36 +AudioWorklets_compileSdkVersion=36 +AudioWorklets_ndkVersion=27.1.12297006 diff --git a/packages/react-native-audio-worklets/android/src/main/AndroidManifestNew.xml b/packages/react-native-audio-worklets/android/src/main/AndroidManifestNew.xml new file mode 100644 index 000000000..94cbbcfc3 --- /dev/null +++ b/packages/react-native-audio-worklets/android/src/main/AndroidManifestNew.xml @@ -0,0 +1 @@ + diff --git a/packages/react-native-audio-worklets/android/src/main/java/com/swmansion/audioworklets/AudioWorkletsPackage.kt b/packages/react-native-audio-worklets/android/src/main/java/com/swmansion/audioworklets/AudioWorkletsPackage.kt new file mode 100644 index 000000000..a1653c8ba --- /dev/null +++ b/packages/react-native-audio-worklets/android/src/main/java/com/swmansion/audioworklets/AudioWorkletsPackage.kt @@ -0,0 +1,20 @@ +package com.swmansion.audioworklets + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfoProvider + +/** + * Stub [BaseReactPackage] so React Native autolinking can include this library as a Gradle + * project. The TurboModule is a pure C++ module registered via autolinking's + * `autolinking_cxxModuleProvider` (see `react-native.config.js` cxxModule* fields). + */ +class AudioWorkletsPackage : BaseReactPackage() { + override fun getModule( + name: String, + reactContext: ReactApplicationContext, + ): NativeModule? = null + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { emptyMap() } +} diff --git a/packages/react-native-audio-worklets/babel.config.js b/packages/react-native-audio-worklets/babel.config.js new file mode 100644 index 000000000..f7b3da3b3 --- /dev/null +++ b/packages/react-native-audio-worklets/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], +}; diff --git a/packages/react-native-audio-worklets/common/cpp/NativeAudioWorkletsModule.h b/packages/react-native-audio-worklets/common/cpp/NativeAudioWorkletsModule.h new file mode 100644 index 000000000..428152012 --- /dev/null +++ b/packages/react-native-audio-worklets/common/cpp/NativeAudioWorkletsModule.h @@ -0,0 +1,5 @@ +#pragma once + +// Autolinking includes `` (see cxxModuleHeaderName in +// react-native.config.js). The implementation lives under audioworklets/. +#include diff --git a/packages/react-native-audio-worklets/common/cpp/audioworklets/AudioWorkletsInstaller.h b/packages/react-native-audio-worklets/common/cpp/audioworklets/AudioWorkletsInstaller.h new file mode 100644 index 000000000..f2d235ae4 --- /dev/null +++ b/packages/react-native-audio-worklets/common/cpp/audioworklets/AudioWorkletsInstaller.h @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace audioworklets { + +using namespace facebook; + +class AudioWorkletsInstaller { + public: + static void inject(jsi::Runtime &runtime) { + runtime.global().setProperty( + runtime, + "__createWorkletNode", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "__createWorkletNode"), + 4, + createWorkletNode)); + } + + private: + static std::shared_ptr getContextOrThrow( + jsi::Runtime &runtime, + const jsi::Value &arg) { + auto contextObject = arg.asObject(runtime); + auto contextHostObject = + contextObject.getHostObject(runtime); + if (contextHostObject == nullptr) { + throw jsi::JSError( + runtime, "[react-native-audio-worklets] first argument is not a valid AudioContext"); + } + + return contextHostObject->getContext(); + } + + static std::shared_ptr getSerializableWorkletOrThrow( + jsi::Runtime &runtime, + const jsi::Value &arg) { + return worklets::extractSerializable( + runtime, + arg, + "[react-native-audio-worklets] expected a worklet as the second argument", + worklets::Serializable::ValueType::WorkletType); + } + + static jsi::Value createWorkletNode( + jsi::Runtime &runtime, + const jsi::Value & /*thisValue*/, + const jsi::Value *args, + size_t count) { + if (count < 4) { + throw jsi::JSError( + runtime, "[react-native-audio-worklets] __createWorkletNode expects 4 arguments"); + } + const auto &context = getContextOrThrow(runtime, args[0]); + auto serializableWorklet = getSerializableWorkletOrThrow(runtime, args[1]); + + auto uiRuntimeHolder = args[2].asObject(runtime); + auto uiRuntime = worklets::getWorkletRuntimeFromHolder(runtime, uiRuntimeHolder); + if (uiRuntime == nullptr) { + throw jsi::JSError( + runtime, + "[react-native-audio-worklets] could not resolve the UI worklet runtime. " + "Make sure react-native-worklets is installed."); + } + + auto uiSchedulerHolder = args[3].asObject(runtime); + auto uiScheduler = worklets::getUISchedulerFromHolder(runtime, uiSchedulerHolder); + if (uiScheduler == nullptr) { + throw jsi::JSError( + runtime, + "[react-native-audio-worklets] could not resolve the UI scheduler. " + "Make sure react-native-worklets is installed."); + } + + auto hostObject = std::make_shared( + context->getGraph(), + context, + std::move(uiRuntime), + std::move(uiScheduler), + std::move(serializableWorklet)); + + auto object = jsi::Object::createFromHostObject(runtime, hostObject); + object.setExternalMemoryPressure(runtime, hostObject->getMemoryPressure()); + return object; + } +}; + +} // namespace audioworklets diff --git a/packages/react-native-audio-worklets/common/cpp/audioworklets/HostObjects/WorkletNodeHostObject.h b/packages/react-native-audio-worklets/common/cpp/audioworklets/HostObjects/WorkletNodeHostObject.h new file mode 100644 index 000000000..f8cd5710a --- /dev/null +++ b/packages/react-native-audio-worklets/common/cpp/audioworklets/HostObjects/WorkletNodeHostObject.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace audioworklets { + +using namespace facebook; + +class WorkletNodeHostObject : public audioapi::AudioNodeHostObject { + public: + WorkletNodeHostObject( + const std::shared_ptr &graph, + const std::shared_ptr &context, + std::shared_ptr uiRuntime, + std::shared_ptr uiScheduler, + std::shared_ptr serializableWorklet) + : audioapi::AudioNodeHostObject( + graph, + std::make_unique( + context, + UIWorkletsRunner( + std::move(uiRuntime), + std::move(uiScheduler), + std::move(serializableWorklet)))) {} + + [[nodiscard]] size_t getMemoryPressure() const override { + return AudioNodeHostObject::getMemoryPressure() + + static_cast(audioapi::MAX_CHANNEL_COUNT) * audioapi::RENDER_QUANTUM_SIZE * + sizeof(float); + } +}; + +} // namespace audioworklets diff --git a/packages/react-native-audio-worklets/common/cpp/audioworklets/NativeAudioWorkletsModule.cpp b/packages/react-native-audio-worklets/common/cpp/audioworklets/NativeAudioWorkletsModule.cpp new file mode 100644 index 000000000..f6d07ab4b --- /dev/null +++ b/packages/react-native-audio-worklets/common/cpp/audioworklets/NativeAudioWorkletsModule.cpp @@ -0,0 +1,17 @@ +#include +#include + +#include +#include + +namespace facebook::react { + +NativeAudioWorkletsModule::NativeAudioWorkletsModule(std::shared_ptr jsInvoker) + : NativeAudioWorkletsModuleCxxSpec(std::move(jsInvoker)) {} + +bool NativeAudioWorkletsModule::install(jsi::Runtime &runtime) { + audioworklets::AudioWorkletsInstaller::inject(runtime); + return true; +} + +} // namespace facebook::react diff --git a/packages/react-native-audio-worklets/common/cpp/audioworklets/NativeAudioWorkletsModule.h b/packages/react-native-audio-worklets/common/cpp/audioworklets/NativeAudioWorkletsModule.h new file mode 100644 index 000000000..4ba19bc1e --- /dev/null +++ b/packages/react-native-audio-worklets/common/cpp/audioworklets/NativeAudioWorkletsModule.h @@ -0,0 +1,20 @@ +#pragma once + +// Generated by React Native codegen from `src/specs/NativeAudioWorkletsModule.ts` +// (codegenConfig name: "rnaudioworklets"). +#include + +#include +#include + +namespace facebook::react { + +class NativeAudioWorkletsModule + : public NativeAudioWorkletsModuleCxxSpec { + public: + explicit NativeAudioWorkletsModule(std::shared_ptr jsInvoker); + + bool install(jsi::Runtime &runtime); +}; + +} // namespace facebook::react diff --git a/packages/react-native-audio-worklets/common/cpp/audioworklets/UIWorkletsRunner.cpp b/packages/react-native-audio-worklets/common/cpp/audioworklets/UIWorkletsRunner.cpp new file mode 100644 index 000000000..7c1c08023 --- /dev/null +++ b/packages/react-native-audio-worklets/common/cpp/audioworklets/UIWorkletsRunner.cpp @@ -0,0 +1,49 @@ +#include + +#include +#include +#include + +namespace audioworklets { + +UIWorkletsRunner::UIWorkletsRunner( + std::shared_ptr uiRuntime, + std::shared_ptr uiScheduler, + std::shared_ptr serializableWorklet) + : uiRuntime_(std::move(uiRuntime)), + uiScheduler_(std::move(uiScheduler)), + serializableWorklet_(std::move(serializableWorklet)) {} + +void UIWorkletsRunner::invokeOnUI( + std::shared_ptr>> channels, + size_t channelCount, + std::function onComplete) const { + worklets::scheduleOnUI( + uiScheduler_, + [uiRuntime = uiRuntime_, + serializableWorklet = serializableWorklet_, + channels = std::move(channels), + channelCount, + onComplete = std::move(onComplete)]() { + std::atomic_thread_fence(std::memory_order_acquire); + + jsi::Runtime &rt = worklets::getJSIRuntimeFromWorkletRuntime(uiRuntime); + + auto audioData = jsi::Array(rt, channelCount); + for (size_t ch = 0; ch < channelCount; ++ch) { + audioData.setValueAtIndex(rt, ch, jsi::ArrayBuffer(rt, (*channels)[ch])); + } + + worklets::runSyncOnRuntime( + uiRuntime, + serializableWorklet, + jsi::Value(std::move(audioData)), + jsi::Value(static_cast(channelCount))); + + if (onComplete) { + onComplete(); + } + }); +} + +} // namespace audioworklets diff --git a/packages/react-native-audio-worklets/common/cpp/audioworklets/UIWorkletsRunner.h b/packages/react-native-audio-worklets/common/cpp/audioworklets/UIWorkletsRunner.h new file mode 100644 index 000000000..752ceed6f --- /dev/null +++ b/packages/react-native-audio-worklets/common/cpp/audioworklets/UIWorkletsRunner.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include + +namespace audioworklets { + +using namespace facebook; + +/** + * Drives a worklet on the shared UI worklet runtime for UI-animation nodes. + * + * Built on the stable C++ APIs of both dependencies: + * - `audioapi/compatibility/StableAPI.h` for audio buffers, + * - `worklets/Compat/StableApi.h` for UI-thread scheduling and worklet invocation. + * + * Model: the UI worklet runtime is owned/driven by the UI thread (it is the + * same runtime react-native-reanimated animates on). We must never touch its + * JSI runtime from the audio thread, so audio data is snapshotted on the audio + * thread and the actual worklet invocation is marshalled onto the UI thread via + * `scheduleOnUI`. This is fire-and-forget: UI animation does not need a result + * and must not block the audio thread. + * + * All state is held in shared pointers so a scheduled job stays valid even if + * the owning node is destroyed before the job runs on the UI thread. + */ +class UIWorkletsRunner { + public: + UIWorkletsRunner( + std::shared_ptr uiRuntime, + std::shared_ptr uiScheduler, + std::shared_ptr serializableWorklet); + + [[nodiscard]] bool isValid() const { + return uiRuntime_ != nullptr && uiScheduler_ != nullptr && serializableWorklet_ != nullptr; + } + + /// @brief Schedules the worklet to run on the UI thread with the given audio + /// data. The channel buffer pool is captured (kept alive) by the scheduled job; + /// the JSI arrays are built on the UI runtime, inside the UI thread. + /// @param channels Shared, fixed-size pool of per-channel snapshot buffers. + /// @param channelCount Number of channels to expose to the worklet. + /// @param onComplete Invoked on the UI thread once the worklet returns. + /// @note Audio Thread only. + void invokeOnUI( + std::shared_ptr>> channels, + size_t channelCount, + std::function onComplete) const; + + private: + std::shared_ptr uiRuntime_; + std::shared_ptr uiScheduler_; + std::shared_ptr serializableWorklet_; +}; + +} // namespace audioworklets diff --git a/packages/react-native-audio-worklets/common/cpp/audioworklets/core/WorkletNode.cpp b/packages/react-native-audio-worklets/common/cpp/audioworklets/core/WorkletNode.cpp new file mode 100644 index 000000000..95c677ab2 --- /dev/null +++ b/packages/react-native-audio-worklets/common/cpp/audioworklets/core/WorkletNode.cpp @@ -0,0 +1,77 @@ +#include +#include + +#include +#include +#include +#include + +namespace audioworklets { + +WorkletNode::WorkletNode( + const std::shared_ptr &context, + UIWorkletsRunner workletRunner) + : audioapi::AudioNode(context), + workletRunner_(std::move(workletRunner)), + busy_(std::make_shared>(false)), + minFramesBetweenDispatch_( + static_cast( + std::max(1.0f, std::floor(context->getSampleRate() / kMaxUiDispatchRateHz)))) { + snapshotBuffers_ = std::make_shared>>( + static_cast(audioapi::MAX_CHANNEL_COUNT)); + for (size_t ch = 0; ch < static_cast(audioapi::MAX_CHANNEL_COUNT); ++ch) { + (*snapshotBuffers_)[ch] = + std::make_shared(audioapi::RENDER_QUANTUM_SIZE); + } +} + +void WorkletNode::processNode(int framesToProcess) { + if (framesToProcess <= 0) { + return; + } + + const size_t frameCount = static_cast(framesToProcess); + const size_t channelCount = audioBuffer_->getNumberOfChannels(); + + if (channelCount == 0) { + return; + } + + framesSinceLastDispatch_ += frameCount; + + if (framesSinceLastDispatch_ < minFramesBetweenDispatch_) { + return; + } + + framesSinceLastDispatch_ = 0; + dispatchToUI(frameCount, channelCount); +} + +void WorkletNode::dispatchToUI(size_t frameCount, size_t channelCount) { + if (!workletRunner_.isValid()) { + return; + } + + const size_t channelsToCopy = + std::min(channelCount, static_cast(audioapi::MAX_CHANNEL_COUNT)); + + // Drop this quantum if the previous UI invocation has not completed yet. + bool expected = false; + if (!busy_->compare_exchange_strong( + expected, true, std::memory_order_acq_rel, std::memory_order_acquire)) { + return; + } + + for (size_t ch = 0; ch < channelsToCopy; ++ch) { + (*snapshotBuffers_)[ch]->copy(*audioBuffer_->getChannel(ch), 0, 0, frameCount); + } + + std::atomic_thread_fence(std::memory_order_release); + + auto busy = busy_; + workletRunner_.invokeOnUI(snapshotBuffers_, channelsToCopy, [busy]() { + busy->store(false, std::memory_order_release); + }); +} + +} // namespace audioworklets diff --git a/packages/react-native-audio-worklets/common/cpp/audioworklets/core/WorkletNode.h b/packages/react-native-audio-worklets/common/cpp/audioworklets/core/WorkletNode.h new file mode 100644 index 000000000..b99496fac --- /dev/null +++ b/packages/react-native-audio-worklets/common/cpp/audioworklets/core/WorkletNode.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace audioworklets { + +/** + * A pass-through analysis node that hands render-quantum audio snapshots to a + * JavaScript worklet on the UI runtime so the UI can be animated from live + * audio (e.g. amplitude/RMS visualizers). + * + * UI dispatches are capped at ~120 Hz (sample-rate aware) so the audio thread + * does not flood the UI scheduler. Audio flows through unchanged. + */ +class WorkletNode : public audioapi::AudioNode { + public: + WorkletNode( + const std::shared_ptr &context, + UIWorkletsRunner workletRunner); + + protected: + void processNode(int framesToProcess) override; + + private: + static constexpr float kMaxUiDispatchRateHz = 120.0f; + + void dispatchToUI(size_t frameCount, size_t channelCount); + + UIWorkletsRunner workletRunner_; + + /// @brief Fixed pool of per-channel UI snapshot buffers (MAX_CHANNEL_COUNT × render quantum). + /// Allocated once in the constructor; never reallocated. Held in a shared_ptr so + /// scheduled UI jobs keep the pool alive without per-quantum vector allocation. + std::shared_ptr>> snapshotBuffers_; + + /// @brief True while a UI-thread worklet invocation is still pending. Prevents + /// the audio thread from flooding the UI scheduler; quanta are dropped instead. + std::shared_ptr> busy_; + + /// @brief Audio-thread accumulator for sample-rate-aware UI dispatch throttling. + size_t framesSinceLastDispatch_{0}; + + /// @brief Minimum audio frames between UI worklet dispatches (~120 Hz at context sample rate). + size_t minFramesBetweenDispatch_{0}; +}; + +} // namespace audioworklets diff --git a/packages/react-native-audio-worklets/common/cpp/clangd/CMakeLists.txt b/packages/react-native-audio-worklets/common/cpp/clangd/CMakeLists.txt new file mode 100644 index 000000000..0ea4a67dd --- /dev/null +++ b/packages/react-native-audio-worklets/common/cpp/clangd/CMakeLists.txt @@ -0,0 +1,44 @@ +# Internal CMake script for generating compile_commands.json for clangd / VS Code C++ tooling. + +cmake_minimum_required(VERSION 3.12.0) +project(rnaudioworklets-clangd LANGUAGES CXX) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_CXX_STANDARD 20) + +set(COMMON_CPP_DIR ${CMAKE_SOURCE_DIR}/..) +set(WORKLETS_PACKAGE_DIR ${CMAKE_SOURCE_DIR}/../../..) +set(REPO_ROOT_DIR ${CMAKE_SOURCE_DIR}/../../../../../) +set(REACT_NATIVE_DIR ${REPO_ROOT_DIR}/node_modules/react-native) +set(WORKLETS_DIR ${REPO_ROOT_DIR}/node_modules/react-native-worklets/Common/cpp) +set(AUDIO_API_COMMON_CPP_DIR ${REPO_ROOT_DIR}/packages/react-native-audio-api/common/cpp) + +set(CODEGEN_JNI_DIR "${WORKLETS_PACKAGE_DIR}/android/build/generated/source/codegen/jni") +set(CODEGEN_IOS_DIR "${REPO_ROOT_DIR}/apps/fabric-example/ios/build/generated/ios/ReactCodegen") + +file(GLOB_RECURSE AUDIO_WORKLETS_SOURCES CONFIGURE_DEPENDS + "${COMMON_CPP_DIR}/audioworklets/*.cpp") + +add_library(rnaudioworklets_cursor STATIC ${AUDIO_WORKLETS_SOURCES}) + +target_compile_definitions(rnaudioworklets_cursor PRIVATE RCT_NEW_ARCH_ENABLED) + +set(FABRIC_EXAMPLE_IOS_DIR ${REPO_ROOT_DIR}/apps/fabric-example/ios) +set(PODS_HEADERS_DIR "${FABRIC_EXAMPLE_IOS_DIR}/Pods/Headers/Public") + +target_include_directories( + rnaudioworklets_cursor + PRIVATE + "${COMMON_CPP_DIR}" + "${AUDIO_API_COMMON_CPP_DIR}" + "${WORKLETS_DIR}" + "${REACT_NATIVE_DIR}/ReactCommon" + "${REACT_NATIVE_DIR}/ReactCommon/jsi" + "${REACT_NATIVE_DIR}/ReactCommon/callinvoker" + "${REACT_NATIVE_DIR}/ReactCommon/react/nativemodule/core" + "${REACT_NATIVE_DIR}/ReactCommon/react/bridging" + "${CODEGEN_JNI_DIR}/react/renderer/components/rnaudioworklets" + "${CODEGEN_IOS_DIR}" + "${PODS_HEADERS_DIR}/ReactNativeDependencies" + "${PODS_HEADERS_DIR}/React-Core" +) diff --git a/packages/react-native-audio-worklets/common/cpp/clangd/SETUP.md b/packages/react-native-audio-worklets/common/cpp/clangd/SETUP.md new file mode 100644 index 000000000..2c513a3e9 --- /dev/null +++ b/packages/react-native-audio-worklets/common/cpp/clangd/SETUP.md @@ -0,0 +1,19 @@ +`CMakeLists.txt` in this directory can be used to build the C++ side and generate `compile_commands.json` for the VS Code / clangd C++ extension. + +**Prerequisites** + +1. Install dependencies from the monorepo root (`yarn install`). +2. Generate TurboModule codegen headers once (either is enough): + - Android: `cd packages/react-native-audio-worklets/android && ./gradlew generateCodegenArtifactsFromSchema` + - iOS: build `apps/fabric-example` once, or run `pod install` in that app +3. For full IntelliSense on TurboModule files (`NativeAudioWorkletsModule.cpp`), run `pod install` in `apps/fabric-example/ios` so Folly/React-Core headers are available under `Pods/Headers/Public`. + +**Generate compile_commands.json** + +From this directory (`common/cpp/clangd`): + +```bash +./generate-and-copy.sh +``` + +This writes `compile_commands.json` to the package root (`packages/react-native-audio-worklets/`), where clangd picks it up when editing files in this package. diff --git a/packages/react-native-audio-worklets/common/cpp/clangd/generate-and-copy.sh b/packages/react-native-audio-worklets/common/cpp/clangd/generate-and-copy.sh new file mode 100755 index 000000000..5ee3288e4 --- /dev/null +++ b/packages/react-native-audio-worklets/common/cpp/clangd/generate-and-copy.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +cmake -B build . +cp build/compile_commands.json ../../../compile_commands.json diff --git a/packages/react-native-audio-worklets/ios/audioworklets/NativeAudioWorkletsModuleProvider.h b/packages/react-native-audio-worklets/ios/audioworklets/NativeAudioWorkletsModuleProvider.h new file mode 100644 index 000000000..ae4c54741 --- /dev/null +++ b/packages/react-native-audio-worklets/ios/audioworklets/NativeAudioWorkletsModuleProvider.h @@ -0,0 +1,10 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NativeAudioWorkletsModuleProvider : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-audio-worklets/ios/audioworklets/NativeAudioWorkletsModuleProvider.mm b/packages/react-native-audio-worklets/ios/audioworklets/NativeAudioWorkletsModuleProvider.mm new file mode 100644 index 000000000..426ea500d --- /dev/null +++ b/packages/react-native-audio-worklets/ios/audioworklets/NativeAudioWorkletsModuleProvider.mm @@ -0,0 +1,15 @@ +#import "NativeAudioWorkletsModuleProvider.h" + +#import +#import +#import + +@implementation NativeAudioWorkletsModuleProvider + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params.jsInvoker); +} + +@end diff --git a/packages/react-native-audio-worklets/package.json b/packages/react-native-audio-worklets/package.json new file mode 100644 index 000000000..0d4f5eb41 --- /dev/null +++ b/packages/react-native-audio-worklets/package.json @@ -0,0 +1,125 @@ +{ + "name": "react-native-audio-worklets", + "version": "1.0.0", + "description": "react-native-worklets integration for react-native-audio-api: WorkletNode for UI-thread audio visualization. Requires React Native New Architecture.", + "source": "src/index", + "main": "lib/module/index", + "module": "lib/module/index", + "react-native": "src/index", + "types": "lib/typescript/index.d.ts", + "files": [ + "src/", + "lib/", + "common/", + "ios/", + "android/", + "CMakeLists.txt", + "RNAudioWorklets.podspec", + "react-native.config.js", + "!**/__tests__", + "!**/__mocks__", + "!**/.*", + "!**/node_modules" + ], + "scripts": { + "test": "jest", + "typecheck": "tsc --noEmit", + "lint": "yarn lint:js && yarn lint:cpp && yarn lint:ios && yarn lint:kotlin", + "lint:js": "eslint src && yarn prettier --check src", + "lint:cpp": "./scripts/cpplint.sh", + "lint:ios": "yarn format:ios -Werror", + "lint:kotlin": "ktlint 'android/src/main/java/**/*.kt'", + "format": "yarn format:js && yarn format:android:kotlin && yarn format:ios && yarn format:common", + "format:check": "yarn format:check:js && yarn format:check:android:kotlin && yarn format:check:ios && yarn format:check:common", + "format:check:js": "prettier --check src", + "format:check:android:kotlin": "ktlint 'android/src/main/java/**/*.kt'", + "format:check:ios": "find ios/audioworklets -type f \\( -iname \"*.h\" -o -iname \"*.m\" -o -iname \"*.mm\" \\) | xargs clang-format --dry-run --Werror", + "format:check:common": "find common/cpp/ \\( -path 'common/cpp/clangd/build' -prune -o -type f \\( -iname \"*.h\" -o -iname \"*.cpp\" -o -iname \"*.hpp\" \\) -print \\) | xargs clang-format --dry-run --Werror", + "format:js": "prettier --write --list-different src", + "format:android:kotlin": "ktlint -F 'android/src/main/java/**/*.kt'", + "format:ios": "find ios/audioworklets -type f \\( -iname \"*.h\" -o -iname \"*.m\" -o -iname \"*.mm\" \\) | xargs clang-format -i", + "format:common": "find common/cpp/ \\( -path 'common/cpp/clangd/build' -prune -o -type f \\( -iname \"*.h\" -o -iname \"*.cpp\" -o -iname \"*.hpp\" \\) -print \\) | xargs clang-format -i", + "build": "yarn workspace react-native-audio-api build && bob build" + }, + "keywords": [ + "react-native", + "audio", + "web audio api", + "worklets", + "audio worklet" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/software-mansion/react-native-audio-api.git" + }, + "author": "Software Mansion (https://github.com/software-mansion)", + "license": "MIT", + "bugs": { + "url": "https://github.com/software-mansion/react-native-audio-api/issues" + }, + "homepage": "https://github.com/software-mansion/react-native-audio-api#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-audio-api": ">= 1.0.0", + "react-native-worklets": ">= 0.10.0" + }, + "dependencies": { + "semver": "^7.7.3" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.3", + "@react-native/babel-preset": "0.85.0", + "@react-native/eslint-config": "0.85.0", + "@react-native/metro-config": "0.85.0", + "@react-native/typescript-config": "0.85.0", + "@types/jest": "^29.5.5", + "@types/react": "^19.2.0", + "@types/semver": "7.7.1", + "eslint": "^8.57.0", + "jest": "^29.7.0", + "prettier": "^3.3.3", + "react": "19.2.3", + "react-native": "0.85.0", + "react-native-audio-api": "workspace:*", + "react-native-builder-bob": "0.33.1", + "react-native-worklets": "0.10.1", + "typescript": "~6.0.3" + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + [ + "module", + { + "esm": true + } + ], + "typescript", + "commonjs" + ] + }, + "codegenConfig": { + "name": "rnaudioworklets", + "type": "modules", + "jsSrcsDir": "src/specs", + "android": { + "javaPackageName": "com.swmansion.audioworklets" + }, + "ios": { + "modulesProvider": { + "NativeAudioWorkletsModule": "NativeAudioWorkletsModuleProvider" + } + } + }, + "create-react-native-library": { + "type": "module-legacy", + "languages": "cpp", + "version": "0.37.1" + } +} diff --git a/packages/react-native-audio-worklets/react-native.config.js b/packages/react-native-audio-worklets/react-native.config.js new file mode 100644 index 000000000..8bc3360b9 --- /dev/null +++ b/packages/react-native-audio-worklets/react-native.config.js @@ -0,0 +1,18 @@ +// Autolinking for Android/iOS cxx module integration. +// Android: `android/build.gradle` runs Codegen; the app's autolinking CMake +// links `react_codegen_rnaudioworklets` + the static `react-native-audio-worklets` +// target from the root `CMakeLists.txt`. iOS: `RNAudioWorklets.podspec` + +// `NativeAudioWorkletsModuleProvider`. +module.exports = { + dependency: { + platforms: { + ios: {}, + android: { + cmakeListsPath: 'build/generated/source/codegen/jni/CMakeLists.txt', + cxxModuleCMakeListsModuleName: 'react-native-audio-worklets', + cxxModuleCMakeListsPath: '../CMakeLists.txt', + cxxModuleHeaderName: 'NativeAudioWorkletsModule', + }, + }, + }, +}; diff --git a/packages/react-native-audio-worklets/scripts/cpplint.sh b/packages/react-native-audio-worklets/scripts/cpplint.sh new file mode 100755 index 000000000..9355a4ace --- /dev/null +++ b/packages/react-native-audio-worklets/scripts/cpplint.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +if which cpplint >/dev/null; then + find common/cpp \( -path 'common/cpp/clangd/build' -prune -o -type d -name build -prune -o \( \( -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) -a -type f \) -print \) | xargs cpplint --linelength=100 --filter=-legal/copyright,-readability/todo,-readability/nolint,-build/namespaces,-build/include_order,-whitespace,-build/c++17,-build/c++20,-runtime/references,-runtime/string,-readability/braces --quiet --recursive "$@" +else + echo "error: cpplint not installed, download from https://github.com/cpplint/cpplint" 1>&2 + exit 1 +fi diff --git a/packages/react-native-audio-worklets/src/AudioWorkletsModule.ts b/packages/react-native-audio-worklets/src/AudioWorkletsModule.ts new file mode 100644 index 000000000..ae68672ce --- /dev/null +++ b/packages/react-native-audio-worklets/src/AudioWorkletsModule.ts @@ -0,0 +1,64 @@ +// Import react-native-audio-api for its side effect: this guarantees the core +// native module has installed its JSI globals (createAudioContext, ...) before +// we install the worklet extensions on top of them. +import 'react-native-audio-api'; +import semverGte from 'semver/functions/gte'; + +import NativeAudioWorkletsModule from './specs/NativeAudioWorkletsModule'; +import type { IWorkletsModule } from './types'; + +const MIN_WORKLETS_VERSION = '0.10.0'; + +class AudioWorkletsModuleImpl { + #workletsModule: IWorkletsModule | null = null; + #workletsVersion = 'unknown'; + + constructor() { + this.#verifyWorklets(); + + if (!NativeAudioWorkletsModule) { + throw new Error( + 'react-native-audio-worklets: native module not found. This package requires React Native ' + + 'New Architecture (TurboModules). Enable newArchEnabled on Android and ' + + 'RCT_NEW_ARCH_ENABLED=1 in your Podfile, then rebuild the native app.' + ); + } + + if (!this.#isInstalled()) { + NativeAudioWorkletsModule.install(); + } + } + + #verifyWorklets(): void { + let workletsPackage: IWorkletsModule; + try { + workletsPackage = require('react-native-worklets'); + this.#workletsVersion = + require('react-native-worklets/package.json').version; + } catch { + throw new Error( + 'react-native-audio-worklets requires react-native-worklets to be installed.' + ); + } + + if (!semverGte(this.#workletsVersion, MIN_WORKLETS_VERSION)) { + throw new Error( + `react-native-audio-worklets requires react-native-worklets >= ${MIN_WORKLETS_VERSION}, ` + + `but ${this.#workletsVersion} is installed.` + ); + } + + this.#workletsModule = workletsPackage; + } + + #isInstalled(): boolean { + return globalThis.__createWorkletNode != null; + } + + get workletsModule(): IWorkletsModule { + return this.#workletsModule!; + } +} + +const AudioWorkletsModule = new AudioWorkletsModuleImpl(); +export default AudioWorkletsModule; diff --git a/packages/react-native-audio-worklets/src/WorkletNode.ts b/packages/react-native-audio-worklets/src/WorkletNode.ts new file mode 100644 index 000000000..c5c6733b8 --- /dev/null +++ b/packages/react-native-audio-worklets/src/WorkletNode.ts @@ -0,0 +1,26 @@ +import { AudioNode, BaseAudioContext } from 'react-native-audio-api'; + +import AudioWorkletsModule from './AudioWorkletsModule'; +import type { WorkletNodeCallback } from './types'; + +export default class WorkletNode extends AudioNode { + constructor(context: BaseAudioContext, callback: WorkletNodeCallback) { + const workletsModule = AudioWorkletsModule.workletsModule; + const shareableWorklet = workletsModule.createSerializable(callback); + + if (globalThis.__createWorkletNode == null) { + throw new Error( + 'react-native-audio-worklets: worklet extensions are not installed.' + ); + } + + const node = globalThis.__createWorkletNode( + context.context, + shareableWorklet, + workletsModule.getUIRuntimeHolder(), + workletsModule.getUISchedulerHolder() + ); + + super(context, node); + } +} diff --git a/packages/react-native-audio-worklets/src/globals.d.ts b/packages/react-native-audio-worklets/src/globals.d.ts new file mode 100644 index 000000000..8155379f0 --- /dev/null +++ b/packages/react-native-audio-worklets/src/globals.d.ts @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +declare global { + // eslint-disable-next-line no-var + var __createWorkletNode: + | (( + audioContext: unknown, + shareableWorklet: unknown, + uiRuntimeHolder: unknown, + uiSchedulerHolder: unknown + ) => any) + | undefined; +} +/* eslint-enable @typescript-eslint/no-explicit-any */ + +export {}; diff --git a/packages/react-native-audio-worklets/src/index.ts b/packages/react-native-audio-worklets/src/index.ts new file mode 100644 index 000000000..ea6a81790 --- /dev/null +++ b/packages/react-native-audio-worklets/src/index.ts @@ -0,0 +1,2 @@ +export { default as WorkletNode } from './WorkletNode'; +export type { WorkletNodeCallback } from './types'; diff --git a/packages/react-native-audio-worklets/src/specs/NativeAudioWorkletsModule.ts b/packages/react-native-audio-worklets/src/specs/NativeAudioWorkletsModule.ts new file mode 100644 index 000000000..a0b13dcce --- /dev/null +++ b/packages/react-native-audio-worklets/src/specs/NativeAudioWorkletsModule.ts @@ -0,0 +1,11 @@ +'use strict'; +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + install(): boolean; +} + +export default TurboModuleRegistry.getEnforcing( + 'NativeAudioWorkletsModule' +); diff --git a/packages/react-native-audio-worklets/src/types.ts b/packages/react-native-audio-worklets/src/types.ts new file mode 100644 index 000000000..d896582c2 --- /dev/null +++ b/packages/react-native-audio-worklets/src/types.ts @@ -0,0 +1,25 @@ +export interface IWorkletsModule { + /** Creates a serializable value. */ + createSerializable: (value: T) => T; + /** Returns the holder object wrapping the shared UI worklet runtime. */ + getUIRuntimeHolder: () => object; + /** Returns the holder object wrapping the shared UI scheduler. */ + getUISchedulerHolder: () => object; +} + +/** + * Invoked on the UI worklet runtime at most ~120 times per second with the + * latest render-quantum snapshot (when the prior callback has finished). + * + * @param audioBuffers - One `ArrayBuffer` per channel of **32-bit float PCM** + * (not interleaved). Wrap with `new Float32Array(buffer)` for zero-copy + * access. + * @param numberOfChannels - Active channel count for this quantum (same as + * `audioBuffers.length`). Use when iterating channels in a loop. + * + * The function must include the `'worklet'` directive. + */ +export type WorkletNodeCallback = ( + audioBuffers: Array, + numberOfChannels: number +) => void; diff --git a/packages/react-native-audio-worklets/tsconfig.json b/packages/react-native-audio-worklets/tsconfig.json new file mode 100644 index 000000000..4bcf5fd5c --- /dev/null +++ b/packages/react-native-audio-worklets/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "removeComments": false, + "moduleSuffixes": ["", ".native", ".web"], + "paths": { + "react-native-audio-worklets": ["./src"] + } + }, + "include": ["src"] +} diff --git a/packages/test-app-screen/src/Test.tsx b/packages/test-app-screen/src/Test.tsx index 0a12d34cd..c00d1fc85 100644 --- a/packages/test-app-screen/src/Test.tsx +++ b/packages/test-app-screen/src/Test.tsx @@ -11,7 +11,6 @@ import { oscillatorTestWithStereoPanner, } from './OscillatorTest'; import { streamerTest } from './StreamingTest'; -import { workletTest } from './WorkletsTest'; import { recorderTest, recorderPlaybackTest } from './RecorderTest'; import { audioBufferFormatsTest, @@ -90,7 +89,7 @@ const Test: FC = () => { }; }, []); - const setupAudioContext = async () => { + const setupAudioContext = () => { if (!audioContextRef.current) { audioContextRef.current = new AudioContext({ sampleRate: SAMPLE_RATE }); } @@ -155,18 +154,6 @@ const Test: FC = () => { }, 5000); }; - const workletsTest = () => { - setIsTesting(true); - setupAudioContext(); - - setTestingInfo('Worklet test that reduces gain to 0.1'); - workletTest(audioContextRef); - setTimeout(() => { - setTestingInfo('Worklet test completed.'); - setIsTesting(false); - }, 4000); - }; - const audioBufferSourceTest = async () => { setupAudioContext(); setIsTesting(true); @@ -257,8 +244,7 @@ const Test: FC = () => { paddingTop: 200, backgroundColor: 'black', height: '100%', - }} - > + }}> {testingInfo} @@ -275,11 +261,6 @@ const Test: FC = () => { />