Skip to content

Commit 12d1cb8

Browse files
author
maciejmakowski2003
committed
Merge branch 'main' into fix/ios/framework-static-linkage
2 parents f920d73 + a2ff918 commit 12d1cb8

File tree

84 files changed

+2341
-2702
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+2341
-2702
lines changed

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v20.11.1
1+
v22.16.0
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import {
2+
Canvas,
3+
Group,
4+
Path,
5+
Skia,
6+
useCanvasRef,
7+
useCanvasSize,
8+
} from '@shopify/react-native-skia';
9+
import React, { useMemo } from 'react';
10+
import { StyleSheet, View } from 'react-native';
11+
import { AudioBuffer } from 'react-native-audio-api';
12+
import { SharedValue, useDerivedValue } from 'react-native-reanimated';
13+
14+
interface PlaybackVisualizationProps {
15+
buffer: AudioBuffer | null;
16+
currentPositionSeconds: SharedValue<number>;
17+
durationSeconds: number;
18+
}
19+
20+
const PlaybackVisualization: React.FC<PlaybackVisualizationProps> = (props) => {
21+
const { buffer, currentPositionSeconds, durationSeconds } = props;
22+
const canvasRef = useCanvasRef();
23+
const { size } = useCanvasSize(canvasRef);
24+
25+
const bufferWaveform = useMemo(() => {
26+
const height = size.height;
27+
const width = size.width;
28+
29+
if (!buffer || width === 0 || height === 0) {
30+
return [];
31+
}
32+
33+
const channelData = buffer.getChannelData(0);
34+
const barWidth = 2;
35+
const barGap = 2;
36+
37+
const totalBars = Math.floor(width / (barWidth + barGap));
38+
const samplesPerBar = Math.floor(channelData.length / totalBars);
39+
let maxValue = 0;
40+
41+
const waveform = new Array(totalBars).fill(0);
42+
43+
for (let i = 0; i < channelData.length; i++) {
44+
maxValue = Math.max(maxValue, Math.abs(channelData[i]));
45+
}
46+
47+
for (let i = 0; i < totalBars; i++) {
48+
const startSample = i * samplesPerBar;
49+
const endSample = startSample + samplesPerBar;
50+
let max = 0;
51+
52+
for (let j = startSample; j < endSample; j++) {
53+
max = Math.max(max, Math.abs(channelData[j]));
54+
}
55+
56+
const barHeight = (max / maxValue) * height * 0.8;
57+
const x = i * (barWidth + barGap);
58+
const y = (height - barHeight) / 2;
59+
60+
waveform[i] = { x, y, width: barWidth, height: barHeight };
61+
}
62+
63+
return waveform;
64+
}, [buffer, size.height, size.width]);
65+
66+
const bgPath = useMemo(() => {
67+
const path = Skia.Path.Make();
68+
69+
bufferWaveform.forEach((bar) => {
70+
path.moveTo(bar.x, bar.y);
71+
path.lineTo(bar.x, bar.y + bar.height);
72+
});
73+
74+
path.close();
75+
return path;
76+
}, [bufferWaveform]);
77+
78+
const progressPath = useDerivedValue(() => {
79+
const path = Skia.Path.Make();
80+
81+
const totalBars = bufferWaveform.length;
82+
const currentBar = Math.floor(
83+
(currentPositionSeconds.value / durationSeconds) * totalBars
84+
);
85+
86+
for (let i = 0; i < currentBar; i++) {
87+
const bar = bufferWaveform[i];
88+
path.moveTo(bar.x, bar.y);
89+
path.lineTo(bar.x, bar.y + bar.height);
90+
}
91+
92+
path.close();
93+
return path;
94+
}, [bufferWaveform, buffer]);
95+
96+
if (!buffer) {
97+
return null;
98+
}
99+
100+
return (
101+
<View style={styles.container}>
102+
<Canvas ref={canvasRef} style={styles.canvas}>
103+
<Group>
104+
<Path
105+
path={bgPath}
106+
color="rgba(255, 255, 255, 0.2)"
107+
style="stroke"
108+
strokeWidth={2}
109+
strokeCap="round"
110+
/>
111+
<Path
112+
path={progressPath}
113+
color="#00a9f0"
114+
style="stroke"
115+
strokeWidth={2}
116+
strokeCap="round"
117+
/>
118+
</Group>
119+
</Canvas>
120+
</View>
121+
);
122+
};
123+
124+
export default PlaybackVisualization;
125+
126+
const styles = StyleSheet.create({
127+
container: {
128+
flex: 1,
129+
maxHeight: 250,
130+
width: '100%',
131+
backgroundColor: 'rgba(0, 0, 0, 0.15)',
132+
flexDirection: 'column',
133+
},
134+
canvas: {
135+
flex: 1,
136+
},
137+
});

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

Lines changed: 111 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import React, { FC, useCallback, useEffect, useState } from 'react';
2-
import { AudioManager } from 'react-native-audio-api';
2+
import {
3+
AudioBuffer,
4+
AudioManager,
5+
RecordingNotificationManager,
6+
} from 'react-native-audio-api';
37

48
import { Alert, StyleSheet, View } from 'react-native';
59
import { Container } from '../../components';
610

