Building a Beat Machine With React

Recently I completed my third hackathon with Mintbean.io. The challenge was to build a Beat Machine in 1 week. The application had to be a front end web application, no backend or server/serverless components were allowed to be used.

You can play with BeatJuice here 👉 : https://lnkd.in/gTE8iU9

The tech stack that was used was #JavaScript #reactjs #HowlerJS #MaterialUI & #FeaturePeek

BeatJuice in action

One of the first things we implemented as a group was a pull request workflow using gh pr create (github CLI):

This was a hugely beneficial workflow to follow as it gave each other a chance to review our code collectively and approve and fix merge conflicts ahead of time.

I also continued learning React and setting state using setInterval. One of the trickier aspects of the project was setting up the playhead and synchronising this with the playback of the music.

Something I would like to improve on is the modularity of code and passing props and state around. A lot of times with a project my intention is to make different components and keep the modular however one file always seems to take most of the load.

Long files! Okay for a hackathon but not following the single responsibility principle well! I want to improve upon this.

Originally I wanted to separate the BeatTracker component (playhead) and the patches however the issue was the counter couldn’t be passed down as props as it was just sticking on 0 and not iterating up. I think that was due to the setInterval logic acting too quickly dependent on the tempo.

//gets index position and assigns that to setSquaresconst playHeadLoop = () => {//make a shallow copy of the patternlet pattern = [...squares]//make a shallow copy of the mutable objectlet position = squares[animCount]//replace the 0 with a 1position = 1;pattern[animCount] = positionsetSquares(pattern)//get the square to animatelet squareToAnimate = document.getElementById(`${animCount}`)//find previousSquarelet previousSquare = getPreviousSquare()//distribute classes as neededpreviousSquare.classList.remove('playead')previousSquare.classList.add('inactive')squareToAnimate.classList.remove('inactive')squareToAnimate.classList.add('playhead')}

I had to move all the components handling the playhead visuals to the app.js out of BeatTracker as well as the ResetSquares() function and GetPreviousSquare().

const resetSquares = () => {setCounter(0)setSquares([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])setPlayHeadArray(squares.map((square, i) => ( <td key={i + squares} id={i} className={square > 0 ? "playhead" : "inactive cycle"}></td> )))}const getPreviousSquare = () => {if(counter === 0){return document.getElementById('15')} else {return document.getElementById(`${counter - 1}`)}}

We had a component setup called useBPM to hold this function in a modular way.

export default function useBPM(bpm) {return (60 * 1000 / 4) / bpm;}

There could have been other solutions to tackle the BPM such as using Howler.js’ inbuilt loop or webAudioAPI but I decided to try controlling the step with stateful logic:

