Drum Machine
Intro
This is the third project in the freeCodeCamp Front End Development Libraries Certification. The objective is to build a drum machine. It will have nine drum pads. When the user click a drum pad or presses the key shown on it, it should play the audio clip related to that pad and show the name of the sound on the display of the drum machine. The suggested method is to use a front end library like React. Read more about the project at Build a Drum Machine
Check out the end product at https://drum-machine.projects.yasakdogra.com
Planning
We will use React, Redux and Tailwind CSS for this project. We will also use Font Awesome for power button icon. Check out how to setup a basic project at: React with Redux, Tailwind and More
Thinking in React components, we can make the UI with the main DrumMachine
component, which will have a Drum
component and a Controls
component. The Drum
component will have several instances of DrumPad
component. The Controls
component will have a Power
component and a Display
component. The Power
component will use a FontAwesome
component for the power button.
We can split the state into drum state and power state. The drum state will keep track of the key presses and audio clip played. The power state will keep track of power on/off condition.
State
Power State
Let’ start with the easy part. We just need to use a boolean to save the power state and create an action to toggle the power.
Create the file src/state/powerState.ts and import createSlice
import { createSlice } from "@reduxjs/toolkit";
Create the interface and initial state for power
interface PowerState {
on: boolean
}
const initialState: PowerState = {
on: true
}
Create and export action creators, action reminders and reducer
const powerState = createSlice({
name: 'power',
initialState,
reducers: {
togglePower(state) {
state.on = !state.on
}
}
})
export const { togglePower } = powerState.actions
export default powerState.reducer
Drum State
For the drum state, we need to keep track of currently playing pads so we can highlight the drum pads as long as the audio associated with them is playing. We can activate multiple pads at the same time, so we will need to keep track of each of them individually. We also need to keep track of the last played track to show it on the display.
Create the file src/state/drumState.ts. This time we will also import PayloadAction so we can send the id of the drum pad with every play action
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
Create interface and set an initial state
interface State {
playing: { [key: string]: boolean },
latest: string
}
const initialState: State = {
playing: {
'Heater-1': false,
'Heater-2': false,
'Heater-3': false,
'Heater-4': false,
'Clap': false,
'Open-HH': false,
'Kick-n-Hat': false,
'Kick': false,
'Closed-HH': false
},
latest: ''
}
We need two actions for the drum state. setActive
will set the drum pad state to playing on pad click or key press. setInactive
will set the drum pad state to inactive once the audio clip has finished playing.
Create and export the action type, action creators and reducer
const drumState = createSlice({
name: 'drum',
initialState,
reducers: {
setActive(state, action: PayloadAction<{id: string}>) {
state.playing[action.payload.id] = true
state.latest = action.payload.id
},
setInactive(state, action: PayloadAction<{id: string}>) {
state.playing[action.payload.id] = false
},
}
})
export const { setActive, setInactive } = drumState.actions
export default drumState.reducer
State Store
Now we just create a store to and use both the reducers we created above. We also export RootState
type and AppDispatch
type.
Create src/state/store.ts
import { configureStore } from "@reduxjs/toolkit"
import drumReducer from "./drumState"
import powerReducer from "./powerState"
export const store = configureStore({
reducer: {
power: powerReducer,
drum: drumReducer
}
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Components
For this project, let’s use bottom-up approach to create the components. We will go with this sequence
Power component
Create a file src/components/Power.tsx with imports for Redux, state store, and font awesome
import { useDispatch, useSelector } from "react-redux"
import { AppDispatch, RootState } from "../state/store"
import { togglePower } from "../state/powerState"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faPowerOff } from "@fortawesome/free-solid-svg-icons"
Create the Power
component. Inside, get power state using useSelector and state.power.on
. Set the font awesome power icon to different colors depending on power state. On click, dispatch an action to switch power state using togglePower
action creator.
export default function Power() {
const power = useSelector((state: RootState) => state.power.on)
const dispatch = useDispatch<AppDispatch>()
const handlePowerButton = () => {
dispatch(togglePower())
};
return (
<div id="power" className="flex flex-col items-center w-4/5">
<p className="text-2xl">Power</p>
<FontAwesomeIcon className={`${power ? 'text-amber-600' : 'text-amber-900'}`} size="3x" icon={faPowerOff} onClick={handlePowerButton} />
</div>
)
}
Display component
Create src/components/Display.tsx with imports for Redux and state store
import { useSelector } from "react-redux"
import { RootState } from "../state/store"
Create the Display
component. Inside we will use state.power.on
to activate or deactivate the display, state.drum.latest
to set the display text and state.drum.playing[latest]
to set the color of the display text
export default function Display() {
const power = useSelector((state: RootState) => state.power.on)
const latest = useSelector((state: RootState) => state.drum.latest)
const active = useSelector((state: RootState) => state.drum.playing[latest])
return (
<div className="w-full flex flex-wrap justify-center items-center">
<div className="w-3/4"><p className="w-fit m-auto text-2xl">Display</p></div>
<div className="w-3/4 border-2 bg-zinc-400 border-orange-800 h-12 flex flex-wrap items-center"><p id="display" className={`w-fit m-auto text-2xl font-bold ${active ? 'text-orange-400' : 'text-orange-900'}`}>{power ? latest: ""}</p></div>
</div>
)
}
Drumpad component
We will render the Drumpad
component multiple times in our Drum
component with different text and audio attached. We can create a generic component and pass it the fields that are different for each pad. For handling clicks, we will pass it a click handler function from the Drum
component.
Create a file src/components/Drumpad.tsx and add imports
import { useSelector } from "react-redux";
import { RootState } from "../state/store";
Create a type for props. We will send it an id
string, a text
string to display on the drum pad, audio string to set the URL of the audio element and onActivate
function to use on mouse click
type DrumPadProps = {
id: string;
text: string;
audio: string;
onActivate: (id: string, text: string) => void;
};
Create the functional component. Inside the function, we will use state.power.on
to check if the drum pad should be on and state.drum.playing[id]
to check if the current pad is playing. We will translate the pad and its shadow and use CSS transition to animate pad press
export default function DrumPad ({ id, text, audio, onActivate }: DrumPadProps) {
const power = useSelector((state: RootState) => state.power.on);
const playing = useSelector((state: RootState) => state.drum.playing[id]);
return (
<div
id={id}
className={`drum-pad h-16 w-16 flex items-center justify-center
select-none font-bold cursor-pointer rounded-md text-3xl transition-all duration-75
${ playing ?
"bg-stone-400 text-orange-400 ring-2 ring-orange-400 shadow-[0_0px_0px_0px_rgba(0,0,0,0.3)] translate-y-1"
: power? "bg-stone-400 text-amber-700 ring-2 ring-amber-700 shadow-[0_5px_0px_0px_rgba(0,0,0,0.3)]"
: "bg-stone-400 text-amber-900 ring-2 ring-amber-900 shadow-[0_5px_0px_0px_rgba(0,0,0,0.3)]"
}`}
onClick={() => onActivate(id, text)}
>
{text}
<audio id={text} className="clip" src={audio} />
</div>
);
};
Controls component
The Controls
component is just a composition of the Power
and Display
component.
Create src/components/Controls.tsx and import Power
and Display
import Display from "./Display"
import Power from "./Power"
Create the Controls
component. We will wrap these children in a flex div.
export default function Controls() {
return (
<div id="controls" className="flex flex-wrap items-center justify-center">
<Power />
<Display />
</div>
)
}
Drum component
The Drum
component has a bit more going on compared to the Control
component. We will need to handle keyboard key presses, create a function to pass to drum pads for handling clicks and handle the power on and off as well. Since there are multiple DrumPad
components, we can create a single function in the parent Drum
to handle the clicks.
Create src/components/Drum.tsx and add imports. We will need useEffect
hook to handle power on and off.
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "../state/store";
import { setActive, setInactive } from "../state/drumState";
import DrumPad from "./DrumPad";
Create a type that holds fields for pads
type PAD = {
key: string;
id: string;
src: string;
};
Use the type above and create a const that has all our drum pads
const PADS: PAD[] = [
{
key: "Q",
id: "Heater-1",
src: "/assets/audio/Heater-1.mp3",
},
{
key: "W",
id: "Heater-2",
src: "/assets/audio/Heater-2.mp3",
},
{
key: "E",
id: "Heater-3",
src: "/assets/audio/Heater-3.mp3",
},
{
key: "A",
id: "Heater-4",
src: "/assets/audio/Heater-4_1.mp3",
},
{
key: "S",
id: "Clap",
src: "/assets/audio/Heater-6.mp3",
},
{
key: "D",
id: "Open-HH",
src: "/assets/audio/Dsc_Oh.mp3",
},
{
key: "Z",
id: "Kick-n'-Hat",
src: "/assets/audio/Kick_n_Hat.mp3",
},
{
key: "X",
id: "Kick",
src: "/assets/audio/RP4_KICK_1.mp3",
},
{
key: "C",
id: "Closed-HH",
src: "/assets/audio/Cev_H2.mp3",
},
];
Now we are going to create the functional component.
Let’s start by creating a function playSound
to handle the clicks. This function will dispatch setActive
action with the id
of the clicked drum pad. Then it will find the audio element inside the clicked drum pad and play the audio. When the audio clip finishes, it will dispatch setInactive
action with the id
of the clicked drum pad.
We will over the PADS
constant we created and add a DrumPad
for each element and pass in fields and the playSound
function as props
export default function Drum() {
const power = useSelector((state: RootState) => state.power.on);
const dispatch = useDispatch<AppDispatch>();
const playSound = (id: string, text: string) => {
if (!power) return;
dispatch(setActive({ id }));
const ae = document.getElementById(text) as HTMLAudioElement;
ae.currentTime = 0;
ae.onended = () => dispatch(setInactive({ id }));
ae.play();
};
return (
<div id="drum" className="grid grid-cols-3 gap-4 w-72 place-items-stretch">
{PADS.map((pad) => (
<DrumPad key={pad.key} id={pad.id} text={pad.key} audio={pad.src} onActivate={playSound} />
))}
</div>
);
};
We also need to handle key presses to activate the drum pads. We can do that by adding a keydown
event handler on the window. We will do that inside useEffect
which will be run at the first render and also when the power state changes. Let’s add that to our functional component
export default function Drum() {
const power = useSelector((state: RootState) => state.power.on);
const dispatch = useDispatch<AppDispatch>();
const playSound = (id: string, text: string) => {
if (!power) return;
dispatch(setActive({ id }));
const ae = document.getElementById(text) as HTMLAudioElement;
ae.currentTime = 0;
ae.onended = () => dispatch(setInactive({ id }));
ae.play();
};
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const key = e.key.toUpperCase();
const pad = PADS.find((pad) => pad.key === key);
if (pad) {
playSound(pad.id, pad.key);
}
};
if (power) {
window.addEventListener("keydown", handleKeyDown);
}
else {
window.removeEventListener("keydown", handleKeyDown);
}
return () => window.removeEventListener("keydown", handleKeyDown);
}, [power]);
return (
<div id="drum" className="grid grid-cols-3 gap-4 w-72 place-items-stretch">
{PADS.map((pad) => (
<DrumPad key={pad.key} id={pad.id} text={pad.key} audio={pad.src} onActivate={playSound} />
))}
</div>
);
};
Drum Machine component
The drum machine is a simple wrapper around the Drum
and Controls
components. We will use div with grid view and set number of columns to one on small screens, so the drum and controls stack vertically. We will set columns to two on wider devices. Throw in a little gap and round the borders and we are done!
Create src/components/DrumMachine.tsx
import Controls from "./Controls";
import Drum from "./Drum";
export default function DrumMachine() {
return (
<div id="drum-machine" className="grid grid-cols-1 md:grid-cols-2 gap-6 bg-gray-300 p-8 rounded-2xl">
<Drum />
<Controls />
</div>
);
};
App component
Finally, we will add the drum machine to our App
component
import "./App.css";
import DrumMachine from "./components/DrumMachine";
function App() {
return (
<DrumMachine />
);
}
export default App;
Thank you for reading. You can also check out my other projects for this series below.