Playback MP3-Loop (gapless)

This is one of the most frequently asked question in my inbox.

Is it possible to loop MP3 without gaps?
Yes, but to answer the question I like to explain, why gap-less MP3 looping was actually not possible before Flash10 (Except for Flash-IDE encoded MP3 or ugly hacks).

Encoding Audio
Every audio encoder works by subdivided the audio stream into frames. Each time-frame contains information about the current frequency bands in use. You can easily follow, why an algorithm needs at least a couple of amplitudes (called a window) to analyze the spectrum. A single amplitude does not have any information about frequencies. Imagine a snapshot of a speakers membrane. There is no way to say anything about its velocity, not even its sign.

MP3 loss-less
MP3 also encodes audio with a certain amount of samples each frame. The length of a frame however is not free to choose. That is why encoder add more data (encoder delay) to the audio stream then actually necessary to ensure minimal frame length (depending on algorithm). Unfortunately the engineers of MP3 back then forgot to add the information how much silence they added to the encoded file. To make it absolutely clear: This information is lost for all times. You may write algorithms to estimate the delay, but it is not possible to recover exactly.

Btw: FlashIDE encoded MP3 loop gap-less, cause the SWF contains the original amount.

Does Lame help?
Lame offers embedding this missing information in a hidden MP3 frame. But to get this information you have to load the MP3 as a flash.utils.ByteArray, parse all MP3 frame-headers (no decoding necessary) and do some calculations. But afterward you need to load it again into a flash.media.Sound object to make use of the extract method. So better store this information of each loop in your database or simply somewhere in the Actionscript.

Solution
Once you have access to the original amount of samples you can use the Flash10 Playback API to playback your loop and wrap it seamlessly on low-level. You only have to take the encoder delay into account. If you extract from the very beginning you read nothing else than zero amplitudes. Check out the magic-number provided in the source code. This number works perfectly with Lame encoded MP3. This number is not Magic of course. It simply combines the amount of samples LAME added in front of the MP3 – plus – the amount of samples the Flash decoder thinks it were added. You can actually loop waveforms now.

Example

Get Adobe Flash player

Track Void Panic | wav | mp3 | 124417 samples | Sourcecode

Still to complicated?
Vote for native Vorbis support!

61 thoughts on “Playback MP3-Loop (gapless)”

  1. @ariel: This is not a bug.
    MAGIC_DELAY is found by comparison of the original WAV and the Flashplayer(!) decoded audio.
    However the Flash Sound API on other platforms has different properties since the native environment and most likely the underlying code has nothing in common. You have to find another “MAGIC_DELAY”.

  2. Another question, if MP3 files pose a problem, then can we extract out the raw sound data (at runtime) and play and loop that? Could we then save that raw data to file and load it in in the future instead of the MP3?

  3. I think AS3SWF can get the samples from an MP3 via the tags meta data.

    tagDefineSound = TagDefineSound.createWithMP3(1, mp3);

    trace(tagDefineSound.toString())

    [14:DefineSound] SoundID: 1, Format: MP3, Rate: 44kHz, Size: 16bit, Type: stereo, Samples: 428544

    It also seems it can show the type (stereo or mono), rate, size and format. Thank you Claus.

    AS3SWF
    https://github.com/claus/as3swf/wiki/Play-MP3-directly-from-ByteArray

    PS I’ve read it may be possible to do correctly looping when the cue points are embedded in SWF (via Flash IDE). If AS3SWF is able to embed a mp3 at runtime it may be possible to embed the cue times with it (and thus have gapless loops with standard Sound class???).

  4. IT WORKED!!! Bart’s solution worked perfectly for me. I used the tool he suggested to convert my WAV files into MP3s and then it worked. The MP3s I generated from garageband did not work, even though I exported a clip that did not have any gaps. So, use this tool (as Bart suggested) and make sure you install LAME and add lame’s install folder to your windows PATH variable list for the easiest time with the GUI version. Here are all the URLs you’ll need to do these things:

    http://www.compuphase.com/mp3/mp3loops.htm

    http://lame1.buanzo.com.ar/#lamewindl

    http://geekswithblogs.net/renso/archive/2009/10/21/how-to-set-the-windows-path-in-windows-7.aspx

  5. Another cheesy solution is to “trim” the bytearray. (similar to classic string “trim” a-la PHP).

    The trick is to check if target.readFloat() is > +/-0.0099

    I’m guessing that when you read the bytes for the id3v1 + id3v2 and any other non-sample data, the “readFloat” value is way out of range.

    From purely analyzing the readFloat values, it looks like a lot of 0’s and values like +/-0.000091552734375

    But sample data is generally +/-0.0900.

    Again, this is rather cheesy, but seems to work ok without having to load WAVs or embed in SWFs, external MP3s are nice.

    Here’s some psuedo code to “trim”:

    // collect = a vector containing readFloat()’s from the mp3 extracted to bytearray
    var b:int = 0;
    var f:int = 0;
    var runaway:int = 0;
    var runawayLen:int = collect.length;

    while (!found){

    if( inrange(collect[f]) ){
    foundFront = true;
    }
    if( inrange(collect[b]) ){
    foundBack = true;
    }

    if(foundFront == false){
    countFront++;
    }
    if(foundBack == false){
    countBack++;
    }

    if((foundFront && foundBack) || runaway > runawayLen){
    found = true;
    }
    b–;
    f++;
    runaway++;
    }

    function inrange(theVal):Boolean{
    if( theVal > 0){
    if(theVal > 0.0099){
    return true;
    } else {
    return false;
    }
    } else if( theVal < 0){
    if(theVal < -0.0099){
    return true;
    } else {
    return false;
    }
    } else {
    return false;
    }
    }

    var rawSampleData:Array = []; // probably want to use a vector.
    var count:int = 0;
    for(var i=countFront;i<collected.length-countBack; i++){
    rawSampleData[count] = collected[i];
    count++
    }

Comments are closed.