7-
import { audioRecorder as Recorder } from '../../singletons';
11+
import { Easing, useSharedValue, withTiming } from 'react-native-reanimated';
12+
import { audioRecorder as Recorder, audioContext } from '../../singletons';
813
import ControlPanel from './ControlPanel';
14+
import PlaybackVisualization from './PlaybackVisualization';
915
import RecordingTime from './RecordingTime';
1016
import RecordingVisualization from './RecordingVisualization';
1117
import Status from './Status';
@@ -20,6 +26,28 @@ AudioManager.setAudioSessionOptions({
2026
const Record: FC = () => {
2127
const [state, setState] = useState<RecordingState>(RecordingState.Idle);
2228
const [hasPermissions, setHasPermissions] = useState<boolean>(false);
29+
const [recordedBuffer, setRecordedBuffer] = useState<AudioBuffer | null>(
30+
null
31+
);
32+
const currentPositionSV = useSharedValue(0);
33+
34+
const updateNotification = (paused: boolean) => {
35+
RecordingNotificationManager.show({
36+
paused,
37+
});
38+
};
39+
40+
const setupNotification = (paused: boolean) => {
41+
RecordingNotificationManager.show({
42+
title: 'Recording Demo',
43+
contentText: paused ? 'Paused recording' : 'Recording...',
44+
paused,
45+
smallIconResourceName: 'logo',
46+
pauseIconResourceName: 'pause',
47+
resumeIconResourceName: 'resume',
48+
color: 0xff6200,
49+
});
50+
}
2351

2452
const onStartRecording = useCallback(async () => {
2553
if (state !== RecordingState.Idle) {
@@ -47,6 +75,7 @@ const Record: FC = () => {
4775
}
4876

4977
const result = Recorder.start();
78+
setupNotification(false);
5079

5180
if (result.status === 'success') {
5281
console.log('Recording started, file path:', result.path);
@@ -61,26 +90,61 @@ const Record: FC = () => {
6190

6291
const onPauseRecording = useCallback(() => {
6392
Recorder.pause();
93+
updateNotification(true);
6494
setState(RecordingState.Paused);
6595
}, []);
6696

6797
const onResumeRecording = useCallback(() => {
6898
Recorder.resume();
99+
updateNotification(false);
69100
setState(RecordingState.Recording);
70101
}, []);
71102

72-
const onStopRecording = useCallback(() => {
73-
Recorder.stop();
103+
const onStopRecording = useCallback(async () => {
104+
const info = Recorder.stop();
105+
RecordingNotificationManager.hide();
74106
setState(RecordingState.ReadyToPlay);
107+
108+
if (info.status !== 'success') {
109+
Alert.alert('Error', `Failed to stop recording: ${info.message}`);
110+
setRecordedBuffer(null);
111+
return;
112+
}
113+
114+
const audioBuffer = await audioContext.decodeAudioData(info.path);
115+
setRecordedBuffer(audioBuffer);
75116
}, []);
76117

77118
const onPlayRecording = useCallback(() => {
78119
if (state !== RecordingState.ReadyToPlay) {
79120
return;
80121
}
81122

123+
if (!recordedBuffer) {
124+
Alert.alert('Error', 'No recorded audio to play.');
125+
return;
126+
}
127+
128+
const source = audioContext.createBufferSource();
129+
source.buffer = recordedBuffer;
130+
source.connect(audioContext.destination);
131+
source.start(audioContext.currentTime + 0.1);
132+
133+
source.onEnded = () => {
134+
setState(RecordingState.Idle);
135+
};
136+
137+
setTimeout(() => {
138+
currentPositionSV.value = 0;
139+
140+
withTiming(recordedBuffer.duration, {
141+
duration: recordedBuffer.duration * 1000,
142+
easing: Easing.linear,
143+
});
144+
}, 100);
145+
82146
setState(RecordingState.Playing);
83-
}, [state]);
147+
}, [state, recordedBuffer, currentPositionSV]);
84148

85149
const onToggleState = useCallback(
86150
(action: RecordingState) => {
@@ -139,21 +203,60 @@ const Record: FC = () => {
139203
})();
140204
}, []);
141205

206+
useEffect(() => {
207+
const pauseListener = RecordingNotificationManager.addEventListener(
208+
'recordingNotificationPause',
209+
() => {
210+
console.log('Notification pause action received');
211+
onPauseRecording();
212+
}
213+
);
214+
215+
const resumeListener = RecordingNotificationManager.addEventListener(
216+
'recordingNotificationResume',
217+
() => {
218+
console.log('Notification resume action received');
219+
onResumeRecording();
220+
}
221+
);
222+
223+
return () => {
224+
pauseListener.remove();
225+
resumeListener.remove();
226+
RecordingNotificationManager.hide();
227+
};
228+
}, [onPauseRecording, onResumeRecording]);
229+
142230
useEffect(() => {
143231
Recorder.enableFileOutput();
144232

145233
return () => {
146234
Recorder.disableFileOutput();
235+
Recorder.stop();
236+
AudioManager.setAudioSessionActivity(false);
237+
RecordingNotificationManager.hide();
147238
};
148239
}, []);
149240

150241
return (
151242
<Container disablePadding>
152243
<Status state={state} />
153244
<View style={styles.spacerM} />
154-
<RecordingTime state={state} />
155-
<View style={styles.spacerS} />
156-
<RecordingVisualization state={state} />
245+
{[RecordingState.Playing, RecordingState.ReadyToPlay].includes(state) ? (
246+
<>
247+
<PlaybackVisualization
248+
buffer={recordedBuffer}
249+
currentPositionSeconds={currentPositionSV}
250+
durationSeconds={recordedBuffer?.duration || 0}
251+
/>
252+
</>
253+
) : (
254+
<>
255+
<RecordingTime state={state} />
256+
<View style={styles.spacerS} />
257+
<RecordingVisualization state={state} />
258+
</>
259+
)}
157260
<View style={styles.spacerM} />
158261
<ControlPanel state={state} onToggleState={onToggleState} />
159262
</Container>

0 commit comments

Comments
 (0)