Skip to content
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
08a1d81
feat: simple worklet callback (ios only)
poneciak57 Aug 22, 2025
3c603f1
fix: fixed worklets usage and dependencies
poneciak57 Aug 22, 2025
389dee3
feat: simple worklet callback (android)
poneciak57 Aug 26, 2025
585729d
chore: formatting and podfile.lock
poneciak57 Aug 26, 2025
e143434
feat: created example showcasing worklets usage
poneciak57 Aug 26, 2025
79946ca
fix: fixed issue with worklets sideeffects not being visible
poneciak57 Aug 27, 2025
da987ca
chore: formatting and package.json deps updt
poneciak57 Aug 27, 2025
45c5833
fix: fixed test and added feature flag
poneciak57 Aug 27, 2025
db5829c
chore: fixed yarn.lock and formatting
poneciak57 Aug 27, 2025
2bcdb29
chore: removed worklet part from recorder example
poneciak57 Aug 27, 2025
1a20444
feat: moved all worklets functionality into runner
poneciak57 Aug 28, 2025
e711fdb
feat: made worklets completly optional (android only)
poneciak57 Aug 28, 2025
e23e0a2
chore: minor changes
poneciak57 Aug 28, 2025
c0e5845
feat: made worklets completly optional (ios too)
poneciak57 Aug 29, 2025
490b2d3
chore: ios imports formatting
poneciak57 Aug 29, 2025
345f480
fix: added error for using worklets callback when worklets unavailable
poneciak57 Aug 29, 2025
11528ba
chore: neatpicks
poneciak57 Aug 29, 2025
470599c
docs: updated docs
poneciak57 Aug 29, 2025
6bf19fa
fix: performed requested changes
poneciak57 Aug 29, 2025
fcb4389
Merge branch 'main' into feat/worklets-support
poneciak57 Aug 29, 2025
4fee078
fix: removed unused kotlin import
poneciak57 Aug 29, 2025
211082c
feat: worklet node implementation
poneciak57 Sep 1, 2025
21dbc75
docs: small fixes
poneciak57 Sep 1, 2025
6c0bba8
fix: fixed tests
poneciak57 Sep 1, 2025
e62fcb7
docs: added documentation for worklet node
poneciak57 Sep 1, 2025
9f6772c
chore: fixed formatting
poneciak57 Sep 1, 2025
4c3c080
chore: updated podfile lock
poneciak57 Sep 1, 2025
fc6203b
Merge branch 'main' into feat/worklets-support
poneciak57 Sep 1, 2025
a755afd
chore: cmake redundancy fix
poneciak57 Sep 4, 2025
1ff0b4c
chore: removed worklets from recorder
poneciak57 Sep 5, 2025
854fdc0
feat: switched to array buffers for faster arguments preparation
poneciak57 Sep 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .yarn/releases/yarn-4.5.0.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -922,4 +922,4 @@ is-windows/index.js:
* Copyright © 2015-2018, Jon Schlinkert.
* Released under the MIT License.
*)
*/
*/
4 changes: 2 additions & 2 deletions apps/common-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
"react-native-audio-api": "workspace:*",
"react-native-background-timer": "2.4.1",
"react-native-gesture-handler": "2.28.0",
"react-native-reanimated": "4.0.2",
"react-native-reanimated": "^4.0.0",
"react-native-safe-area-context": "5.6.0",
"react-native-screens": "4.14.1",
"react-native-svg": "15.12.1",
"react-native-worklets": "0.4.2"
"react-native-worklets": "^0.4.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand Down
200 changes: 200 additions & 0 deletions apps/common-app/src/examples/Worklets/Worklets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { useEffect, useRef } from "react";
import { Text, Button, View, StyleSheet } from 'react-native';
import {
AudioContext,
AudioManager,
AudioRecorder,
RecorderAdapterNode,
WorkletNode
} from 'react-native-audio-api';
import { Container } from "../../components";
import { Extrapolation, useSharedValue } from "react-native-reanimated";
import Animated, {
useAnimatedStyle,
withSpring,
interpolate,
} from "react-native-reanimated";


