Search code examples
javascripthtmlaudiohtml5-audioweb-audio-api

Create Seamless Loop of Audio - Web


I want to create a seamless loop of an audio file. But in all approaches I used so far, there was a noticeable gap between end & start.


This is what I tried so far:

First approach was to use the audio in the HTML and it loops but there is still a noticeable delay when going from the end of the track to the beginning.

<audio loop autoplay>
    <source src="audio.mp3" type="audio/mpeg">
<audio>

Then I tried it from JavaScript with the same result:

let myAudio = new Audio(file);
myAudio.loop = true; 
myAudio.play();

After that I tried this (according to this answer)

myAudio.addEventListener(
    'timeupdate',
    function() {
        var buffer = .44;
        if (this.currentTime > this.duration - buffer) {
            this.currentTime = 0;
            this.play();
        }
     },
     false
);

I played around with the buffer but I only got it to reduce the gap but not leave it out entirely.

I turned to the library SeamlessLoop (GitHub) and got it to work to loop seamlessly in Chromium browsers (but not in the latest Safari. Didn't test in other browsers). Code I used for that:

let loop = new SeamlessLoop();
// My File is 58 Seconds long. Btw there aren't any gaps in the file.
loop.addUri(file, 58000, 'sound1');
loop.callback(soundsLoaded);
function soundsLoaded() {
    let n = 1;
    loop.start('sound' + n);
}

EDIT: I tried another approach: Looping it trough two different audio elements:

var current_player = "a";
var player_a = document.createElement("audio");
var player_b = document.createElement("audio");

player_a.src = "sounds/back_music.ogg";
player_b.src = player_a.src;

function loopIt(){
    var player = null;

    if(current_player == "a"){
        player = player_b;
        current_player = "b";
    }
    else{
        player = player_a;
        current_player = "a";
    }

    player.play();

    /*
        3104.897 is the length of the audio clip in milliseconds.
        Received from player.duration. 
        This is a different file than the first one
    */
    setTimeout(loopIt, 3104.897); 
}

loopIt();

But as milliseconds in browsers are not consistent or granular enough this doesn't work too well but it does work much better than the normal "loop" property of the audio.


Can anyone guide me into the right direction to loop the audio seamlessly?


Solution

  • You can use the Web Audio API instead. There are a couple of caveats with this, but it will allow you to loop accurately down to the single sample level.

    The caveats are that you have to load the entire file into memory. This may not be practical with large files. If the files are only a few seconds it should however not be any problem.

    The second is that you have to write control buttons manually (if needed) as the API has a low-level approach. This means play, pause/stop, mute, volume etc. Scanning and possibly pausing can be a challenge of their own.

    And lastly, not all browsers support Web Audio API - in this case you will have to fallback to the regular Audio API or even Flash, but if your target is modern browsers this should not be a major problem nowadays.

    Example

    This will load a 4 bar drum-loop and play without any gap when looped. The main steps are:

    • It loads the audio from a CORS enabled source (this is important, either use the same domain as your page or set up the external server to allow for cross-origin usage as Dropbox does for us in this example).
    • AudioContext then decodes the loaded file
    • The decoded file is used for the source node
    • The source node is connected to an output
    • Looping is enabled and the buffer is played from memory.

    var actx = new (AudioContext || webkitAudioContext)(),
        src = "https://dl.dropboxusercontent.com/s/fdcf2lwsa748qav/drum44.wav",
        audioData, srcNode;  // global so we can access them from handlers
    
    // Load some audio (CORS need to be allowed or we won't be able to decode the data)
    fetch(src, {mode: "cors"}).then(function(resp) {return resp.arrayBuffer()}).then(decode);
    
    // Decode the audio file, then start the show
    function decode(buffer) {
      actx.decodeAudioData(buffer, playLoop);
    }
    
    // Sets up a new source node as needed as stopping will render current invalid
    function playLoop(abuffer) {
      if (!audioData) audioData = abuffer;  // create a reference for control buttons
      srcNode = actx.createBufferSource();  // create audio source
      srcNode.buffer = abuffer;             // use decoded buffer
      srcNode.connect(actx.destination);    // create output
      srcNode.loop = true;                  // takes care of perfect looping
      srcNode.start();                      // play...
    }
    
    // Simple example control
    document.querySelector("button").onclick = function() {
      if (srcNode) {
        srcNode.stop();
        srcNode = null;   
        this.innerText = "Play";
      } else {
        playLoop(audioData);
        this.innerText = "Stop";
      }
    };
    <button>Stop</button>