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.

1 npx create-react-app piano-hooks 2 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.

1 import SoundFontPlayer from "soundfont-player" 2 import AudioContext from "./AudioContext" 3 4 const NullSoundFontPlayerNoteAudio = { 5 stop ( ) { } , 6 } 7 8 const NullSoundFontPlayer = { 9 play ( ) { 10 return NullSoundFontPlayerNoteAudio 11 } , 12 } 13 const AudioPlayer = ( ) => { 14 15 const audioContext = AudioContext && new AudioContext ( ) 16 17 18 let soundPlayer = NullSoundFontPlayer 19 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 37 export default AudioPlayer

and AudioContext.js will contain

1 export default window . AudioContext

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

1 import React , { useEffect } from "react" 2 import "./App.css" 3 import AudioPlayer from "./core/AudioPlayer" 4 function 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 22 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,

1 import React , { useEffect , useState } from "react" 2 import AudioPlayer from "./AudioPlayer" 3 4 const 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 36 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.

1 import React , { Fragment } from "react" 2 import InstrumentAudio from "./Keyboard/InstrumentAudio" 3 import getNotesBetween from "./utils/getNotesBetween" 4 5 const 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 17 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.

1 const TONES = [ "C" , "C#" , "D" , "D#" , "E" , "F" , "F#" , "G" , "G#" , "A" , "A#" , "B" ] 2 const OCTAVE_NUMBERS = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 ] 3 4 export 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.

1 import NOTES from "../constants/note" 2 3 export 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.

1 import React , { Fragment , useState } from "react" 2 import InstrumentAudio from "./Keyboard/InstrumentAudio" 3 import getNotesBetween from "./utils/getNotesBetween" 4 import isAccidentalNote from "./utils/isAccidentalNote" 5 6 const 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 56 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

1 import NOTES from "../constants/note" 2 export 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.

1 useEffect ( ( ) => { 2 window . addEventListener ( "keydown" , handleKeyDown ) 3 window . addEventListener ( "keyup" , handleKeyUp ) 4 } , [ ] ) 5 6 const 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 15 const 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

1 import React , { Fragment } from "react" 2 import Instrument from "./Instrument" 3 4 const 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 } 108 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

Demo

To Read More