Skip to content

Commit

Permalink
feat: docs - more live examples
Browse files Browse the repository at this point in the history
  • Loading branch information
michalsek committed Dec 18, 2024
1 parent b913304 commit 623070d
Show file tree
Hide file tree
Showing 9 changed files with 729 additions and 16 deletions.
62 changes: 46 additions & 16 deletions packages/audiodocs/docs/fundamentals/making-a-piano-keyboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
sidebar_position: 3
---

import InteractiveExample from '@site/src/components/InteractiveExample';

# Making a piano keyboard

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.
Expand Down Expand Up @@ -71,13 +73,8 @@ Like previously, we will need to preload the audio files in order to be able to
First comes the import section and list of the sources we will use, also lets help ourselves with type shorthand for partial record:

```tsx
import {
GainNode,
AudioBuffer,
AudioContext,
AudioBufferSourceNode,
} from 'react-native-audio-api';
import * as FileSystem from 'expo-file-system';
import { AudioBuffer, AudioContext } from 'react-native-audio-api';

/* ... */

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

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

if (!audioContext || !buffer) {
return;
}

const source = new AudioBufferSourceNode(audioContext, {
buffer,
});
Expand All @@ -133,6 +134,13 @@ export default function SimplePiano() {
}
```

When we put everything all together, we will get something like this:

import ItHangs from '@site/src/examples/SimplePiano/ItHangsComponent';
import ItHangsSrc from '!!raw-loader!@site/src/examples/SimplePiano/ItHangsSource';

<InteractiveExample component={ItHangs} src={ItHangsSrc} />

Great! But a lot of things are a bit off here:

- We are not stopping the sound when the button is released, which is kind of the way piano should work, right? 🙃
Expand Down Expand Up @@ -171,12 +179,20 @@ And finally we can implement the `onKeyPressOut` function
```tsx
const onKeyPressOut = (which: KeyName) => {
const source = playingNotesRef.current[which];

if (source) {
source.stop();
}
};
```

Putting it all together again we get:

import PressOutComponent from '@site/src/examples/SimplePiano/PressOutComponent';
import PressOutSrc from '!!raw-loader!@site/src/examples/SimplePiano/PressOutSource';

<InteractiveExample component={PressOutComponent} src={PressOutSrc} />

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? 🤔
It leave a bit unpleasant feeling, right? So let's try to make it a bit more smooth.

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

const source = aCtx.createBufferSource();
if (!audioContext || !buffer) {
return;
}

const source = audioContext.createBufferSource();
source.buffer = buffer;

const envelope = aCtx.createGain();
const envelope = audioContext.createGain();

source.connect(envelope);
envelope.connect(audioContext.destination);
Expand All @@ -258,7 +278,13 @@ and the `onKeyPressOut` function:
```tsx
const onKeyPressOut = (which: KeyName) => {
const audioContext = audioContextRef.current!;
const { source, envelope, startedAt } = playingNotesRef.current[which];
const playingNote = playingNotesRef.current[which];

if (!playingNote || !audioContext) {
return;
}

const { source, envelope, startedAt } = playingNote;

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

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

In result we can hear something like this:

import EnvelopesComponent from '@site/src/examples/SimplePiano/EnvelopesComponent';
import EnvelopesSrc from '!!raw-loader!@site/src/examples/SimplePiano/EnvelopesSource';

<InteractiveExample component={EnvelopesComponent} src={EnvelopesSrc} />

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 />
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?

Expand Down Expand Up @@ -332,18 +365,15 @@ const onKeyPressIn = (which: KeyName) => {
const source = aCtx.createBufferSource();
const envelope = aCtx.createGain();
source.buffer = buffer;

// rest of the code remains the same
};
```

## Final results

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

