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.
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,
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-hooks2npm 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.
1import SoundFontPlayer from "soundfont-player"2import AudioContext from "./AudioContext"34const NullSoundFontPlayerNoteAudio = {5 stop() {},6}78const NullSoundFontPlayer = {9 play() {10 return NullSoundFontPlayerNoteAudio11 },12}13const AudioPlayer = () => {14 //Audio Context15 const audioContext = AudioContext && new AudioContext()1617 //soundPlayer18 let soundPlayer = NullSoundFontPlayer19 //setInstrument20 const Player = {21 setInstrument(instrumentName) {22 SoundFontPlayer.instrument(audioContext, instrumentName)23 .then(soundfontPlayer => {24 soundPlayer = soundfontPlayer25 })26 .catch(e => {27 soundPlayer = NullSoundFontPlayer28 })29 },30 playNote(note) {31 soundPlayer.play(note)32 },33 }34 return Player35}3637export 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()67 useEffect(() => {8 audioPlayer.setInstrument("acoustic_grand_piano")9 }, [])1011 const handleClick = () => {12 audioPlayer.playNote("C4")13 }1415 return (16 <div className="app-container">17 <button onClick={handleClick}>Play</button>18 </div>19 )20}2122export 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.
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"34const InstrumentAudio = ({ instrumentName, notes }) => {5 const [instrumentPlayer, setInstrumentPlayer] = useState(null)6 useEffect(() => {7 setInstrumentPlayer(AudioPlayer())8 }, [])910 useEffect(() => {11 if (instrumentPlayer) {12 setInstrument()13 playNotes()14 }15 }, [instrumentPlayer])1617 useEffect(() => {18 if (notes && notes.length > 0) {19 playNotes()20 }21 }, [notes])2223 const setInstrument = () => {24 instrumentPlayer.setInstrument(instrumentName)25 }2627 const playNotes = () => {28 if (instrumentPlayer) {29 instrumentPlayer.playNote(notes[0])30 }31 }3233 return null34}3536export 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"45const 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}1617export 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]34export 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"23export 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}
A Hands-On Guidebook to Learn Cloud Native Web Development - From Zero To Production
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"56const Instrument = ({7 instrumentName,8 startNote,9 endNote,10 renderPianoKey,11 keyboardMap,12}) => {13 const notes = getNotesBetween(startNote, endNote)1415 const [state, setState] = useState({16 notesPlaying: [],17 })1819 const onPlayNoteStart = note => {20 setState({ ...state, notesPlaying: [...state.notesPlaying, note] })21 }2223 const onPlayNoteEnd = note => {24 setState({25 ...state,26 notesPlaying: state.notesPlaying.filter(27 notePlaying => notePlaying !== note28 ),29 })30 }3132 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 <InstrumentAudio49 instrumentName={instrumentName}50 notes={state.notesPlaying}51 />52 </Fragment>53 )54}5556export 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}, [])56const 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}1415const 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 !== note23 ),24 })25 }26 }27}
1import React, { Fragment } from "react"2import Instrument from "./Instrument"34const Piano = () => {5 const accidentalKey = ({ isPlaying, text, eventHandlers }) => {6 return (7 <div className="piano-accidental-key-wrapper">8 <button9 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 }1920 const naturalKey = ({ isPlaying, text, eventHandlers }) => {21 return (22 <button23 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 }3233 const renderPianoKey = ({34 isAccidentalNote,35 isNotePlaying,36 startPlayingNote,37 stopPlayingNote,38 keyboardShortcut,39 }) => {40 const KeyComponent = isAccidentalNote ? accidentalKey : naturalKey4142 const eventHandlers = {43 onMouseDown: startPlayingNote,44 onMouseUp: stopPlayingNote,45 onTouchStart: startPlayingNote,46 onMouseOut: stopPlayingNote,47 onTouchEnd: stopPlayingNote,48 }4950 return (51 <KeyComponent52 isPlaying={isNotePlaying}53 text={keyboardShortcut.join("/")}54 eventHandlers={eventHandlers}55 />56 )57 }5859 return (60 <div className="piano-container">61 <Instrument62 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
No spam, ever. Unsubscribe anytime.