function Worklets() {
const SAMPLE_RATE = 16000;
const recorderRef = useRef<AudioRecorder | null>(null);
const aCtxRef = useRef<AudioContext | null>(null);
const recorderAdapterRef = useRef<RecorderAdapterNode | null>(null);
const workletNodeRef = useRef<WorkletNode | null>(null);

const bar0 = useSharedValue(0);
const bar1 = useSharedValue(0);
const bar2 = useSharedValue(0); // center bar
const bar3 = useSharedValue(0);
const bar4 = useSharedValue(0);

useEffect(() => {
AudioManager.setAudioSessionOptions({
iosCategory: 'playAndRecord',
iosMode: 'spokenAudio',
iosOptions: ['defaultToSpeaker', 'allowBluetoothA2DP'],
});

AudioManager.requestRecordingPermissions();
recorderRef.current = new AudioRecorder({
sampleRate: SAMPLE_RATE,
bufferLengthInSamples: 1024,
});
}, []);

const start = () => {
if (!recorderRef.current) {
console.error("Recorder is not initialized");
return;
}

const worklet = (audioData: Array<Float32Array>, inputChannelCount: number) => {
'worklet';


// Calculates RMS amplitude
let sum = 0;
for (let i = 0; i < audioData.length; i++) {
sum += audioData[0][i] * audioData[0][i];
}
const rms = Math.sqrt(sum / audioData[0].length);
const scaledAmplitude = Math.min(rms * 500, 1);

console.log(`RMS: ${rms}, Scaled: ${scaledAmplitude}`);

bar0.value = bar1.value;
bar1.value = bar2.value;
bar3.value = bar2.value;
bar4.value = bar3.value;
bar2.value = scaledAmplitude;
};

aCtxRef.current = new AudioContext({ sampleRate: SAMPLE_RATE });
recorderAdapterRef.current = aCtxRef.current.createRecorderAdapter();
workletNodeRef.current = aCtxRef.current.createWorkletNode(worklet, 512, 1);
recorderAdapterRef.current.connect(workletNodeRef.current);
workletNodeRef.current.connect(aCtxRef.current.destination);

recorderRef.current.connect(recorderAdapterRef.current);
recorderRef.current.start();
console.log("Recording started");

if (aCtxRef.current.state === 'suspended') {
aCtxRef.current.resume();
}
}

const stop = () => {
if (!recorderRef.current) {
console.error("Recorder is not initialized");
return;
}
recorderRef.current.stop();
recorderAdapterRef.current = null;
aCtxRef.current = null;
console.log("Recording stopped");
bar0.value = 0;
bar1.value = 0;
bar2.value = 0;
bar3.value = 0;
bar4.value = 0;
}

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

const backgroundColor = interpolate(
amplitude,
[0, 0.5, 1],
[0, 0.5, 1],
Extrapolation.CLAMP
);

const barWidth = 40 - (distanceFromCenter * 5);
const opacity = 1 - (distanceFromCenter * 0.15);

return {
height: withSpring(height, { damping: 20, stiffness: 200 }),
width: barWidth,
backgroundColor: `rgba(${Math.floor(backgroundColor * 255)}, ${Math.floor((1 - backgroundColor) * 255)}, 100, ${opacity})`,
};
});
};

return (
<Container>
<Text style={styles.title}>Audio Worklets Visualizer</Text>
<Text style={styles.subtitle}>Speak into the microphone to see the animation</Text>

<View style={styles.visualizer}>
<View style={styles.barsContainer}>
{Array.from({ length: 5 }, (_, index) => (
<Animated.View
key={index}
style={[styles.bar, createBarStyle(index)]}
/>
))}
</View>
</View>

<Button onPress={start} title="Start Recording" />
<Button onPress={stop} title="Stop Recording" />
</Container>
);
}

const styles = StyleSheet.create({
title: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 10,
textAlign: 'center',
},
subtitle: {
fontSize: 14,
color: '#666',
marginBottom: 30,
textAlign: 'center',
},
visualizer: {
height: 250,
justifyContent: 'flex-end',
alignItems: 'center',
marginVertical: 30,
backgroundColor: '#f0f0f0',
borderRadius: 10,
padding: 20,
},
barsContainer: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'center',
gap: 8,
},
bar: {
borderRadius: 20,
minHeight: 10,
},
});

export default Worklets;
8 changes: 8 additions & 0 deletions apps/common-app/src/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import AudioVisualizer from './AudioVisualizer';
import OfflineRendering from './OfflineRendering';
import Record from './Record/Record';
import PlaybackSpeed from './PlaybackSpeed/PlaybackSpeed';
import Worklets from './Worklets/Worklets';

