Peter Morris
Peter Morris

Reputation: 23224

Trigger JavaScript events at specific points in an audio

I am playing an ogg file using an Audio object in JavaScript. I am trying to synchronise visual events with the position of the audio.

audio.ontimeupdate seems to trigger with a specific regularity. Is there a way I can instead set time markers and have an event fired as close as possible to the exact moment that the audio plays that point in the sample.

        let audio = new Audio(`audio/${filename}`);

        let timings = [1, 1.1, 1.2, 1.3, 1.4, 1.5, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5];
        audio.ontimeupdate = function () {
            let time = audio.currentTime;
            while (timings.length > 0 && timings[0] <= time) {
                console.log(`Waiting for ${timings[0]}, audio time = ${time} : late by ${time - timings[0]}`);
                timings.splice(0, 1);
            }
        }

The output here is just not close enough to the subscription points. One is late by a quarter of a second.

Upvotes: 0

Views: 357

Answers (1)

Peter Morris
Peter Morris

Reputation: 23224

This is what I did. I set a timeout for the first event time and then start to play the audio.

When the event is triggered I check if it is too soon (delay playing the audio) and if it is then I set another timeout for nextEventTime - currentTime.

When the event is triggered and it is not too soon, then I dequeue event times until the next value is in the future. This is giving me a discrepancy of only about 20 milliseconds, which is definitely good enough!

class AudioClip {
    constructor(url, eventTimings) {
        this._audio = new Audio(url);
        this._filename = filename;
        this._eventTimings = eventTimings || [];
        this._timeoutId = null;
    }

    play() {
        if (!this._audio.paused) {
            return;
        }
        this._setTimeoutForNextEvent();
        this._audio.play();
    }

    pause() {
        if (this._audio.paused) {
            return;
        }
        this._clearTimeout();
        this._audio.pause();
    }

    dispose() {
        this.pause();
    }

    _setTimeoutForNextEvent() {
        let _this = this;

        this._clearTimeout(this);
        if (this._eventTimings.length > 0) {
            let delayMs = this._eventTimings[0] - (this._audio.currentTime * 1000);
            this._timeoutId = setTimeout(() => { _this._triggerCurrentEvent(); }, delayMs);
        }
    }

    _triggerCurrentEvent() {
        this._clearTimeout();

        // If we are too early, wait
        let nextEventTimeSeconds = this._eventTimings[0] / 1000;
        let audioTime = this._audio.currentTime;
        if (audioTime < nextEventTimeSeconds) {
            this._setTimeoutForNextEvent();
            return;
        }

        // Otherwise, trigger the event
        while (this._eventTimings.length > 0 && this._eventTimings[0] <= audioTime) {
            console.log(`At ${this._audio.currentTime}: Event = ${this._eventTimings[0]}`);
            this._eventTimings.splice(0, 1);
        }
        this._setTimeoutForNextEvent(this);
    }

    _clearTimeout() {
        if (this._timeoutId) {
            clearTimeout(this._timeoutId);
            this._timeoutId = null;
        }
    }
}

Upvotes: 1

Related Questions