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.

1npx create-react-app piano-hooks
2npm i soundfont-player

Once it is done, add a following code for Audio Player and Audio Context.

Screenshot 2020 02 01 at 7 45 43 PM

Audio context will have the context and Audio Player will have two methods that are setInstrument and playNote.

1import SoundFontPlayer from "soundfont-player"
2import AudioContext from "./AudioContext"
3
4const NullSoundFontPlayerNoteAudio = {
5 stop() {},
6}
7
8const NullSoundFontPlayer = {
9 play() {
10 return NullSoundFontPlayerNoteAudio
11 },
12}
13const AudioPlayer = () => {
14 //Audio Context
15 const audioContext = AudioContext && new AudioContext()
16
17 //soundPlayer
18 let soundPlayer = NullSoundFontPlayer
19 //setInstrument
20 const Player = {
21 setInstrument(instrumentName) {
22 SoundFontPlayer.instrument(audioContext, instrumentName)
23 .then(soundfontPlayer => {
24 soundPlayer = soundfontPlayer
25 })
26 .catch(e => {
27 soundPlayer = NullSoundFontPlayer
28 })
29 },
30 playNote(note) {
31 soundPlayer.play(note)
32 },
33 }
34 return Player
35}
36
37export default AudioPlayer

and AudioContext.js will contain

1export default window.AudioContext

After that, Let's test if it is working properly, add the following code in App.js

1import React, { useEffect } from "react"
2import "./App.css"
3import AudioPlayer from "./core/AudioPlayer"
4function App() {
5 const audioPlayer = AudioPlayer()
6
7 useEffect(() => {
8 audioPlayer.setInstrument("acoustic_grand_piano")
9 }, [])
10
11 const handleClick = () => {
12 audioPlayer.playNote("C4")
13 }
14
15 return (
16 <div className="app-container">
17 <button onClick={handleClick}>Play</button>
18 </div>
19 )
20}
21
22export 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.

Screenshot 2020 02 01 at 7 54 10 PM

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,

1import React, { useEffect, useState } from "react"
2import AudioPlayer from "./AudioPlayer"
3
4const InstrumentAudio = ({ instrumentName, notes }) => {
5 const [instrumentPlayer, setInstrumentPlayer] = useState(null)
6 useEffect(() => {
7 setInstrumentPlayer(AudioPlayer())
8 }, [])
9
10 useEffect(() => {
11 if (instrumentPlayer) {
12 setInstrument()
13 playNotes()
14 }
15 }, [instrumentPlayer])
16
17 useEffect(() => {
18 if (notes && notes.length > 0) {
19 playNotes()
20 }
21 }, [notes])
22
23 const setInstrument = () => {
24 instrumentPlayer.setInstrument(instrumentName)
25 }
26
27 const playNotes = () => {
28 if (instrumentPlayer) {
29 instrumentPlayer.playNote(notes[0])
30 }
31 }
32
33 return null
34}
35
36export 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.

1import React, { Fragment } from "react"
2import InstrumentAudio from "./Keyboard/InstrumentAudio"
3import getNotesBetween from "./utils/getNotesBetween"
4
5const Instrument = ({ instrumentName, startNote, endNote }) => {
6 const notes = getNotesBetween(startNote, endNote)
7 return (
8 <Fragment>
9 {notes.map(note => {
10 return <Fragment>Note is : {note}</Fragment>
11 })}
12 <InstrumentAudio />
13 </Fragment>
14 )
15}
16
17export 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.

1const TONES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
2const OCTAVE_NUMBERS = [1, 2, 3, 4, 5, 6, 7]
3
4export default OCTAVE_NUMBERS.reduce((notes, octaveNumber) => {
5 const notesInOctave = TONES.map(tone => `${tone}${octaveNumber}`)
6 return [...notes, ...notesInOctave]
7}, [])

After that, create a file getNotesBetween.js to get all the notes between start note and end note.

1import NOTES from "../constants/note"
2
3export default function getNotesBetween(startNote, endNote) {
4 const startingIndex = NOTES.indexOf(startNote)
5 const endingIndex = NOTES.indexOf(endNote)
6 return NOTES.slice(startingIndex, endingIndex + 1)
7}

Now, it is time to add the instrument and it's state notes in the Instrument.js.

