Skip to content

Commit 913449b

Browse files
poneciak57Kacper Ponetamaciejmakowski2003
authored
Feat/recorder adapter node (#569)
* feat: implemented thread safe circular buffer * feat: implemented recorder_adapter_node implemented RecorderAdapterNode: - hostObject - typescript interface - c++ classes modified AndroidRecorder (currently commented out) implemented method for creating node on BaseAudioContext modified AudioRecorder so it works with the adapter well modified example app so it can showcase how it works * fix: fixed some errors - set isInitialized on adapter node when it is ready - added copying data to all channels in adapter node processing - removed debuging logs * chore: some changes request by msluszniak - added noexcept to constructors - added std:: namespace prefix to memory ops - marked properties as mutable in CircularOverflowableAudioArray and made read const - cleaned Recorder example from commented out code * fix: changed adapter node to extend audio node - changed AdapterNode to extend AudioNode instead of AudioScheduledSourceNode - Added showcase of how u can stop recorder and adapter will play silence in the recorder example * feat: added adapter buffer write in ios recorder now IOSAudioRecorder also writes to the adapter circular buffer but currently there is a problem that it seems to not work correctly on ios. The played data is somehow corrupted and has "robotic" feel also the previous example recorder app does not work on ios * chore: conducted tests and cleaned minor things * feat: aligned adapter node usage to other nodes changed on how adapter node is connected to the recorder moved adapter buffer to adapter node * fix: fixed bug with ios misconfiguration there was a weird bug that on ios devices sample rates kept being misconfigured. It was caused by allowBluetooth mainly but took a lot of time to find. * docs: updated documentation * fix: aplied requested changes - updated recorder example by combining both example - fixed misleading note in docs - cleaned up some comments and logs - tested if everything works fine on various sample rates - fixed example usage code in docs to not use refs * Update packages/audiodocs/docs/inputs/audio-recorder.mdx Co-authored-by: Maciej Makowski <[email protected]> * fix: aplied requested changes - fixed adapter node docs - fixed access modifiers for recorder and adapter node in ts * fix: fixed possible thread safety related issues - added disconnect method for recorder - added lock for adapter node access in recorder now when we change recorders adapter nothing bad will happen - added init checking for adapter so it is checked in typescript layer - updated docs * fix: added disconnect method on ts layer - added disconnect method in typescript layer - fixed namming in adapter node --------- Co-authored-by: Kacper Poneta <[email protected]> Co-authored-by: Maciej Makowski <[email protected]>
1 parent 4fc09d1 commit 913449b

File tree

24 files changed

+501
-44
lines changed

24 files changed

+501
-44
lines changed

apps/common-app/src/examples/Record/Record.tsx

Lines changed: 88 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,102 @@
11
import React, { useRef, FC, useEffect } from 'react';
22
import {
3-
AudioBuffer,
43
AudioContext,
54
AudioManager,
65
AudioRecorder,
6+
RecorderAdapterNode,
77
AudioBufferSourceNode,
8+
AudioBuffer
89
} from 'react-native-audio-api';
910

1011
import { Container, Button } from '../../components';
12+
import { View, Text } from 'react-native';
13+
import { colors } from '../../styles';
14+
15+
const SAMPLE_RATE = 16000;
1116

1217
const Record: FC = () => {
1318
const recorderRef = useRef<AudioRecorder | null>(null);
19+
const aCtxRef = useRef<AudioContext | null>(null);
20+
const recorderAdapterRef = useRef<RecorderAdapterNode | null>(null);
1421
const audioBuffersRef = useRef<AudioBuffer[]>([]);
1522
const sourcesRef = useRef<AudioBufferSourceNode[]>([]);
16-
const aCtxRef = useRef<AudioContext | null>(null);
23+
1724

1825
useEffect(() => {
1926
AudioManager.setAudioSessionOptions({
2027
iosCategory: 'playAndRecord',
2128
iosMode: 'spokenAudio',
22-
iosOptions: ['allowBluetooth', 'defaultToSpeaker'],
29+
iosOptions: ['defaultToSpeaker', 'allowBluetoothA2DP'],
2330
});
24-
31+
2532
recorderRef.current = new AudioRecorder({
26-
sampleRate: 16000,
27-
bufferLengthInSamples: 16000,
33+
sampleRate: SAMPLE_RATE,
34+
bufferLengthInSamples: SAMPLE_RATE,
2835
});
2936
}, []);
3037

31-
const onReplay = () => {
32-
const aCtx = new AudioContext({ sampleRate: 16000 });
38+
39+
const startEcho = () => {
40+
if (!recorderRef.current) {
41+
console.error('AudioContext or AudioRecorder is not initialized');
42+
return;
43+
}
44+
45+
aCtxRef.current = new AudioContext({ sampleRate: SAMPLE_RATE });
46+
recorderAdapterRef.current = aCtxRef.current.createRecorderAdapter();
47+
recorderAdapterRef.current.connect(aCtxRef.current.destination);
48+
recorderRef.current.connect(recorderAdapterRef.current);
49+
50+
recorderRef.current.start();
51+
console.log('Recording started');
52+
console.log('Audio context state:', aCtxRef.current.state);
53+
if (aCtxRef.current.state === 'suspended') {
54+
console.log('Resuming audio context');
55+
aCtxRef.current.resume();
56+
}
57+
}
58+
59+
/// This stops only the recording, not the audio context
60+
const stopEcho = () => {
61+
if (!recorderRef.current) {
62+
console.error('AudioRecorder is not initialized');
63+
return;
64+
}
65+
recorderRef.current.stop();
66+
aCtxRef.current = null;
67+
recorderAdapterRef.current = null;
68+
console.log('Recording stopped');
69+
}
70+
71+
const startRecordReplay = () => {
72+
if (!recorderRef.current) {
73+
console.error('AudioRecorder is not initialized');
74+
return;
75+
}
76+
77+
recorderRef.current.onAudioReady((event) => {
78+
const { buffer, numFrames, when } = event;
79+
80+
console.log(
81+
'Audio recorder buffer ready:',
82+
buffer.duration,
83+
numFrames,
84+
when
85+
);
86+
audioBuffersRef.current.push(buffer);
87+
});
88+
89+
recorderRef.current.start();
90+
91+
setTimeout(() => {
92+
recorderRef.current?.stop();
93+
console.log('Recording stopped');
94+
}, 5000);
95+
96+
}
97+
98+
const stopRecordReplay = () => {
99+
const aCtx = new AudioContext({ sampleRate: SAMPLE_RATE });
33100
aCtxRef.current = aCtx;
34101

35102
if (aCtx.state === 'suspended') {
@@ -47,9 +114,6 @@ const Record: FC = () => {
47114
source.buffer = buffers[i];
48115

49116
source.connect(aCtx.destination);
50-
source.onended = () => {
51-
console.log('Audio buffer source ended');
52-
};
53117
sourcesRef.current.push(source);
54118

55119
source.start(nextStartAt);
@@ -64,36 +128,22 @@ const Record: FC = () => {
64128
},
65129
(nextStartAt - tNow) * 1000
66130
);
67-
};
68131

69-
const onRecord = () => {
70-
if (!recorderRef.current) {
71-
return;
72-
}
73-
74-
recorderRef.current.onAudioReady((event) => {
75-
const { buffer, numFrames, when } = event;
76-
77-
console.log(
78-
'Audio recorder buffer ready:',
79-
buffer.duration,
80-
numFrames,
81-
when
82-
);
83-
audioBuffersRef.current.push(buffer);
84-
});
85-
86-
recorderRef.current.start();
87-
88-
setTimeout(() => {
89-
recorderRef.current?.stop();
90-
}, 3000);
91-
};
132+
}
92133

93134
return (
94-
<Container centered>
95-
<Button title="Record" onPress={onRecord} />
96-
<Button title="Replay" onPress={onReplay} />
135+
<Container style={{ gap: 40 }}>
136+
<Text style={{ color: colors.white, fontSize: 24, textAlign: 'center' }}>Sample rate: {SAMPLE_RATE}</Text>
137+
<View style={{ alignItems: 'center', justifyContent: 'center', gap: 5 }}>
138+
<Text style={{ color: colors.white, fontSize: 24 }}>Echo example</Text>
139+
<Button title="Start Recording" onPress={startEcho} />
140+
<Button title="Stop Recording" onPress={stopEcho} />
141+
</View>
142+
<View style={{ alignItems: 'center', justifyContent: 'center', gap: 5 }}>
143+
<Text style={{ color: colors.white, fontSize: 24 }}>Record & replay example</Text>
144+
<Button title="Record for Replay" onPress={startRecordReplay} />
145+
<Button title="Replay" onPress={stopRecordReplay} />
146+
</View>
97147
</Container>
98148
);
99149
};

packages/audiodocs/docs/core/base-audio-context.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ The above method lets you create [`AnalyserNode`](/analysis/analyser-node).
5252

5353
#### Returns `AnalyserNode`.
5454

55+
### `createRecorderAdapter`
56+
57+
The above method lets you create [`RecorderAdapterNode`](/sources/recorder-adapter-node).
58+
59+
#### Returns `RecorderAdapterNode`
60+
5561
### `createBiquadFilter`
5662

5763
The above method lets you create `BiquadFilterNode`.

packages/audiodocs/docs/inputs/audio-recorder.mdx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@ The above stops recording.
5151

5252
#### Returns `undefined`.
5353

54+
### `connect`
55+
56+
The above method connects recorder to the adapter. If the recorder is already connected it will change its adapter (it is equivalent of calling disconnect() previously).
57+
Be carefull `RecorderAdapterNode` can be connected only once. You can't connect same adapter twice (even to different recorders).
58+
59+
| Parameters | Type | Description |
60+
| :---: | :---: | :---- |
61+
| `node` | [RecorderAdapterNode](/sources/recorder-adapter-node) | Adapter that we want to connect to. |
62+
63+
#### Returns `undefined`
64+
65+
### `disconnect`
66+
67+
The above method disconnects recorder from the currently connected adapter. It does not need to be called before connect.
5468

5569
### `onAudioReady`
5670

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
sidebar_position: 6
3+
---
4+
5+
import AudioNodePropsTable from "@site/src/components/AudioNodePropsTable"
6+
import { Optional, ReadOnly } from '@site/src/components/Badges';
7+
8+
# RecorderAdapterNode
9+
10+
The `RecorderAdapterNode` is an [`AudioNode`](/core/audio-node) which is an adapter for [`AudioRecorder`](/inputs/audio-recorder).
11+
It lets you compose audio input from recorder into an audio graph.
12+
13+
## Constructor
14+
15+
[`BaseAudioContext.createRecorderAdapter()`](/core/base-audio-context#createrecorderadapter)
16+
17+
## Example
18+
19+
```tsx
20+
const recorder = new AudioRecorder({
21+
sampleRate: 48000,
22+
bufferLengthInSamples: 48000,
23+
});
24+
const audioContext = new AudioContext({ sampleRate: 48000 });
25+
const recorderAdapterNode = aCtxRef.current.createRecorderAdapter();
26+
27+
recorder.connect(recorderAdapterNode);
28+
recorderAdapterNode.connect(audioContext.destination)
29+
```
30+
31+
## Remarks
32+
- Adapter without the recorder will produce silence
33+
- Adapter that is connected only to the recorder will work fine and keep small buffer of recorded Data
34+
- Adapter node will not be garbage collected until he is connected to either some destination or recorder

packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#include <audioapi/android/core/AndroidAudioRecorder.h>
22
#include <audioapi/core/Constants.h>
3+
#include <audioapi/core/sources/RecorderAdapterNode.h>
34
#include <audioapi/events/AudioEventHandlerRegistry.h>
45
#include <audioapi/utils/AudioArray.h>
56
#include <audioapi/utils/AudioBus.h>
67
#include <audioapi/utils/CircularAudioArray.h>
8+
#include <audioapi/utils/CircularOverflowableAudioArray.h>
79

810
namespace audioapi {
911

@@ -65,7 +67,7 @@ DataCallbackResult AndroidAudioRecorder::onAudioReady(
6567
int32_t numFrames) {
6668
if (isRunning_.load()) {
6769
auto *inputChannel = static_cast<float *>(audioData);
68-
circularBuffer_->push_back(inputChannel, numFrames);
70+
writeToBuffers(inputChannel, numFrames);
6971
}
7072

7173
while (circularBuffer_->getNumberOfAvailableFrames() >= bufferLength_) {

packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioRecorderHostObject.h

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <audioapi/core/sources/AudioBuffer.h>
66
#include <audioapi/HostObjects/AudioBufferHostObject.h>
77
#include <audioapi/core/inputs/AudioRecorder.h>
8+
#include <audioapi/HostObjects/RecorderAdapterNodeHostObject.h>
89

910
#ifdef ANDROID
1011
#include <audioapi/android/core/AndroidAudioRecorder.h>
@@ -45,7 +46,22 @@ class AudioRecorderHostObject : public JsiHostObject {
4546

4647
addFunctions(
4748
JSI_EXPORT_FUNCTION(AudioRecorderHostObject, start),
48-
JSI_EXPORT_FUNCTION(AudioRecorderHostObject, stop));
49+
JSI_EXPORT_FUNCTION(AudioRecorderHostObject, stop),
50+
JSI_EXPORT_FUNCTION(AudioRecorderHostObject, connect),
51+
JSI_EXPORT_FUNCTION(AudioRecorderHostObject, disconnect)
52+
);
53+
}
54+
55+
JSI_HOST_FUNCTION(connect) {
56+
auto adapterNodeHostObject = args[0].getObject(runtime).getHostObject<RecorderAdapterNodeHostObject>(runtime);
57+
audioRecorder_->connect(
58+
std::static_pointer_cast<RecorderAdapterNode>(adapterNodeHostObject->node_));
59+
return jsi::Value::undefined();
60+
}
61+
62+
JSI_HOST_FUNCTION(disconnect) {
63+
audioRecorder_->disconnect();
64+
return jsi::Value::undefined();
4965
}
5066

5167
JSI_HOST_FUNCTION(start) {

packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <audioapi/HostObjects/PeriodicWaveHostObject.h>
1515
#include <audioapi/HostObjects/StereoPannerNodeHostObject.h>
1616
#include <audioapi/HostObjects/AnalyserNodeHostObject.h>
17+
#include <audioapi/HostObjects/RecorderAdapterNodeHostObject.h>
1718

1819
#include <jsi/jsi.h>
1920
#include <memory>
@@ -40,6 +41,7 @@ class BaseAudioContextHostObject : public JsiHostObject {
4041
JSI_EXPORT_PROPERTY_GETTER(BaseAudioContextHostObject, currentTime));
4142

4243
addFunctions(
44+
JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createRecorderAdapter),
4345
JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createOscillator),
4446
JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createCustomProcessor),
4547
JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createGain),
@@ -73,6 +75,12 @@ class BaseAudioContextHostObject : public JsiHostObject {
7375
return {context_->getCurrentTime()};
7476
}
7577

78+
JSI_HOST_FUNCTION(createRecorderAdapter) {
79+
auto recorderAdapter = context_->createRecorderAdapter();
80+
auto recorderAdapterHostObject = std::make_shared<RecorderAdapterNodeHostObject>(recorderAdapter);
81+
return jsi::Object::createFromHostObject(runtime, recorderAdapterHostObject);
82+
}
83+
7684
JSI_HOST_FUNCTION(createOscillator) {
7785
auto oscillator = context_->createOscillator();
7886
auto oscillatorHostObject =
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#pragma once
2+
3+
#include <audioapi/core/sources/RecorderAdapterNode.h>
4+
#include <audioapi/HostObjects/AudioNodeHostObject.h>
5+
6+
#include <memory>
7+
#include <string>
8+
#include <vector>
9+
10+
namespace audioapi {
11+
using namespace facebook;
12+
13+
class AudioRecorderHostObject;
14+
15+
class RecorderAdapterNodeHostObject : public AudioNodeHostObject {
16+
public:
17+
explicit RecorderAdapterNodeHostObject(
18+
const std::shared_ptr<RecorderAdapterNode> &node)
19+
: AudioNodeHostObject(node) {
20+
}
21+
22+
private:
23+
friend class AudioRecorderHostObject;
24+
};
25+
26+
} // namespace audioapi

packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <audioapi/core/sources/AudioBufferQueueSourceNode.h>
1010
#include <audioapi/core/sources/AudioBufferSourceNode.h>
1111
#include <audioapi/core/sources/OscillatorNode.h>
12+
#include <audioapi/core/sources/RecorderAdapterNode.h>
1213
#include <audioapi/core/utils/AudioDecoder.h>
1314
#include <audioapi/core/utils/AudioNodeManager.h>
1415
#include <audioapi/events/AudioEventHandlerRegistry.h>
@@ -49,6 +50,12 @@ std::shared_ptr<AudioDestinationNode> BaseAudioContext::getDestination() {
4950
return destination_;
5051
}
5152

53+
std::shared_ptr<RecorderAdapterNode> BaseAudioContext::createRecorderAdapter() {
54+
auto recorderAdapter = std::make_shared<RecorderAdapterNode>(this);
55+
nodeManager_->addProcessingNode(recorderAdapter);
56+
return recorderAdapter;
57+
}
58+
5259
std::shared_ptr<OscillatorNode> BaseAudioContext::createOscillator() {
5360
auto oscillator = std::make_shared<OscillatorNode>(this);
5461
nodeManager_->addSourceNode(oscillator);

packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <audioapi/core/types/ContextState.h>
44
#include <audioapi/core/types/OscillatorType.h>
55

6+
67
#include <functional>
78
#include <memory>
89
#include <string>
@@ -30,6 +31,7 @@ class AudioDecoder;
3031
class AnalyserNode;
3132
class AudioEventHandlerRegistry;
3233
class IAudioEventHandlerRegistry;
34+
class RecorderAdapterNode;
3335

3436
class BaseAudioContext {
3537
public:
@@ -42,6 +44,7 @@ class BaseAudioContext {
4244
[[nodiscard]] std::size_t getCurrentSampleFrame() const;
4345
std::shared_ptr<AudioDestinationNode> getDestination();
4446

47+
std::shared_ptr<RecorderAdapterNode> createRecorderAdapter();
4548
std::shared_ptr<OscillatorNode> createOscillator();
4649
std::shared_ptr<CustomProcessorNode> createCustomProcessor(const std::string& identifier);
4750
std::shared_ptr<GainNode> createGain();

0 commit comments

Comments
 (0)