type NavigationParamList = {
Oscillator: undefined;
Expand All @@ -21,6 +22,7 @@ type NavigationParamList = {
AudioVisualizer: undefined;
OfflineRendering: undefined;
Record: undefined;
Worklets: undefined;
};

export type ExampleKey = keyof NavigationParamList;
Expand Down Expand Up @@ -88,4 +90,10 @@ export const Examples: Example[] = [
subtitle: 'Record audio',
screen: Record,
},
{
key: 'Worklets',
title: 'Worklets',
subtitle: 'Process audio on ui thread with worklet support',
screen: Worklets,
}
] as const;
14 changes: 7 additions & 7 deletions apps/fabric-example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2456,7 +2456,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- RNReanimated (4.0.2):
- RNReanimated (4.0.3):
- boost
- DoubleConversion
- fast_float
Expand All @@ -2483,11 +2483,11 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNReanimated/reanimated (= 4.0.2)
- RNReanimated/reanimated (= 4.0.3)
- RNWorklets
- SocketRocket
- Yoga
- RNReanimated/reanimated (4.0.2):
- RNReanimated/reanimated (4.0.3):
- boost
- DoubleConversion
- fast_float
Expand All @@ -2514,11 +2514,11 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNReanimated/reanimated/apple (= 4.0.2)
- RNReanimated/reanimated/apple (= 4.0.3)
- RNWorklets
- SocketRocket
- Yoga
- RNReanimated/reanimated/apple (4.0.2):
- RNReanimated/reanimated/apple (4.0.3):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -3081,9 +3081,9 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: c91900fa724baee992f01c05eeb4c9e01a807f78
ReactCodegen: 8125d6ee06ea06f48f156cbddec5c2ca576d62e6
ReactCommon: 116d6ee71679243698620d8cd9a9042541e44aa6
RNAudioAPI: 030854e3b6c68bf7ffe0c49cc49a73d3c9e003dc
RNAudioAPI: 2984bc61208c5e254c5fb18074c31c3878567a4d
RNGestureHandler: 3a73f098d74712952870e948b3d9cf7b6cae9961
RNReanimated: b43b4178f9c5c355badd51bc0df933d1eabddc83
RNReanimated: 26a0c8e5bdc05223128507e42c5ed7fb66359cbf
RNScreens: 6ced6ae8a526512a6eef6e28c2286e1fc2d378c3
RNSVG: 6f39605a4c4d200b11435c35bd077553c6b5963a
RNWorklets: f115e8b64e3df5427c68792b3abda90d0f173bb6
Expand Down
20 changes: 20 additions & 0 deletions packages/audiodocs/docs/core/base-audio-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ The above method lets you create [`RecorderAdapterNode`](/docs/sources/recorder-

#### Returns `RecorderAdapterNode`

### `createWorkletNode` <MobileOnly />

The above method lets you create [`WorkletNode`](/docs/worklets/worklet-node).

| Parameters | Type | Description |
| :---: | :---: | :---- |
| `worklet` | `(Array<Float32Array>, number) => void` | The worklet to be executed. |
| `bufferLength` | `number` | The size of the buffer that will be passed to the worklet on each call. |
| `inputChannelCount` | `number` | The number of channels that the node expects as input (it will get min(expected, provided)). |

#### Errors

| Error type | Description |
| :---: | :---- |
| `Error` | `react-native-worklet` is not found as dependency. |
| `NotSupportedError` | `bufferLength` < 1. |
| `NotSupportedError` | `inputChannelCount` is not in range [1, 32]. |

#### Returns `WorkletNode`.

### `createBuffer`

The above method lets you create [`AudioBuffer`](/docs/sources/audio-buffer).
Expand Down
14 changes: 13 additions & 1 deletion packages/audiodocs/docs/inputs/audio-recorder.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,26 @@ The above method disconnects recorder from the currently connected adapter. It d

### `onAudioReady`

The above allows user to set a callback after every portion of data deliverance.
Allows user to set a callback after every portion of data deliverance.

| Parameters | Type | Description |
| :---: | :---: | :---- |
| `callback` | [(OnAudioReadyEventType => void)](/docs/inputs/audio-recorder#onaudioreadyeventtype) | callback that will be invoked |

#### Returns `undefined`.

### `onAudioReadyWorklet`

Allows user to set a worklet callback after every portion of data deliverance. You can read more about how react-native-audio-api supports worklets [here](/docs/worklets/introduction).

| Parameters | Type | Description |
| :---: | :---: | :---- |
| `callback` | (audioData: Float32Array, timestamp: number) => void | worklet callback that will be invoked |

#### Returns `undefined`.

#### Throws `Error` when `react-native-worklets` is not installed as a dependency.

## Remarks

### `AudioRecorderOptions`
Expand Down
Loading