import InteractiveExample from '@site/src/components/InteractiveExample';
import SimplePiano from '@site/src/examples/SimplePiano/Component';
import SimplePianoSrc from '!!raw-loader!@site/src/examples/SimplePiano/Source';
import SimplePiano from '@site/src/examples/SimplePiano/FinalComponent';
import SimplePianoSrc from '!!raw-loader!@site/src/examples/SimplePiano/FinalSource';

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

Expand Down
134 changes: 134 additions & 0 deletions packages/audiodocs/src/examples/SimplePiano/EnvelopesComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { View, Text, Pressable } from 'react-native';
import React, { FC, useEffect, useRef } from 'react';
import {
GainNode,
AudioBuffer,
AudioContext,
AudioBufferSourceNode,
} from 'react-native-audio-api';

type KeyName = 'A' | 'B' | 'C' | 'D' | 'E';

type PR<V> = Partial<Record<KeyName, V>>;

interface ButtonProps {
keyName: KeyName;
onPressIn: (key: KeyName) => void;
onPressOut: (key: KeyName) => void;
}

interface PlayingNote {
source: AudioBufferSourceNode;
envelope: GainNode;
startedAt: number;
}

const Keys = ['A', 'B', 'C', 'D', 'E'] as const;

const sourceList: PR<string> = {
A: '/react-native-audio-api/audio/sounds/C4.mp3',
C: '/react-native-audio-api/audio/sounds/Ds4.mp3',
E: '/react-native-audio-api/audio/sounds/Fs4.mp3',
};

const Button = ({ onPressIn, onPressOut, keyName }: ButtonProps) => (
<Pressable
onPressIn={() => onPressIn(keyName)}
onPressOut={() => onPressOut(keyName)}
style={({ pressed }) => ({
margin: 4,
padding: 12,
borderRadius: 2,
backgroundColor: pressed ? '#d2e6ff' : '#abcdef',
})}
>
<Text style={{ color: 'white' }}>{`${keyName}`}</Text>
</Pressable>
);

const SimplePiano: FC = () => {
const audioContextRef = useRef<AudioContext | null>(null);
const playingNotesRef = useRef<PR<PlayingNote>>({});
const bufferMapRef = useRef<PR<AudioBuffer>>({});

const onKeyPressIn = (which: KeyName) => {
const audioContext = audioContextRef.current;
let buffer = bufferMapRef.current[which];
const tNow = audioContext.currentTime;

if (!audioContext || !buffer) {
return;
}

const source = audioContext.createBufferSource();
source.buffer = buffer;

const envelope = audioContext.createGain();

source.connect(envelope);
envelope.connect(audioContext.destination);

envelope.gain.setValueAtTime(0.001, tNow);
envelope.gain.exponentialRampToValueAtTime(1, tNow + 0.1);

source.start(tNow);

playingNotesRef.current[which] = { source, envelope, startedAt: tNow };
};

const onKeyPressOut = (which: KeyName) => {
const audioContext = audioContextRef.current!;
const playingNote = playingNotesRef.current[which];

if (!playingNote || !audioContext) {
return;
}

const { source, envelope, startedAt } = playingNote;

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

envelope.gain.exponentialRampToValueAtTime(0.0001, tStop + 0.08);
envelope.gain.setValueAtTime(0, tStop + 0.09);
source.stop(tStop + 0.1);

playingNotesRef.current[which] = undefined;
};

useEffect(() => {
if (!audioContextRef.current) {
audioContextRef.current = new AudioContext();
}

Object.entries(sourceList).forEach(async ([key, url]) => {
bufferMapRef.current[key as KeyName] =
await audioContextRef.current!.decodeAudioDataSource(url);
});

return () => {
audioContextRef.current?.close();
};
}, []);

return (
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
}}
>
{Keys.map((key) => (
<Button
onPressIn={onKeyPressIn}
onPressOut={onKeyPressOut}
keyName={key}
key={key}
/>
))}
</View>
);
};

export default SimplePiano;
Loading

0 comments on commit 623070d

Please sign in to comment.