0

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)
  • Do you want to load all the sound files before user run his hands on drum machine? – Vinay Aug 09 '20 at 05:28
  • If that will make the playback of the samples seamless without any lag, then yes? – Aaron Sawatsky Aug 09 '20 at 05:30
  • Preloading all the files is quite expensive if you have multiple files to work with. How many files do you have? – Atlante Avila Aug 09 '20 at 05:39
  • Yes I am sure this should solve that lag issue. Try [this](https://stackoverflow.com/a/31351186/6160662) preloading trick and let me know – Vinay Aug 09 '20 at 05:42
  • Like an obnoxious amount. Right now I'm currently working with 24. 8 files for each row, of which there are 3. – Aaron Sawatsky Aug 09 '20 at 05:43
  • Don't use – Kaiido Aug 09 '20 at 05:46

1 Answers1

0

I think what you might have to do is to play the audio file programmatically. It's kind of hard to understand how your sequencer works without giving us more access to your code, the way I would approach this is by programmatically playing the audio file once the user has selected the sequence. I would assign a variable to play once the steps are selected on your sequencer.

var audio = new Audio('audio_file.mp3');
audio.play();

audio.play would be called on the sequence. Not sure if this helps but that's part of the api I think you were referring to.

Atlante Avila
  • 1,340
  • 2
  • 16
  • 37
  • I've gone ahead and edited the OP to show sequencer code. I am completely unfamiliar with APIs right now so I don't believe what you suggested would work in my particular situation. I appreciate your comment though! – Aaron Sawatsky Aug 09 '20 at 05:43