adding id3 tags html5 filesystem api

551 views Asked by At

I have scenario, where i am building a podcast web applications that allows to listen and store .mp3 podcast file.

I am trying to implement a basic web interface where someone can add the entire id3 tag from the client side (the file will be stored locally on the client side : this client is not like the everyone client but just the guy who gets the raw podcast file without any id3 tag preferably). He then hosts this one page locally adds the correct id3 tags and then copies these .mp3 do a WebDav folder.

I do understand that edits needs to be done at server, but it would really helpful if it all can be done locally on the browser.

Off course there is no ready library to edit files , so i decided to use the HTML5 filesystem api, i.e drop the file into the virtual file system , edit it there are then copy it back to the local system. (for copying there is a ready library FileSaver.js) .

I have been able to do the following: 1) associate the mp3 file dropped at a drop zone to filesystem api using webkitGetAsEntry

2) copy this file then to the file system api.

part of the code looks like:

function onDrop(e) 
{
    e.preventDefault();
    e.stopPropagation();

    var items = e.dataTransfer.items;
    var files = e.dataTransfer.files;

    for (var i = 0, item; item = items[i]; ++i) 
    {
        // Skip this one if we didn't get a file.
        if (item.kind != 'file') {
            continue;
        }

        var entry = item.webkitGetAsEntry();

        if (entry.isFile) 
        {
            // Copy the dropped entry into local filesystem.
            entry.copyTo(cwd, null, function(copiedEntry) {
            //setLoadingTxt({txt: DONE_MSG});
            renderMp3Writer(entry);

My confusion is how do i add the entire id3 tag ? . I am lost at this point as i am not sure about:

1) can we add the entire id3 tag into the file from the fileWriter method? 2) If yes would this be a binary edit or how?? .

Any Help would be useful. tried the below but i am guessing i am wrong.

var blob1 = new Blob(['ID3hTIT2ga'], {type: 'audio/mp3'});
fileWriter.write(blob1);
1

There are 1 answers

6
AudioBubble On BEST ANSWER

You need to build a ID3 buffer, then create a buffer large enough to hold both the ID3 and MP3 file, insert the ID3 and append the MP3 data.

For this you need the ID3 specification and use typed arrays with DataView to build your array.

The ID3 overall structure is defined like this (see link above):

 +-----------------------------+
 |      Header (10 bytes)      |
 +-----------------------------+
 |       Extended Header       |
 | (variable length, OPTIONAL) |
 +-----------------------------+
 |   Frames (variable length)  |
 +-----------------------------+
 |           Padding           |
 | (variable length, OPTIONAL) |
 +-----------------------------+
 | Footer (10 bytes, OPTIONAL) |
 +-----------------------------+

At this point the buffer length is unknown so you need to do this in steps. There are several ways to do this, you can build up small buffer segments for each field, then sum them up into a single buffer. Or you can make a larger buffer you know can hold all the fields you want to include and copy the sum of field from that buffer to the final one.

The latter tends to be simpler and as we're dealing with very small sizes this could be the best way (considering that each fragment in the first approach has their overheads).

So the first thing you need to do is to define the header. The header is defined this way:

ID3v2/file identifier      "ID3"
ID3v2 version              $04 00
ID3v2 flags                %abcd0000  (note: bit-representation)
ID3v2 size             4 * %0xxxxxxx  (note: bit-representation/mask)

ID3 and version are fixed values (other versions exists of course, but lets follow the current).

You can probably ignore most of the flags, if not all, by setting them to 0. But check the docs for your use-case, for example if you want to use extended headers.

Size is defined:

The ID3v2 tag size is stored as a 32 bit synchsafe integer (section 6.2), making a total of 28 effective bits (representing up to 256MB).

The ID3v2 tag size is the sum of the byte length of the extended
header, the padding and the frames after unsynchronisation. If a
footer is present this equals to ('total size' - 20) bytes, otherwise ('total size' - 10) bytes.

An example how you can build your buffer. First define a buffer big enough to hold all the data as well as a DataView:

var id3buffer = new ArrayBuffer(1024),    // 1kb "space"
    view = new DataView(id3buffer);

The DataView defaults to big-endian which is perfect, so all we need to do now is to fill in the data where it should be. We can make a few helper methods to help us move position at the same time as we write. Positions for DataView are byte-bound:

 var pos = 0;    // global start position

function setU8(value) {
    view.setUint8(pos++, value)
}

function setU16(value) {
    view.setUint16(pos, value);
    pos += 2;
}

function setU32(value) {
    view.setUint32(pos, value);
    pos += 4;
}

etc. you can make helpers to write text unicode strings (see TextEncoder for example) and so forth.

To define the header, we can write in the "magic" word ID3. You could convert a string, or since it's only 3 bytes also just write it straight-forward. ID3 = 0x494433 in hex so:

setU8(0x49);     // at pos 0
setU8(0x44);     // at pos 1
setU8(0x33);     // at pos 2

Since we made a wrapper we don't need to worry about the buffer position.

Then write in version (according to spec v.2.4.0 uses 0x0400 not using major version (2)):

setU16(0x0400);  // default is big-endian so this works

Now you can continue with flags and size (see specs).

When the ID3 header is filled up pos will now hold the total length. So make a new buffer for ID3 tag and MP3 buffer:

var mp3 = new ArrayBuffer(pos + mp3Buffer.byteLength),
    view8 = new Uint8Array(mp3);

The view8 view will allow us to do a simple copy to destination:

// create a segment from the tag buffer that will fit target:
var segment = new Uint8Array(view.buffer, 0, n); // replace n with actual length
view8.set(segment, 0);
view8.set(mp3buffer, pos);

If everything went OK you now have a MP3 with a ID3 tag (remember to check for existing ID3s - you need to scan to to end).

You can now send the ArrayBuffer to server, or convert to Blob for IndexedDB, or to an Object-URL if you want to present a link for download (none shown here as answer is becoming out-of-scope).

This should be enough to get you started - as said, you need to study the specs. If you're not familiar with typed array, check those out as well.

Also see the site for other resources (frames etc.).

Sync-safe values

"MP3" files uses frames which starts with 11 bits, all set to 1. If the size field of the header happen to contain 11 bits set to 1, the decoder could mistakenly interpret it as sound data. To avoid this the concept of sync-safe integers are used making sure that each byte's MSB (most signicant bit, bit 7) always is set to 0. The bit is moved to the left, the next byte is shifted one bit, for ID3 tag 4 times (hence the 4x %01111111).

Here is how to encode and decode sync-safe integers using JavaScript (from Wikipedia C/C++ source):

// test values
var value = 0xfffffff,
    sync = intToSyncsafe(value);
document.write("<pre>Original size: 0x" + value.toString(16) + "<br>");
document.write("Synch-safe   : 0x" + sync.toString(16) + "<br>");
document.write("Decoded value: 0x" + syncsafeToInt(sync).toString(16) + "</pre>");


function intToSyncsafe(value) {
    var out, mask = 0x7f;
    while(mask ^ 0x7fffffff) {
        out = value & ~mask;
        out <<= 1;
        out |= value & mask;
        mask = ((mask + 1) << 8) - 1;
        value = out;
    }
    return out
}

function syncsafeToInt(value) {
    var out = 0, mask = 0x7F000000;
    while (mask) {
        out >>= 1;
        out |= value & mask;
        mask >>= 8;
    }
    return out;
}

The sync-safe value would show the bits like: &b01111111011111110111111101111111 for the example value used in the demo above.