Skip to content

Commit 5a218c6

Browse files
maciejmakowski2003Maciej Makowski
andauthored
Feat/new examples (#593)
* refactor: refactored AudioFile * feat: added new playback speed example * fix: fixed small nitpicks * fix: added SAMPLE_RATE constant * fix: nitpicks --------- Co-authored-by: Maciej Makowski <maciej.makowski2608@gmail.com>
1 parent 4f06516 commit 5a218c6

File tree

12 files changed

+293
-11
lines changed

12 files changed

+293
-11
lines changed

apps/common-app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@react-navigation/stack": "7.3.6",
1313
"@shopify/react-native-skia": "2.1.1",
1414
"react-native-audio-api": "workspace:*",
15+
"react-native-background-timer": "^2.4.1",
1516
"react-native-dotenv": "3.4.11",
1617
"react-native-gesture-handler": "2.26.0",
1718
"react-native-reanimated": "3.18.0",
@@ -29,6 +30,7 @@
2930
"@react-native/typescript-config": "0.80.0",
3031
"@types/jest": "^29.5.13",
3132
"@types/react": "^19.1.0",
33+
"@types/react-native-background-timer": "^2.0.2",
3234
"@types/react-test-renderer": "^19.1.0",
3335
"eslint": "^8.57.0",
3436
"jest": "^29.6.3",

apps/common-app/src/components/Select.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import Spacer from './Spacer';
88
import MenuIcon from './icons/MenuIcon';
99
import CheckCircleIcon from './icons/CheckedCircleIcon';
1010

11-
1211
interface SelectProps<T extends string> {
1312
value: T;
1413
options: T[];
@@ -29,8 +28,7 @@ function Select<T extends string>(props: SelectProps<T>) {
2928
onPress={() => {
3029
onChange(option);
3130
setModalOpen(false);
32-
}}
33-
>
31+
}}>
3432
<View style={styles.optionRow}>
3533
<CheckCircleIcon selected={option === value} color={colors.white} />
3634
<Spacer.Horizontal size={12} />

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AudioManager } from 'react-native-audio-api';
44
import { Container, Button, Spacer } from '../../components';
55
import AudioPlayer from './AudioPlayer';
66
import { colors } from '../../styles';
7+
import BackgroundTimer from 'react-native-background-timer';
78