const App = () => {const [isPlaying, setIsPlaying] = useState(false)const [tempo, setTempo] = useState(120);const [volNum, setVolNum] = useState(50)const [squares, setSquares] = useState([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);const [playHeadArray, setPlayHeadArray] = useState([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])

The arrays above worked as a toggle for the playhead squares with a 0 = inactive and a 1=active

let beats = Bpm(tempo)useEffect(() => {if (isPlaying) {const interval = setInterval(() => {resetSquares();playHeadLoop()loop();// each square, counter increments to one (should be using and if else and setCounter here since it is part of state?)counter = counter >= 15 ? 0 : counter + 1// loop creates an array of up to 6 sounds that are then played at the same time
// this
resetSquares();}, beats);return () => clearInterval(interval)}resetSquares();
//any time isPlaying, grid, beats, volNum or counter changes re-render the app via the useEffect
}, [isPlaying, grid, beats, volNum, counter])

This was the underlying logic put into a useEffect to control the player. Originally as stated above I wanted more modularity with the code and to keep the animation and samples seperate however we were running into sync issues so I had to merge the logic in this file. With more time we could refactor the logic out of our app.js.

Folder structure and components

I’d like to draw attention to this line:

counter = counter >= 15 ? 0 : counter + 1

This line in particular is troublesome as React state should not be directly updated inside a component this way. The correct usage is

setState(prevState => ++prevState)

Our useEffect logic would follow this pattern:

useEffect(() => 
{ if (isPlaying)
{ if (counter < 15)
{ setCounter(prevState => ++prevState);
} else
{ setCounter(0);
} } }, [isPlaying]);

Although the combining that with the setInterval like this:

useEffect(() => {if (isPlaying) {const interval = setInterval(() => {resetSquares();playHeadLoop()loop();// each square, counter increments to one (should be using and if else and setCounter here since it is part of state?) if (counter < 15) {
setCounter(prevState => ++prevState);
} else
{ setCounter(0);
}
// loop creates an array of up to 6 sounds that are then played at the same timeresetSquares();}, beats);return () => clearInterval(interval)}resetSquares();}, [isPlaying, grid, beats, volNum, counter])

also made the counter sporadically stick at 0 and not tick up to 16 at the speed of our formula (60 * 1000 / 4) / bpm so for 120 BPM this would be

(60 * 1000 / 4) / 120 = 125ms

I think the reason the counter doesn’t iterate upwards is because the logic may loop quicker than React can update the state hence the output would always be

counter in useEffect: 0counter in useEffect: 0counter in useEffect: 0counter in useEffect: 0

I wasn’t able to find a way to put in the correct logic this would be one improvement I would like to make in refactoring. I think learning about other hooks like useContext, useRef and useMemo would be a workaround for this problem.

This project helped get me more practise with Functions Objects Conditionals, Loops and Arrays in JavaScript with objects and arrays to manage and access in the form of the squares and also whilst building our instrument bank:

export const instruments = [{ name: 'Clap', sound: "./DrumSamples/Claps/Clap1.wav", pattern: {0: false, 1: false, 2: false, 3: false, 4: false, 5: false, 6: false, 7: false, 8: false, 9: false, 10: false, 11: false, 12: false, 13: false, 14: false, 15: false }, color: '#b50000' },{ name: 'Hi-hat (open)', sound: "./DrumSamples/OpenHats/OpenHiHat01.wav", pattern: {0: false, 1: false, 2: false, 3: false, 4: false, 5: false, 6: false, 7: false, 8: false, 9: false, 10: false, 11: false, 12: false, 13: false, 14: false, 15: false }, color: '#bcc200' },{ name: 'Hi-hat (closed)', sound: "./DrumSamples/ClosedHats/HiHat01.wav", pattern: {0: false, 1: false, 2: false, 3: false, 4: false, 5: false, 6: false, 7: false, 8: false, 9: false, 10: false, 11: false, 12: false, 13: false, 14: false, 15: false }, color: '#ff00ff' },{ name: 'Snare 2', sound: "./DrumSamples/AltSnare1/AltSD25.wav", pattern: {0: false, 1: false, 2: false, 3: false, 4: false, 5: false, 6: false, 7: false, 8: false, 9: false, 10: false, 11: false, 12: false, 13: false, 14: false, 15: false }, color: '#00DACD' },{ name: 'Snare 1', sound: "./DrumSamples/MainSnare/Snare1.wav", pattern: {0: false, 1: false, 2: false, 3: false, 4: false, 5: false, 6: false, 7: false, 8: false, 9: false, 10: false, 11: false, 12: false, 13: false, 14: false, 15: false },color: '#ff7700' },{ name: 'Kick', sound: "./DrumSamples/Kicks/KickDrum01.wav", pattern: {0: false, 1: false, 2: false, 3: false, 4: false, 5: false, 6: false, 7: false, 8: false, 9: false, 10: false, 11: false, 12: false, 13: false, 14: false, 15: false }, color: '#00990a' },{ name: 'Bassline', sound: "./BassSamples/HighE-BassNote.wav", pattern: {0: false, 1: false, 2: false, 3: false, 4: false, 5: false, 6: false, 7: false, 8: false, 9: false, 10: false, 11: false, 12: false, 13: false, 14: false, 15: false }, color: 'blue' }]// returns different sound pointer depending on the position of the counter on the gridexport function getBassNote(position) {if(position >= 14) {return "./BassSamples/G-BassNote.wav";}if(position >= 12){return "./BassSamples/A-BassNote.wav";}if(position >= 10){return "./BassSamples/B-BassNote.wav";}if(position >= 8){return "./BassSamples/D-BassNote.wav";}if(position >= 6) {return "./BassSamples/HighE-BassNote.wav";}if(position >= 4){return "./BassSamples/D-BassNote.wav";}if(position >= 2){return "./BassSamples/B-BassNote.wav";}if(position >= 0){return "./BassSamples/HighE-BassNote.wav";}return "./BassSamples/HighE-BassNote.wav";}

I also used materialUI and CSS diving into the inbuilt properties to customise the buttons, sliders and icons.

import { Button, makeStyles } from '@material-ui/core';const useStyles = makeStyles((theme) => ({button: {margin: theme.spacing(1),backgroundColor: '#330704',border: '1px solid',borderColor: '#000','&:hover': {backgroundColor: '#440704',color: '#FFF',border: '1px solid',borderColor: '#FFF'}},}));

MaterialUI provides inbuilt methods accessible via a theme object where you can change properties of the element you want to customise. In this case I am changing the default colour of a button, the outline colour and also changing the colour properties on hover.

MaterialUI button properties — changing button colours on hover

Future Improvements

As well as some of refactors I suggested above future versions of beatJuice could easily implement:

A clear pattern function — that clears the current pattern (without having to refresh)

A waveform visualizer — Howler.JS provides some methods to link into an audio visualiser so it would be cool to see the waveform as sound is playing.

JSON export /import— we already have the object set up to save out the 1s and 0s for our pattern so I think it would be easy to implement this so you could save the pattern.

Audio render — I think this would be another easy thing to accomplish with another npm package to give the user an MP3 file of their sample.

I had such a great time this on this hackathon and it was a pleasure to work with Yimeng Yu and Edward Smith via google meet. please check them both out on Linkedin!

Play with the beatJuice here: https://dashboard.featurepeek.com/peek/k5zh70zl#/

See the repo here: https://github.com/AndrewRLloyd88/beat-machine-hackathon

About the Author

Andrew is a dynamic full-stack developer and passionate learner who is comfortable speaking to clients as well as fellow developers. Eager to use cutting-edge technologies, alongside building long lasting applications and websites.

He is a graduate from Lighthouse Lab’s web development bootcamp, and is a part of the mintbean.io community.

He has learned and implemented various skills including

  • HTML5
  • JavaScript
  • JQuery
  • CSS3
  • MySQL/PostgreSQL
  • Bootstrap
  • MaterialUI
  • React
  • Redux

Prior to attending bootcamp in August 2020 his areas of work have been: administration, lecturing, film production and post-production and customer service across various sectors.

He would love to connect for opportunities and find a way to bring his knowledge and enthusiasm to your projects!

--

--