1import React, { Fragment, useState } from "react"
2import InstrumentAudio from "./Keyboard/InstrumentAudio"
3import getNotesBetween from "./utils/getNotesBetween"
4import isAccidentalNote from "./utils/isAccidentalNote"
5
6const Instrument = ({
7 instrumentName,
8 startNote,
9 endNote,
10 renderPianoKey,
11 keyboardMap,
12}) => {
13 const notes = getNotesBetween(startNote, endNote)
14
15 const [state, setState] = useState({
16 notesPlaying: [],
17 })
18
19 const onPlayNoteStart = note => {
20 setState({ ...state, notesPlaying: [...state.notesPlaying, note] })
21 }
22
23 const onPlayNoteEnd = note => {
24 setState({
25 ...state,
26 notesPlaying: state.notesPlaying.filter(
27 notePlaying => notePlaying !== note
28 ),
29 })
30 }
31
32 return (
33 <Fragment>
34 {notes.map(note => {
35 return (
36 <Fragment key={note}>
37 {renderPianoKey({
38 note,
39 isAccidentalNote: isAccidentalNote(note),
40 isNotePlaying: state.notesPlaying.includes(note),
41 startPlayingNote: () => onPlayNoteStart(note),
42 stopPlayingNote: () => onPlayNoteEnd(note),
43 keyboardShortcut: getKeyboardShortcutsForNote(keyboardMap, note),
44 })}
45 </Fragment>
46 )
47 })}
48 <InstrumentAudio
49 instrumentName={instrumentName}
50 notes={state.notesPlaying}
51 />
52 </Fragment>
53 )
54}
55
56export 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

1import NOTES from "../constants/note"
2export default note => {
3 return NOTES.includes(note) && note.includes("#")
4}

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.

1useEffect(() => {
2 window.addEventListener("keydown", handleKeyDown)
3 window.addEventListener("keyup", handleKeyUp)
4}, [])
5
6const handleKeyDown = e => {
7 if (isRegularKey(e) && !e.repeat) {
8 const note = getNoteFromKeyboardKey(e.key)
9 if (note) {
10 setState({ ...state, notesPlaying: [...state.notesPlaying, note] })
11 }
12 }
13}
14
15const handleKeyUp = e => {
16 if (isRegularKey(e) && !e.repeat) {
17 const note = getNoteFromKeyboardKey(e.key)
18 if (note) {
19 setState({
20 ...state,
21 notesPlaying: state.notesPlaying.filter(
22 notePlaying => notePlaying !== note
23 ),
24 })
25 }
26 }
27}

Piano

1import React, { Fragment } from "react"
2import Instrument from "./Instrument"
3
4const Piano = () => {
5 const accidentalKey = ({ isPlaying, text, eventHandlers }) => {
6 return (
7 <div className="piano-accidental-key-wrapper">
8 <button
9 className={`piano-accidental-key ${
10 isPlaying ? "piano-accidental-key-playing" : ""
11 } `}
12 {...eventHandlers}
13 >
14 <div className="piano-text">{text}</div>
15 </button>
16 </div>
17 )
18 }
19
20 const naturalKey = ({ isPlaying, text, eventHandlers }) => {
21 return (
22 <button
23 className={`piano-natural-key ${
24 isPlaying ? "piano-natural-key-playing" : ""
25 } `}
26 {...eventHandlers}
27 >
28 <div className="piano-text">{text}</div>
29 </button>
30 )
31 }
32
33 const renderPianoKey = ({
34 isAccidentalNote,
35 isNotePlaying,
36 startPlayingNote,
37 stopPlayingNote,
38 keyboardShortcut,
39 }) => {
40 const KeyComponent = isAccidentalNote ? accidentalKey : naturalKey
41
42 const eventHandlers = {
43 onMouseDown: startPlayingNote,
44 onMouseUp: stopPlayingNote,
45 onTouchStart: startPlayingNote,
46 onMouseOut: stopPlayingNote,
47 onTouchEnd: stopPlayingNote,
48 }
49
50 return (
51 <KeyComponent
52 isPlaying={isNotePlaying}
53 text={keyboardShortcut.join("/")}
54 eventHandlers={eventHandlers}
55 />
56 )
57 }
58
59 return (
60 <div className="piano-container">
61 <Instrument
62 instrumentName={"acoustic_grand_piano"}
63 startNote={"C3"}
64 endNote={"B5"}
65 renderPianoKey={renderPianoKey}
66 keyboardMap={{
67 Q: "C3",
68 2: "C#3",
69 W: "D3",
70 3: "D#3",
71 E: "E3",
72 R: "F3",
73 5: "F#3",
74 T: "G3",
75 6: "G#3",
76 Y: "A3",
77 7: "A#3",
78 U: "B3",
79 I: "C4",
80 9: "C#4",
81 O: "D4",
82 0: "D#4",
83 P: "E4",
84 Z: "F4",
85 S: "F#4",
86 X: "G4",
87 D: "G#4",
88 C: "A4",
89 F: "A#4",
90 V: "B4",
91 B: "C5",
92 H: "C#5",
93 N: "D5",
94 J: "D#5",
95 M: "E5",
96 ",": "F5",
97 L: "F#5",
98 ".": "G5",
99 ";": "G#5",
100 "/": "A5",
101 "'": "A#5",
102 A: "B5",
103 }}
104 />
105 </div>
106 )
107}
108export 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

Demo

To Read More

Everything you need to know about d...

Docker volume is a persistent data storage mechanism to store the data in docker...

How to remove docker image - Docker...

This guide will cover everything you need to know about removing docker images f...

How to build an Actionable data ta...

In this article, we will see how to build an Actionable data table using a react...