Building a Piano with React Hooks
Building a Piano with React Hooks
In this article, we will see how to build a piano with react hooks. Building a Piano with React Hooks. if you are completely new to react hooks, check out this course.
Recent Articles,
TypeScript for React developers in 2020
Building Real time API using graphql subscriptions
Before we proceed further, we will see a demo
https://www.youtube.com/watch?v=9R8zMAMYHWE
Things to consider while building a piano is,
- How to map the laptop keys to piano notes.
- Map the audio with key press.
- How to render the piano keyboard in react.
Let’s try to break it down one by one. Firstly, we will see how to Add the Audio to react application in a button click.
we will be using a library called sound font player for audio in react application.
npx create-react-app piano-hooks
npm i soundfont-player
Once it is done, add a following code for Audio Player and Audio Context.
Audio context will have the context and Audio Player will have two methods that are setInstrument and playNote.
import SoundFontPlayer from "soundfont-player";
import AudioContext from "./AudioContext";
const NullSoundFontPlayerNoteAudio = {
stop() {},
};
const NullSoundFontPlayer = {
play() {
return NullSoundFontPlayerNoteAudio;
},
};
const AudioPlayer = () => {
//Audio Context
const audioContext = AudioContext && new AudioContext();
//soundPlayer
let soundPlayer = NullSoundFontPlayer;
//setInstrument
const Player = {
setInstrument(instrumentName) {
SoundFontPlayer.instrument(audioContext, instrumentName)
.then((soundfontPlayer) => {
soundPlayer = soundfontPlayer;
})
.catch((e) => {
soundPlayer = NullSoundFontPlayer;
});
},
playNote(note) {
soundPlayer.play(note);
},
};
return Player;
};
export default AudioPlayer;
and AudioContext.js will contain
export default window.AudioContext;
After that, Let’s test if it is working properly, add the following code in App.js
import React, { useEffect } from "react";
import "./App.css";
import AudioPlayer from "./core/AudioPlayer";
function App() {
const audioPlayer = AudioPlayer();
useEffect(() => {
audioPlayer.setInstrument("acoustic_grand_piano");
}, []);
const handleClick = () => {
audioPlayer.playNote("C4");
};
return (
<div className="app-container">
<button onClick={handleClick}>Play</button>
</div>
);
}
export default App;
Basically, we have a button that play the note when we click it. Here, useEffect will run on every component mount and set the instrument with a name.
Keyboard - Render Props
Let’s try to use a render props concepts on instrument. if you are not familiar with render props, check out this course.
Mainly, Instrument has two important parts.they are Instrument itself and instrumentAudio.
Firstly, we will see how to setup the instrumentAudio. we will move our app.js logic to instrumentAudio.
create a file InstrumentAudio.js and add the following code,
import React, { useEffect, useState } from "react";
import AudioPlayer from "./AudioPlayer";
const InstrumentAudio = ({ instrumentName, notes }) => {
const [instrumentPlayer, setInstrumentPlayer] = useState(null);
useEffect(() => {
setInstrumentPlayer(AudioPlayer());
}, []);
useEffect(() => {
if (instrumentPlayer) {
setInstrument();
playNotes();
}
}, [instrumentPlayer]);
useEffect(() => {
if (notes && notes.length > 0) {
playNotes();
}
}, [notes]);
const setInstrument = () => {
instrumentPlayer.setInstrument(instrumentName);
};
const playNotes = () => {
if (instrumentPlayer) {
instrumentPlayer.playNote(notes[0]);
}
};
return null;
};
export default InstrumentAudio;
Here, we are maintaining the instrumentPlayer in state, so that we can have the control of it.
when the component mount first, it will call the setInstrument method which will set the instrument with the name.
After that, every time the notes props change, it will play the note which is defined in the useEffect which has notes dependancy.
Now, it is time to implement the Instrument itself. instrument will have the start note and end note as props. based on that, it will render all the notes in between.
import React, { Fragment } from "react";
import InstrumentAudio from "./Keyboard/InstrumentAudio";
import getNotesBetween from "./utils/getNotesBetween";
const Instrument = ({ instrumentName, startNote, endNote }) => {
const notes = getNotesBetween(startNote, endNote);
return (
<Fragment>
{notes.map((note) => {
return <Fragment>Note is : {note}</Fragment>;
})}
<InstrumentAudio />
</Fragment>
);
};
export default Instrument;
Here we get all the notes in between the start note and end note. create a file called notes.js and add the following code.
const TONES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
const OCTAVE_NUMBERS = [1, 2, 3, 4, 5, 6, 7];
export default OCTAVE_NUMBERS.reduce((notes, octaveNumber) => {
const notesInOctave = TONES.map((tone) => `${tone}${octaveNumber}`);
return [...notes, ...notesInOctave];
}, []);
After that, create a file getNotesBetween.js to get all the notes between start note and end note.
import NOTES from "../constants/note";
export default function getNotesBetween(startNote, endNote) {
const startingIndex = NOTES.indexOf(startNote);
const endingIndex = NOTES.indexOf(endNote);
return NOTES.slice(startingIndex, endingIndex + 1);
}
Now, it is time to add the instrument and it’s state notes in the Instrument.js.
import React, { Fragment, useState } from "react";
import InstrumentAudio from "./Keyboard/InstrumentAudio";
import getNotesBetween from "./utils/getNotesBetween";
import isAccidentalNote from "./utils/isAccidentalNote";
const Instrument = ({
instrumentName,
startNote,
endNote,
renderPianoKey,
keyboardMap,
}) => {
const notes = getNotesBetween(startNote, endNote);
const [state, setState] = useState({
notesPlaying: [],
});
const onPlayNoteStart = (note) => {
setState({ ...state, notesPlaying: [...state.notesPlaying, note] });
};
const onPlayNoteEnd = (note) => {
setState({
...state,
notesPlaying: state.notesPlaying.filter(
(notePlaying) => notePlaying !== note
),
});
};
return (
<Fragment>
{notes.map((note) => {
return (
<Fragment key={note}>
{renderPianoKey({
note,
isAccidentalNote: isAccidentalNote(note),
isNotePlaying: state.notesPlaying.includes(note),
startPlayingNote: () => onPlayNoteStart(note),
stopPlayingNote: () => onPlayNoteEnd(note),
keyboardShortcut: getKeyboardShortcutsForNote(keyboardMap, note),
})}
</Fragment>
);
})}
<InstrumentAudio
instrumentName={instrumentName}
notes={state.notesPlaying}
/>
</Fragment>
);
};
export default Instrument;
Logic here is, renderPianoKey is a render props with the state from Instrument Component.
isAccidentalNote checks whether the note is a natural key or accidental key.
isAccidentalNote.js
import NOTES from "../constants/note";
export default (note) => {
return NOTES.includes(note) && note.includes("#");
};
isNotePlaying checks the state whether the note is in the state of playing notes.
startPlayingNote method gets called when user clicks the the button, when it is gets called, we add the particular note to the state.
on stopPlayingNote, we remove the note from the state.
finally, we add the keyboard actions such as keydown and keyup to handle the keyboard actions.
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
}, []);
const handleKeyDown = (e) => {
if (isRegularKey(e) && !e.repeat) {
const note = getNoteFromKeyboardKey(e.key);
if (note) {
setState({ ...state, notesPlaying: [...state.notesPlaying, note] });
}
}
};
const handleKeyUp = (e) => {
if (isRegularKey(e) && !e.repeat) {
const note = getNoteFromKeyboardKey(e.key);
if (note) {
setState({
...state,
notesPlaying: state.notesPlaying.filter(
(notePlaying) => notePlaying !== note
),
});
}
}
};
Piano
import React, { Fragment } from "react";
import Instrument from "./Instrument";
const Piano = () => {
const accidentalKey = ({ isPlaying, text, eventHandlers }) => {
return (
<div className="piano-accidental-key-wrapper">
<button
className={`piano-accidental-key ${
isPlaying ? "piano-accidental-key-playing" : ""
} `}
{...eventHandlers}
>
<div className="piano-text">{text}</div>
</button>
</div>
);
};
const naturalKey = ({ isPlaying, text, eventHandlers }) => {
return (
<button
className={`piano-natural-key ${
isPlaying ? "piano-natural-key-playing" : ""
} `}
{...eventHandlers}
>
<div className="piano-text">{text}</div>
</button>
);
};
const renderPianoKey = ({
isAccidentalNote,
isNotePlaying,
startPlayingNote,
stopPlayingNote,
keyboardShortcut,
}) => {
const KeyComponent = isAccidentalNote ? accidentalKey : naturalKey;
const eventHandlers = {
onMouseDown: startPlayingNote,
onMouseUp: stopPlayingNote,
onTouchStart: startPlayingNote,
onMouseOut: stopPlayingNote,
onTouchEnd: stopPlayingNote,
};
return (
<KeyComponent
isPlaying={isNotePlaying}
text={keyboardShortcut.join("/")}
eventHandlers={eventHandlers}
/>
);
};
return (
<div className="piano-container">
<Instrument
instrumentName={"acoustic_grand_piano"}
startNote={"C3"}
endNote={"B5"}
renderPianoKey={renderPianoKey}
keyboardMap={{
Q: "C3",
2: "C#3",
W: "D3",
3: "D#3",
E: "E3",
R: "F3",
5: "F#3",
T: "G3",
6: "G#3",
Y: "A3",
7: "A#3",
U: "B3",
I: "C4",
9: "C#4",
O: "D4",
0: "D#4",
P: "E4",
Z: "F4",
S: "F#4",
X: "G4",
D: "G#4",
C: "A4",
F: "A#4",
V: "B4",
B: "C5",
H: "C#5",
N: "D5",
J: "D#5",
M: "E5",
",": "F5",
L: "F#5",
".": "G5",
";": "G#5",
"/": "A5",
"'": "A#5",
A: "B5",
}}
/>
</div>
);
};
export default Piano;
Since the Instrument uses a render props. we need to pass the instrument component from the Piano.js file.
Here, we have the renderPianoKey function which takes all the argument from that method. if it is a accidental note, it renders the accidental key component.
If it is a natural key note, it renders the natural key component. Also, we need to provide the keyboardmap where each key will be mapped with piano notes.
Complete Source code