Skip to content

Commit 623070d

Browse files
committed
feat: docs - more live examples
1 parent b913304 commit 623070d

File tree

9 files changed

+729
-16
lines changed

9 files changed

+729
-16
lines changed

packages/audiodocs/docs/fundamentals/making-a-piano-keyboard.mdx

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
sidebar_position: 3
33
---
44

5+
import InteractiveExample from '@site/src/components/InteractiveExample';
6+
57
# Making a piano keyboard
68

79
In this section, we will use some of the core audio api interfaces to create a simple piano keyboard. We will learn what is an `AudioParam`, how to use it to change the pitch of the sound.
@@ -71,13 +73,8 @@ Like previously, we will need to preload the audio files in order to be able to
7173
First comes the import section and list of the sources we will use, also lets help ourselves with type shorthand for partial record:
7274

7375
```tsx
74-
import {
75-
GainNode,
76-
AudioBuffer,
77-
AudioContext,
78-
AudioBufferSourceNode,
79-
} from 'react-native-audio-api';
8076
import * as FileSystem from 'expo-file-system';
77+
import { AudioBuffer, AudioContext } from 'react-native-audio-api';
8178

8279
/* ... */
8380

@@ -103,7 +100,7 @@ export default function SimplePiano() {
103100
audioContextRef.current = new AudioContext();
104101
}
105102

106-
Object.entries(sourceList).forEach(async ([key, source]) => {
103+
Object.entries(sourceList).forEach(async ([key, url]) => {
107104
bufferListRef.current[key as KeyName] = await FileSystem.downloadAsync(
108105
url,
109106
`${FileSystem.documentDirectory}/${key}.mp3`
@@ -120,9 +117,13 @@ Now it is finally time to play the sounds, but still nothing new here. We will u
120117
```tsx
121118
export default function SimplePiano() {
122119
const onKeyPressIn = (which: KeyName) => {
123-
const audioContext = audioContextRef.current!;
120+
const audioContext = audioContextRef.current;
124121
const buffer = bufferMapRef.current[which];
125122

123+
if (!audioContext || !buffer) {
124+
return;
125+
}
126+
126127
const source = new AudioBufferSourceNode(audioContext, {
127128
buffer,
128129
});
@@ -133,6 +134,13 @@ export default function SimplePiano() {
133134
}
134135
```
135136

137+
When we put everything all together, we will get something like this:
138+
139+
import ItHangs from '@site/src/examples/SimplePiano/ItHangsComponent';
140+
import ItHangsSrc from '!!raw-loader!@site/src/examples/SimplePiano/ItHangsSource';
141+
142+
<InteractiveExample component={ItHangs} src={ItHangsSrc} />
143+
136144
Great! But a lot of things are a bit off here:
137145

138146
- We are not stopping the sound when the button is released, which is kind of the way piano should work, right? 🙃
@@ -171,12 +179,20 @@ And finally we can implement the `onKeyPressOut` function
171179
```tsx
172180
const onKeyPressOut = (which: KeyName) => {
173181
const source = playingNotesRef.current[which];
182+
174183
if (source) {
175184
source.stop();
176185
}
177186
};
178187
```
179188

189+
Putting it all together again we get:
190+
191+
import PressOutComponent from '@site/src/examples/SimplePiano/PressOutComponent';
192+
import PressOutSrc from '!!raw-loader!@site/src/examples/SimplePiano/PressOutSource';
193+
194+
<InteractiveExample component={PressOutComponent} src={PressOutSrc} />
195+
180196
And they stop on release, just as we wanted. But if we hold the keys for a short time, it sounds a bit strange. Also have You noticed that the sound is simply cut off when we release the key? 🤔
181197
It leave a bit unpleasant feeling, right? So let's try to make it a bit more smooth.
182198

@@ -237,10 +253,14 @@ const onKeyPressIn = (which: KeyName) => {
237253
const buffer = bufferMapRef.current[which];
238254
const tNow = audioContext.currentTime;
239255

240-
const source = aCtx.createBufferSource();
256+
if (!audioContext || !buffer) {
257+
return;
258+
}
259+
260+
const source = audioContext.createBufferSource();
241261
source.buffer = buffer;
242262

243-
const envelope = aCtx.createGain();
263+
const envelope = audioContext.createGain();
244264

245265
source.connect(envelope);
246266
envelope.connect(audioContext.destination);
@@ -258,7 +278,13 @@ and the `onKeyPressOut` function:
258278
```tsx
259279
const onKeyPressOut = (which: KeyName) => {
260280
const audioContext = audioContextRef.current!;
261-
const { source, envelope, startedAt } = playingNotesRef.current[which];
281+
const playingNote = playingNotesRef.current[which];
282+
283+
if (!playingNote || !audioContext) {
284+
return;
285+
}
286+
287+
const { source, envelope, startedAt } = playingNote;
262288

263289
const tStop = Math.max(audioContext.currentTime, startedAt + 1);
264290

@@ -270,6 +296,13 @@ const onKeyPressOut = (which: KeyName) => {
270296
};
271297
```
272298

299+
In result we can hear something like this:
300+
301+
import EnvelopesComponent from '@site/src/examples/SimplePiano/EnvelopesComponent';
302+
import EnvelopesSrc from '!!raw-loader!@site/src/examples/SimplePiano/EnvelopesSource';
303+
304+
<InteractiveExample component={EnvelopesComponent} src={EnvelopesSrc} />
305+
273306
And it finally sounds smooth and nice. But what about decay and sustain phases? Both are done by the audio samples themselves, so we don't need to worry about them. To be honest, same goes for the attack phase, but we have implemented it for the sake of this guide. 🙂 <br /><br />
274307
So the only missing piece left is doing something about the missing sample files for 'B' and 'D' keys. What we can do about that?
275308

@@ -332,18 +365,15 @@ const onKeyPressIn = (which: KeyName) => {
332365
const source = aCtx.createBufferSource();
333366
const envelope = aCtx.createGain();
334367
source.buffer = buffer;
335-
336-
// rest of the code remains the same
337368
};
338369
```
339370

340371
## Final results
341372

342373
As previously, you can see the final results in the live example below with full source code.
343374

344-
import InteractiveExample from '@site/src/components/InteractiveExample';
345-
import SimplePiano from '@site/src/examples/SimplePiano/Component';
346-
import SimplePianoSrc from '!!raw-loader!@site/src/examples/SimplePiano/Source';
375+
import SimplePiano from '@site/src/examples/SimplePiano/FinalComponent';
376+
import SimplePianoSrc from '!!raw-loader!@site/src/examples/SimplePiano/FinalSource';
347377

348378
<InteractiveExample component={SimplePiano} src={SimplePianoSrc} />
349379

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { View, Text, Pressable } from 'react-native';
2+
import React, { FC, useEffect, useRef } from 'react';
3+
import {
4+
GainNode,
5+
AudioBuffer,
6+
AudioContext,
7+
AudioBufferSourceNode,
8+
} from 'react-native-audio-api';
9+
10+
type KeyName = 'A' | 'B' | 'C' | 'D' | 'E';
11+
12+
type PR<V> = Partial<Record<KeyName, V>>;
13+
14+
interface ButtonProps {
15+
keyName: KeyName;
16+
onPressIn: (key: KeyName) => void;
17+
onPressOut: (key: KeyName) => void;
18+
}
19+
20+
interface PlayingNote {
21+
source: AudioBufferSourceNode;
22+
envelope: GainNode;
23+
startedAt: number;
24+
}
25+
26+
const Keys = ['A', 'B', 'C', 'D', 'E'] as const;
27+
28+
const sourceList: PR<string> = {
29+
A: '/react-native-audio-api/audio/sounds/C4.mp3',
30+
C: '/react-native-audio-api/audio/sounds/Ds4.mp3',
31+
E: '/react-native-audio-api/audio/sounds/Fs4.mp3',
32+
};
33+
34+
const Button = ({ onPressIn, onPressOut, keyName }: ButtonProps) => (
35+
<Pressable
36+
onPressIn={() => onPressIn(keyName)}
37+
onPressOut={() => onPressOut(keyName)}
38+
style={({ pressed }) => ({
39+
margin: 4,
40+
padding: 12,
41+
borderRadius: 2,
42+
backgroundColor: pressed ? '#d2e6ff' : '#abcdef',
43+
})}
44+
>
45+
<Text style={{ color: 'white' }}>{`${keyName}`}</Text>
46+
</Pressable>
47+
);
48+
49+
const SimplePiano: FC = () => {
50+
const audioContextRef = useRef<AudioContext | null>(null);
51+
const playingNotesRef = useRef<PR<PlayingNote>>({});
52+
const bufferMapRef = useRef<PR<AudioBuffer>>({});
53+
54+
const onKeyPressIn = (which: KeyName) => {
55+
const audioContext = audioContextRef.current;
56+
let buffer = bufferMapRef.current[which];
57+
const tNow = audioContext.currentTime;
58+
59+
if (!audioContext || !buffer) {
60+
return;
61+
}
62+
63+
const source = audioContext.createBufferSource();
64+
source.buffer = buffer;
65+
66+
const envelope = audioContext.createGain();
67+
68+
source.connect(envelope);
69+
envelope.connect(audioContext.destination);
70+
71+
envelope.gain.setValueAtTime(0.001, tNow);
72+
envelope.gain.exponentialRampToValueAtTime(1, tNow + 0.1);
73+
74+
source.start(tNow);
75+
76+
playingNotesRef.current[which] = { source, envelope, startedAt: tNow };
77+
};
78+
79+
const onKeyPressOut = (which: KeyName) => {
80+
const audioContext = audioContextRef.current!;
81+
const playingNote = playingNotesRef.current[which];
82+
83+
if (!playingNote || !audioContext) {
84+
return;
85+
}
86+
87+
const { source, envelope, startedAt } = playingNote;
88+
89+
const tStop = Math.max(audioContext.currentTime, startedAt + 1);
90+
91+
envelope.gain.exponentialRampToValueAtTime(0.0001, tStop + 0.08);
92+
envelope.gain.setValueAtTime(0, tStop + 0.09);
93+
source.stop(tStop + 0.1);
94+
95+
playingNotesRef.current[which] = undefined;
96+
};
97+
98+
useEffect(() => {
99+
if (!audioContextRef.current) {
100+
audioContextRef.current = new AudioContext();
101+
}
102+
103+
Object.entries(sourceList).forEach(async ([key, url]) => {
104+
bufferMapRef.current[key as KeyName] =
105+
await audioContextRef.current!.decodeAudioDataSource(url);
106+
});
107+
108+
return () => {
109+
audioContextRef.current?.close();
110+
};
111+
}, []);
112+
113+
return (
114+
<View
115+
style={{
116+
flex: 1,
117+
justifyContent: 'center',
118+
alignItems: 'center',
119+
flexDirection: 'row',
120+
}}
121+
>
122+
{Keys.map((key) => (
123+
<Button
124+
onPressIn={onKeyPressIn}
125+
onPressOut={onKeyPressOut}
126+
keyName={key}
127+
key={key}
128+
/>
129+
))}
130+
</View>
131+
);
132+
};
133+
134+
export default SimplePiano;

0 commit comments

Comments
 (0)