89
const URL =
910
'https://software-mansion.github.io/react-native-audio-api/audio/voice/example-voice-01.mp3';
@@ -88,6 +89,12 @@ const AudioFile: FC = () => {
8889
if (event.type === 'began') {
8990
await AudioPlayer.pause();
9091
setIsPlaying(false);
92+
} else if (event.type === 'ended' && event.shouldResume) {
93+
BackgroundTimer.setTimeout(async () => {
94+
AudioManager.setAudioSessionActivity(true);
95+
await AudioPlayer.play();
96+
setIsPlaying(true);
97+
}, 1000);
9198
}
9299
}
93100
);

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,7 @@ const DrumMachine: React.FC = () => {
148148
// @ts-ignore ???
149149
width={size}
150150
height={size}
151-
onLayout={onCanvasLayout}
152-
>
151+
onLayout={onCanvasLayout}>
153152
<Grid />
154153
{patterns.map((pattern) => (
155154
<PatternShape key={pattern.instrumentName} pattern={pattern} />
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import React, {
2+
useState,
3+
FC,
4+
useRef,
5+
useEffect,
6+
useCallback,
7+
useMemo,
8+
} from 'react';
9+
import { View, StyleSheet } from 'react-native';
10+
import { Container, Button, Spacer, Slider, Select } from '../../components';
11+
import { AudioContext } from 'react-native-audio-api';
12+
import type { AudioBufferSourceNode } from 'react-native-audio-api';
13+
import {
14+
PCM_DATA,
15+
labelWidth,
16+
PLAYBACK_SPEED_CONFIG,
17+
SAMPLE_RATE,
18+
} from './constants';
19+
import { TimeStretchingAlgorithm, TIME_STRETCHING_OPTIONS } from './types';
20+
import { getAudioSettings } from './helpers';
21+
22+
const PlaybackSpeed: FC = () => {
23+
const [isPlaying, setIsPlaying] = useState(false);
24+
const [isLoading, setIsLoading] = useState(false);
25+
const [playbackSpeed, setPlaybackSpeed] = useState<number>(
26+
PLAYBACK_SPEED_CONFIG.default
27+
);
28+
const [timeStretchingAlgorithm, setTimeStretchingAlgorithm] =
29+
useState<TimeStretchingAlgorithm>('linear');
30+
31+
const aCtxRef = useRef<AudioContext | null>(null);
32+
const sourceRef = useRef<AudioBufferSourceNode | null>(null);
33+
34+
const audioSettings = useMemo(
35+
() => getAudioSettings(timeStretchingAlgorithm),
36+
[timeStretchingAlgorithm]
37+
);
38+
39+
const initializeAudioContext = useCallback(() => {
40+
if (!aCtxRef.current) {
41+
aCtxRef.current = new AudioContext({ sampleRate: SAMPLE_RATE });
42+
}
43+
return aCtxRef.current;
44+
}, []);
45+
46+
const createSource = useCallback(async (): Promise<AudioBufferSourceNode> => {
47+
const audioContext = initializeAudioContext();
48+
49+
setIsLoading(true);
50+
51+
try {
52+
const buffer = await audioContext.decodePCMInBase64Data(
53+
PCM_DATA,
54+
audioSettings.PSOLA ? playbackSpeed : 1
55+
);
56+
57+
const source = audioContext.createBufferSource({
58+
pitchCorrection: audioSettings.PSOLA
59+
? false
60+
: audioSettings.pitchCorrection,
61+
});
62+
63+
source.buffer = buffer;
64+
source.playbackRate.value = audioSettings.PSOLA ? 1 : playbackSpeed;
65+
source.loop = true;
66+
67+
return source;
68+
} catch (error) {
69+
console.error('Failed to create audio source:', error);
70+
throw error;
71+
} finally {
72+
setIsLoading(false);
73+
}
74+
}, [audioSettings, playbackSpeed, initializeAudioContext]);
75+
76+
const stopPlayback = useCallback(() => {
77+
if (sourceRef.current) {
78+
sourceRef.current.onEnded = null;
79+
sourceRef.current.stop();
80+
sourceRef.current = null;
81+
}
82+
setIsPlaying(false);
83+
}, []);
84+
85+
const startPlayback = useCallback(async () => {
86+
try {
87+
const audioContext = initializeAudioContext();
88+
const source = await createSource();
89+
90+
sourceRef.current = source;
91+
92+
sourceRef.current.onEnded = () => {
93+
setIsPlaying(false);
94+
sourceRef.current = null;
95+
};
96+
97+
sourceRef.current.connect(audioContext.destination);
98+
sourceRef.current.start(audioContext.currentTime);
99+
100+
setIsPlaying(true);
101+
} catch (error) {
102+
console.error('Failed to start playback:', error);
103+
setIsPlaying(false);
104+
}
105+
}, [createSource, initializeAudioContext]);
106+
107+
const togglePlayPause = useCallback(async () => {
108+
if (isPlaying) {
109+
stopPlayback();
110+
} else {
111+
await startPlayback();
112+
}
113+
}, [isPlaying, stopPlayback, startPlayback]);
114+
115+
const handlePlaybackSpeedChange = useCallback(
116+
(newSpeed: number) => {
117+
if (audioSettings.PSOLA) {
118+
stopPlayback();
119+
} else {
120+
if (aCtxRef.current && sourceRef.current) {
121+
sourceRef.current.playbackRate.value = newSpeed;
122+
}
123+
}
124+
125+
setPlaybackSpeed(newSpeed);
126+
},
127+
[audioSettings, stopPlayback]
128+
);
129+
130+
const handleTimeStretchingAlgorithmChange = useCallback(
131+
(newMode: TimeStretchingAlgorithm) => {
132+
setTimeStretchingAlgorithm(newMode);
133+
if (isPlaying) {
134+
stopPlayback();
135+
}
136+
},
137+
[isPlaying, stopPlayback]
138+
);
139+
140+
useEffect(() => {
141+
return () => {
142+
stopPlayback();
143+
aCtxRef.current?.close();
144+
aCtxRef.current = null;
145+
};
146+
}, [stopPlayback]);
147+
148+
return (
149+
<Container>
150+
<View style={styles.algorithmSelectContainer}>
151+
<Select
152+
value={timeStretchingAlgorithm}
153+
options={TIME_STRETCHING_OPTIONS}
154+
onChange={handleTimeStretchingAlgorithmChange}
155+
/>
156+
</View>
157+
158+
<View style={styles.controlsContainer}>
159+
<Button
160+
title={isPlaying ? 'Stop' : 'Play'}
161+
onPress={togglePlayPause}
162+
disabled={isLoading}
163+
/>
164+
165+
<Spacer.Vertical size={20} />
166+
167+
<Slider
168+
label="Playback Speed"
169+
value={playbackSpeed}
170+
onValueChange={handlePlaybackSpeedChange}
171+
min={PLAYBACK_SPEED_CONFIG.min}
172+
max={PLAYBACK_SPEED_CONFIG.max}
173+
step={PLAYBACK_SPEED_CONFIG.step}
174+
minLabelWidth={labelWidth}
175+
/>
176+
</View>
177+
</Container>
178+
);
179+
};
180+
181+
const styles = StyleSheet.create({
182+
algorithmSelectContainer: {
183+
paddingTop: 20,
184+
paddingHorizontal: 20,
185+
},
186+
controlsContainer: {
187+
flex: 1,
188+
justifyContent: 'center',
189+
alignItems: 'center',
190+
},
191+
});
192+
193+
export default PlaybackSpeed;

apps/common-app/src/examples/PlaybackSpeed/constants.ts

Lines changed: 24 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { TimeStretchingAlgorithm, AudioSettings } from './types';
2+
3+
export const getAudioSettings = (
4+
algorithm: TimeStretchingAlgorithm
5+
): AudioSettings => {
6+
switch (algorithm) {
7+
case 'linear':
8+
return { pitchCorrection: false, PSOLA: false };
9+
case 'pitchCorrection':
10+
return { pitchCorrection: true, PSOLA: false };
11+
case 'PSOLA':
12+
return { pitchCorrection: false, PSOLA: true };
13+
}
14+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './PlaybackSpeed';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export type TimeStretchingAlgorithm = 'linear' | 'pitchCorrection' | 'PSOLA';
2+
3+
export interface AudioSettings {
4+
pitchCorrection: boolean;
5+
PSOLA: boolean;
6+
}
7+
8+
export const TIME_STRETCHING_OPTIONS: TimeStretchingAlgorithm[] = [
9+
'linear',
10+
'pitchCorrection',
11+
'PSOLA',
12+
] as const;

apps/common-app/src/examples/index.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import AudioFile from './AudioFile';
99
import AudioVisualizer from './AudioVisualizer';
1010
import OfflineRendering from './OfflineRendering';
1111
import Record from './Record/Record';
12+
import PlaybackSpeed from './PlaybackSpeed/PlaybackSpeed';
1213

1314
type NavigationParamList = {
1415
Oscillator: undefined;
@@ -17,6 +18,7 @@ type NavigationParamList = {
1718
Piano: undefined;
1819
TextToSpeech: undefined;
1920
AudioFile: undefined;
21+
PlaybackSpeed: undefined;
2022
AudioVisualizer: undefined;
2123
OfflineRendering: undefined;
2224
Record: undefined;
@@ -45,6 +47,18 @@ export const Examples: Example[] = [
4547
subtitle: 'Play some notes',
4648
screen: Piano,
4749
},
50+
{
51+
key: 'AudioFile',
52+
title: 'Audio File',
53+
subtitle: 'Play an audio file',
54+
screen: AudioFile,
55+
},
56+
{
57+
key: 'PlaybackSpeed',
58+
title: 'Playback Speed',
59+
subtitle: 'Control playback speed of audio',
60+
screen: PlaybackSpeed,
61+
},
4862
{
4963
key: 'TextToSpeech',
5064
title: 'Text to Speech',
@@ -63,12 +77,6 @@ export const Examples: Example[] = [
6377
subtitle: 'Generate sound waves',
6478
screen: Oscillator,
6579
},
66-
{
67-
key: 'AudioFile',
68-
title: 'Audio File',
69-
subtitle: 'Play an audio file',
70-
screen: AudioFile,
71-
},
7280
{
7381
key: 'AudioVisualizer',
7482
title: 'Audio Visualizer',

0 commit comments

Comments
 (0)