I'm putting together a drum machine/sequencer and while the main functionality of the sequencer works fine, the audio that I've embedded in each drum cell does have a noticeable lag when the sequencer is first played. It seems to correct itself after the first beat and everything plays as normal, until other sounds are added to the pattern.
I have three rows of table cells, with each row representing a different drum sound. When the user constructs a drum pattern using all the sounds available, the loop eventually seems to go out of sync, with some sounds playing a fraction of a second later than others, but later corrects itself. My main concern is that the playback of the samples is inconsistent.
I've embedded the tags in elements with a preload attribute set to auto.
<table class="pad">
<tbody>
<tr>
<td class="kick sounds">
<audio preload="auto" src="https://raw.githubusercontent.com/wesbos/JavaScript30/master/01%20-%20JavaScript%20Drum%20Kit/sounds/kick.wav"></audio>
</td>
</tr>
</tbody>
</table>
So each there are 8 table cells to each row and each cell has the same format as above (with an element embedded). Admittedly, I can imagine that the way I've structured this is quite inefficient and that using the web audio API would work better, but I've yet to learn APIs. Is there something I can do in JS that can make the playback of these audio samples quicker?
EDIT: Here is the sequencer code. It cycles through each element and and checks if there is a cell selected. If so, that element's audio file is played. If not, it skips over to the next column.
class Sequencer {
playButton = btn;
clearButton = clear;
sounds = Array.from(sounds);
kicks = Array.from(kicks);
hihats = Array.from(hihats);
snares = Array.from(snares);
currentBeatIndexKick = 0;
currentBeatIndexHiHat = 0;
currentBeatIndexSnare = 0;
isPlaying = false;
constructor(msPerBeat) {
this.msPerBeat = msPerBeat;
this.playButton.addEventListener('click', () => this.toggleStartStop())
this.clearButton.addEventListener('click', () => this.clear())
this.sounds.forEach(sound => {
sound.addEventListener('click', e => {
if (((e.target.classList.contains('kick')) || (e.target.classList.contains('hihat')) || (e.target.classList.contains('snare'))) && !e.target.classList.contains('selected')) {
e.target.classList.add('selected');
} else {
e.target.classList.remove('selected');
}
})
})
}
toggleStartStop() {
if (this.isPlaying) {
this.stop();
} else {
this.start();
}
}
clear() {
this.kicks.forEach(kick => {
if (kick.classList.contains('selected')) {
kick.classList.remove('selected');
}
});
hihats.forEach(hihat => {
if (hihat.classList.contains('selected')) {
hihat.classList.remove('selected');
}
});
snares.forEach(snare => {
if (snare.classList.contains('selected')) {
snare.classList.remove('selected');
}
});
this.stop();
console.clear();
}
stop() {
this.isPlaying = false;
this.currentBeatIndexKick = 0;
this.currentBeatIndexHiHat = 0;
this.currentBeatIndexSnare = 0;
this.playButton.innerText = 'Play';
}
start() {
this.isPlaying = true;
this.playCurrentNoteAndSetTimeoutKick() // kicks
this.playCurrentNoteAndSetTimeoutHiHats() // hihats
this.playCurrentNoteAndSetTimeoutSnares() // snares
this.playButton.innerText = 'Stop';
}
playCurrentNoteAndSetTimeoutKick() {
if (this.isPlaying && this.kicks[this.currentBeatIndexKick].classList.contains('selected')) {
this.kicks[this.currentBeatIndexKick].childNodes[1].play();
setTimeout(() => {
this.toNextBeatKicks();
this.playCurrentNoteAndSetTimeoutKick();
}, this.msPerBeat)
}
if (this.isPlaying && !this.kicks[this.currentBeatIndexKick].classList.contains('selected'))
setTimeout(() => {
this.toNextBeatKicks();
this.playCurrentNoteAndSetTimeoutKick();
}, this.msPerBeat)
}
playCurrentNoteAndSetTimeoutHiHats() {
if (this.isPlaying && this.hihats[this.currentBeatIndexHiHat].classList.contains('selected')) {
this.hihats[this.currentBeatIndexHiHat].childNodes[1].play();
setTimeout(() => {
this.toNextBeatHiHats();
this.playCurrentNoteAndSetTimeoutHiHats();
}, this.msPerBeat)
}
if (this.isPlaying && !this.hihats[this.currentBeatIndexHiHat].classList.contains('selected'))
setTimeout(() => {
this.toNextBeatHiHats();
this.playCurrentNoteAndSetTimeoutHiHats();
}, this.msPerBeat)
}
playCurrentNoteAndSetTimeoutSnares() {
if (this.isPlaying && this.snares[this.currentBeatIndexSnare].classList.contains('selected')) {
this.snares[this.currentBeatIndexSnare].childNodes[1].play();
setTimeout(() => {
this.toNextBeatSnares();
this.playCurrentNoteAndSetTimeoutSnares();
}, this.msPerBeat)
}
if (this.isPlaying && !this.snares[this.currentBeatIndexSnare].classList.contains('selected'))
setTimeout(() => {
this.toNextBeatSnares();
this.playCurrentNoteAndSetTimeoutSnares();
}, this.msPerBeat)
}
toNextBeatKicks() {
this.currentBeatIndexKick = ++this.currentBeatIndexKick % this.kicks.length;
}
toNextBeatHiHats() {
this.currentBeatIndexHiHat = ++this.currentBeatIndexHiHat % this.hihats.length;
}
toNextBeatSnares() {
this.currentBeatIndexSnare = ++this.currentBeatIndexSnare % this.snares.length;
}
}
const sequencer = new Sequencer(213)