How to Build a Beat Sequencer with Howler.js
This article guide demonstrates how to build a web-based audio beat sequencer using HTML, JavaScript, and Howler.js. You will learn how to load audio samples, build an interactive step-sequencer grid in the browser, and implement a timing loop to play back your custom drum beats in real-time.
1. Project Setup and Howler.js Integration
To begin, you need to create a basic HTML structure and link the Howler.js library via CDN. Howler.js simplifies web audio playback, handling browser compatibility and audio loading behind the scenes.
Create an index.html file with the following markup:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Beat Sequencer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/howler/2.2.3/howler.min.js"></script>
<style>
body { font-family: Arial, sans-serif; background: #121212; color: #fff; text-align: center; }
.sequencer { display: inline-block; margin-top: 50px; }
.row { display: flex; align-items: center; margin: 10px 0; }
.label { width: 80px; text-align: left; font-weight: bold; }
.step { width: 40px; height: 40px; margin: 2px; background: #333; border: 1px solid #555; cursor: pointer; }
.step.active { background: #00ff88; }
.step.playing { border-color: #ff007f; }
.controls { margin-top: 20px; }
button { padding: 10px 20px; font-size: 16px; cursor: pointer; background: #00ff88; border: none; color: #000; font-weight: bold; }
</style>
</head>
<body>
<h1>Simple Beat Sequencer</h1>
<div class="sequencer" id="sequencer"></div>
<div class="controls">
<button id="playBtn">Play</button>
</div>
<script src="app.js"></script>
</body>
</html>2. Loading Audio Samples
Create your JavaScript file named app.js. First,
initialize the audio samples using the Howl class. You can
use standard MP3 or WAV files for your drum sounds (Kick, Snare, and
Hi-Hat).
// Initialize sounds using Howler.js
const kick = new Howl({ src: ['https://actions.google.com/sounds/v1/impacts/kick_drum_sub_bass.ogg'] });
const snare = new Howl({ src: ['https://actions.google.com/sounds/v1/percussions/snare_drum.ogg'] });
const hat = new Howl({ src: ['https://actions.google.com/sounds/v1/percussions/closed_hi_hat.ogg'] });
const tracks = [
{ name: 'Kick', sound: kick, steps: Array(8).fill(false) },
{ name: 'Snare', sound: snare, steps: Array(8).fill(false) },
{ name: 'Hat', sound: hat, steps: Array(8).fill(false) }
];3. Rendering the Sequencer Grid
Generate the interactive interface dynamically using JavaScript. Each track will have 8 step buttons that users can click to toggle the sound on or off for that specific beat count.
const sequencerDiv = document.getElementById('sequencer');
// Render grid
tracks.forEach((track, trackIndex) => {
const row = document.createElement('div');
row.classList.add('row');
const label = document.createElement('div');
label.classList.add('label');
label.innerText = track.name;
row.appendChild(label);
for (let stepIndex = 0; stepIndex < 8; stepIndex++) {
const stepBtn = document.createElement('button');
stepBtn.classList.add('step');
stepBtn.dataset.track = trackIndex;
stepBtn.dataset.step = stepIndex;
stepBtn.addEventListener('click', () => {
track.steps[stepIndex] = !track.steps[stepIndex];
stepBtn.classList.toggle('active');
});
row.appendChild(stepBtn);
}
sequencerDiv.appendChild(row);
});4. Creating the Playback Timing Loop
To play the sounds in sequence, we calculate the time interval based on a Beats Per Minute (BPM) value and use standard JavaScript timing functions to cycle through each column (step) of the grid.
let currentStep = 0;
let isPlaying = false;
let intervalId = null;
const bpm = 120;
const intervalTime = (60000 / bpm) / 2; // Eighth notes at 120 BPM
const playBtn = document.getElementById('playBtn');
function playStep() {
// Remove playing visual indicator from all steps
document.querySelectorAll('.step').forEach(btn => btn.classList.remove('playing'));
// Play active sounds for the current step and highlight the active column
tracks.forEach((track, trackIndex) => {
const stepBtn = document.querySelector(`.step[data-track="${trackIndex}"][data-step="${currentStep}"]`);
stepBtn.classList.add('playing');
if (track.steps[currentStep]) {
track.sound.play();
}
});
// Advance to the next step, loop back to start after step 7
currentStep = (currentStep + 1) % 8;
}
playBtn.addEventListener('click', () => {
if (isPlaying) {
clearInterval(intervalId);
playBtn.innerText = 'Play';
isPlaying = false;
currentStep = 0;
document.querySelectorAll('.step').forEach(btn => btn.classList.remove('playing'));
} else {
playStep(); // Play first step immediately
intervalId = setInterval(playStep, intervalTime);
playBtn.innerText = 'Stop';
isPlaying = true;
}
});