Skip to content

Commit fde5da6

Browse files
maciejmakowski2003Maciej MakowskiMaciej Makowskimichalsek
authored
Feat/analyser node (#242)
* feat: added ts interface for AnalyserNode * refactor: refactored AudioNode * refactor: moved Constants.h to core * feat: added AnalyserNode core class * feat: added HostObject for AnalyserNode * feat: added host functions caching * refactor: removed unused components and moved common files to common example folders and format * fix: fixed metronome, removed unused files and moved common files * fix: back to previous version of hihat * fix: fixed start/stop mechanism bug * feat: added analyser constants to Constants.h * fix: fixed The global process.env.EXPO_OS is not defined warning in fabric-example * feat: added createAnalyser to is interfaces * refactor: refactored AnalyserNode creation and itself * feat: implemented getFloatTimeDomainData and getByteTimeDomainData * fix: fixed Analyser HostObject exported functions * fix: prettier fixes and fixed AnalyserNode props modifiability * feat: added AudioVisualizer example for testing and fixed eslint config * fix: fixed AnalyserNode writing to inputBuffer * feat: added ts error handling * feat: added utils for float to uint8 conversion * feat: added linear to decibels conversion * feat: implemented AnalyserNode * feat: implemented doFFT in FFTFrame * ci: yarn format and small fixes * refactor: refactored FFTFrame, added setups caching * fix: fixed variables names in FFTFrame * docs: updated Web Audio API coverage * feat: added very first version of AudioVisualizer example * fix: small prettier and yarn format fixes * refactor: refactored example * fix: small fixes * feat: added comments to AnalyserNode impl * fix: fixed frequencyBinCount prop * fix: removed log * feat: boost AudioVisualizer * fix: fixed TimeChartLine size * refactor: added removing file:// in decodeAudioSource method (#268) Co-authored-by: Maciej Makowski <maciej.makowski2608@gmail.com> * feat: some changes to the audio visualizer example (#270) * feat: some changes to the audio visualizer example * Update apps/common-app/src/examples/AudioVisualizer/Charts.tsx * refactor: refactored AudioVisualizer example * fix: fixed error handling * refactor: back to useState --------- Co-authored-by: Maciej Makowski <maciej.makowski@swmansion.com> Co-authored-by: Maciej Makowski <maciej.makowski2608@gmail.com> Co-authored-by: Michał Sęk <michal.sek@swmansion.com>
1 parent de2de66 commit fde5da6

Some content is hidden

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

43 files changed

+1223
-98
lines changed

.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ module.exports = {
1717
rules: {
1818
'@typescript-eslint/no-unsafe-call': 'off',
1919
'@typescript-eslint/no-unsafe-member-access': 'off',
20+
'@typescript-eslint/no-floating-promises': 'off',
21+
'@typescript-eslint/no-misused-promises': 'off',
2022
'@typescript-eslint/no-unsafe-return': 'off',
2123
'@typescript-eslint/no-unsafe-assignment': 'off',
2224
'@typescript-eslint/no-unsafe-argument': 'off',

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@ import { colors } from '../styles';
88
type ContainerProps = PropsWithChildren<{
99
style?: StyleProp<ViewStyle>;
1010
centered?: boolean;
11+
disablePadding?: boolean;
1112
}>;
1213

1314
const headerPadding = 120; // eyeballed
1415

1516
const Container: React.FC<ContainerProps> = (props) => {
16-
const { children, style, centered } = props;
17+
const { children, style, centered, disablePadding } = props;
1718

1819
return (
1920
<SafeAreaView
2021
edges={['bottom', 'left', 'right']}
21-
style={[styles.basic, centered && styles.centered, style]}
22-
>
22+
style={[styles.basic, centered && styles.centered, !disablePadding && styles.padding, style]}>
2323
<BGGradient />
2424
{children}
2525
</SafeAreaView>
@@ -31,10 +31,12 @@ export default Container;
3131
const styles = StyleSheet.create({
3232
basic: {
3333
flex: 1,
34-
padding: 24,
3534
paddingTop: headerPadding,
3635
backgroundColor: colors.background,
3736
},
37+
padding: {
38+
padding: 24,
39+
},
3840
centered: {
3941
alignItems: 'center',
4042
justifyContent: 'center',

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,7 @@ const Slider: React.FC<SliderProps> = (props) => {
134134
style={[
135135
styles.label,
136136
!!minLabelWidth && { minWidth: minLabelWidth },
137-
]}
138-
>
137+
]}>
139138
{label}
140139
</Text>
141140
<Spacer.Horizontal size={12} />

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import { ActivityIndicator } from 'react-native';
1212
const AudioFile: FC = () => {
1313
const [isPlaying, setIsPlaying] = useState(false);
1414
const [isLoading, setIsLoading] = useState(false);
15+
const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);
1516

1617
const audioContextRef = useRef<AudioContext | null>(null);
1718
const audioBufferSourceNodeRef = useRef<AudioBufferSourceNode | null>(null);
18-
const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);
1919

2020
const setup = () => {
2121
if (!audioContextRef.current) {
@@ -42,7 +42,7 @@ const AudioFile: FC = () => {
4242
setIsPlaying(false);
4343

4444
setIsLoading(true);
45-
await fetchAudioBuffer(result.assets[0].uri.replace('file://', ''));
45+
await fetchAudioBuffer(result.assets[0].uri);
4646
setIsLoading(false);
4747
}
4848
} catch (error) {
@@ -99,7 +99,7 @@ const AudioFile: FC = () => {
9999
<Button
100100
title={isPlaying ? 'Stop' : 'Play'}
101101
onPress={handlePress}
102-
disabled={!audioBuffer ? true : false}
102+
disabled={!audioBuffer}
103103
/>
104104
</Container>
105105
);
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import * as FileSystem from 'expo-file-system';
3+
import {
4+
AudioContext,
5+
AnalyserNode,
6+
AudioBuffer,
7+
AudioBufferSourceNode,
8+
} from 'react-native-audio-api';
9+
import { ActivityIndicator, View, StyleSheet } from 'react-native';
10+
11+
import FreqTimeChart from './FreqTimeChart';
12+
import { Container, Button } from '../../components';
13+
import { layout } from '../../styles';
14+
15+
const FFT_SIZE = 512;
16+
17+
const URL =
18+
'https://software-mansion-labs.github.io/react-native-audio-api/audio/music/example-music-02.mp3';
19+
20+
const AudioVisualizer: React.FC = () => {
21+
const [isPlaying, setIsPlaying] = useState(false);
22+
const [isLoading, setIsLoading] = useState(false);
23+
24+
const [times, setTimes] = useState<number[]>(
25+
new Array(FFT_SIZE / 2).fill(127)
26+
);
27+
const [freqs, setFreqs] = useState<number[]>(new Array(FFT_SIZE / 2).fill(0));
28+
29+
const audioContextRef = useRef<AudioContext | null>(null);
30+
const analyserRef = useRef<AnalyserNode | null>(null);
31+
const bufferSourceRef = useRef<AudioBufferSourceNode | null>(null);
32+
const audioBufferRef = useRef<AudioBuffer | null>(null);
33+
34+
const handlePlayPause = () => {
35+
if (isPlaying) {
36+
bufferSourceRef.current?.stop();
37+
} else {
38+
if (!audioContextRef.current || !analyserRef.current) {
39+
return;
40+
}
41+
42+
bufferSourceRef.current = audioContextRef.current.createBufferSource();
43+
bufferSourceRef.current.buffer = audioBufferRef.current;
44+
bufferSourceRef.current.connect(analyserRef.current);
45+
46+
bufferSourceRef.current.start();
47+
48+
requestAnimationFrame(draw);
49+
}
50+
51+
setIsPlaying((prev) => !prev);
52+
};
53+
54+
const draw = () => {
55+
if (!analyserRef.current) {
56+
return;
57+
}
58+
59+
const bufferLength = analyserRef.current.frequencyBinCount;
60+
61+
const timesArray = new Array(bufferLength);
62+
analyserRef.current.getByteTimeDomainData(timesArray);
63+
setTimes(timesArray);
64+
65+
const freqsArray = new Array(bufferLength);
66+
analyserRef.current.getByteFrequencyData(freqsArray);
67+
setFreqs(freqsArray);
68+
69+
requestAnimationFrame(draw);
70+
};
71+
72+
useEffect(() => {
73+
if (!audioContextRef.current) {
74+
audioContextRef.current = new AudioContext();
75+
}
76+
77+
if (!analyserRef.current) {
78+
analyserRef.current = audioContextRef.current.createAnalyser();
79+
analyserRef.current.fftSize = FFT_SIZE;
80+
analyserRef.current.smoothingTimeConstant = 0.8;
81+
82+
analyserRef.current.connect(audioContextRef.current.destination);
83+
}
84+
85+
const fetchBuffer = async () => {
86+
setIsLoading(true);
87+
audioBufferRef.current = await FileSystem.downloadAsync(
88+
URL,
89+
FileSystem.documentDirectory + 'audio.mp3'
90+
).then(({ uri }) => {
91+
return audioContextRef.current!.decodeAudioDataSource(uri);
92+
});
93+
94+
setIsLoading(false);
95+
};
96+
97+
fetchBuffer();
98+
99+
return () => {
100+
audioContextRef.current?.close();
101+
};
102+
}, []);
103+
104+
return (
105+
<Container disablePadding>
106+
<View style={{ flex: 0.2 }} />
107+
<FreqTimeChart
108+
timeData={times}
109+
frequencyData={freqs}
110+
frequencyBinCount={
111+
analyserRef.current?.frequencyBinCount || FFT_SIZE / 2
112+
}
113+
/>
114+
<View
115+
style={{ flex: 0.5, justifyContent: 'center', alignItems: 'center' }}>
116+
{isLoading && <ActivityIndicator color="#FFFFFF" />}
117+
<View style={styles.button}>
118+
<Button
119+
onPress={handlePlayPause}
120+
title={isPlaying ? 'Pause' : 'Play'}
121+
disabled={!audioBufferRef.current}
122+
/>
123+
</View>
124+
</View>
125+
</Container>
126+
);
127+
};
128+
129+
const styles = StyleSheet.create({
130+
container: {
131+
flex: 1,
132+
justifyContent: 'center',
133+
alignItems: 'center',
134+
},
135+
button: {
136+
justifyContent: 'center',
137+
flexDirection: 'row',
138+
marginTop: layout.spacing * 2,
139+
},
140+
});
141+
142+
export default AudioVisualizer;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React, {
2+
useState,
3+
useContext,
4+
createContext,
5+
PropsWithChildren,
6+
useMemo,
7+
} from 'react';
8+
import { LayoutChangeEvent, StyleSheet } from 'react-native';
9+
import { Canvas as SKCanvas } from '@shopify/react-native-skia';
10+
11+
interface Size {
12+
width: number;
13+
height: number;
14+
}
15+
16+
interface CanvasContext {
17+
initialized: boolean;
18+
size: Size;
19+
}
20+
21+
const CanvasContext = createContext<CanvasContext>({
22+
initialized: false,
23+
size: { width: 0, height: 0 },
24+
});
25+
26+
const Canvas: React.FC<PropsWithChildren> = ({ children }) => {
27+
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
28+
29+
const onCanvasLayout = (event: LayoutChangeEvent) => {
30+
const { width, height } = event.nativeEvent.layout;
31+
32+
setSize({ width, height });
33+
};
34+
35+
const context = useMemo(
36+
() => ({
37+
initialized: true,
38+
size: { width: size.width, height: size.height },
39+
}),
40+
[size.width, size.height]
41+
);
42+
43+
return (
44+
<SKCanvas style={styles.canvas} onLayout={onCanvasLayout}>
45+
<CanvasContext.Provider value={context}>
46+
{children}
47+
</CanvasContext.Provider>
48+
</SKCanvas>
49+
);
50+
};
51+
52+
export function useCanvas() {
53+
const canvasContext = useContext(CanvasContext);
54+
55+
if (!canvasContext.initialized) {
56+
throw new Error('Canvas context not initialized');
57+
}
58+
59+
return canvasContext;
60+
}
61+
62+
export function withCanvas<P extends object>(
63+
Component: React.ComponentType<P>
64+
) {
65+
return (props: P) => {
66+
return (
67+
<Canvas>
68+
<Component {...props} />
69+
</Canvas>
70+
);
71+
};
72+
}
73+
74+
export default Canvas;
75+
76+
const styles = StyleSheet.create({
77+
canvas: {
78+
flex: 1,
79+
},
80+
});

0 commit comments

Comments
 (0)