Accessing id3 v2.4 tags using the built-in capabilities of Chrome's Javascript FileReader and DataView

After discovering the solution provided by ebidel, individuals can extract id3v1 tags using jDataView:

document.querySelector('input[type="file"]').onchange = function (e) {
    var reader = new FileReader();

    reader.onload = function (e) {
        var dv = new jDataView(this.result);

        // The ID3 "TAG" section starts at byte -128 from the end of file.
        // Refer to http://en.wikipedia.org/wiki/ID3 for details
        if (dv.getString(3, dv.byteLength - 128) == 'TAG') {
            var title = dv.getString(30, dv.tell());
            var artist = dv.getString(30, dv.tell());
            var album = dv.getString(30, dv.tell());
            var year = dv.getString(4, dv.tell());
        } else {
            // No ID3v1 data found.
        }
    };

    reader.readAsArrayBuffer(this.files[0]);
};

We learn that Chrome and other browsers now support DataView (my focus is on Chrome). I am interested to know:

  1. How to extract tags utilizing native DataView.
  2. Extracting id3 v2.4 tags (including APIC image 'coverart')

My goal is to gain insight into working with binary files and understanding concepts like little endian and big endian. To clarify, I seek guidance on extracting a specific tag - let's say the title, the TIT2 tag as an example that will help me grasp the process of extracting other tags too:

function readID3() {
    //https://developer.mozilla.org/en-US/docs/Web/API/DataView
    //and the position
    //http://id3.org/id3v2.4.0-frames
    //var id3={};
    //id3.TIT2=new DataView(this.result,?offset?,?length?)

    /*
     ?
     var a=new DataView(this.result);
     console.dir(String.fromCharCode(a.getUint8(0)));
     ?
    */
}
function readFile() {
    var a = new FileReader();
    a.onload = readID3;
    a.readAsArrayBuffer(this.files[0]);
}
fileBox.addEventListener('change', readFile, false);

This JSFiddle link provides a demonstration.


UPDATE

http://jsfiddle.net/s492L/3/

I have incorporated the use of getString to read the initial line and check for the presence of an ID3 tag. My next step involves locating the position of the first tag (TIT2), determining its variable length string, and verifying it belongs to version 2.4.

//Header
//ID3v2/file identifier    "ID3"
//ID3v2 version            $04 00
//ID3v2 flags         (%ab000000 in v2.2, %abc00000 in v2.3, %abcd0000 in v2.4.x)
//ID3v2 size                 4 * %0xxxxxxx

Potential external resources to explore:

https://developer.mozilla.org/en-US/docs/Web/API/DataView

https://github.com/aadsm/JavaScript-ID3-Reader

Currently, I am utilizing the PHP getid3 library...

Answer №1

If you're looking to extract ID3 tags from audio files, check out the ID3 parser on GitHub.

For a demonstration of how to use it, take a look at this updated fiddle that logs the tags object in the console

To make use of the id3.js library, simply include it in your code like so:

function readFile(){
   id3(this.files[0], function(err, tags) {
       console.log(tags);
   })
}
document.getElementsByTagName('input')[0].addEventListener('change',readFile,false);

Here is the structure of the tags object generated by the id3 function:

{
  "title": "Stairway To Heaven",
  "album": "Stairway To Heaven",
  "artist": "Led Zeppelin",
  "year": "1999",
  "v1": {
    "title": "Stairway To Heaven",
    "artist": "Led Zeppelin",
    "album": "Stairway To Heaven",
    "year": "1999",
    "comment": "Classic Rock",
    "track": 13,
    "version": 1.1,
    "genre": "Other"
  },
  "v2": {
    "version": [3, 0],
    "title": "Stairway To Heaven",
    "album": "Stairway To Heaven",
    "comments": "Classic Rock",
    "publisher": "Virgin Records"
  }
}

I hope this explanation proves useful for your project!

Answer №2

After discovering the code snippet on this page, I decided to convert it into Javascript, which you can find here.

Here is the translated code that I implemented:

DataView.prototype.getChar=function(start) {
    return String.fromCharCode(this.getUint8(start));
};
DataView.prototype.getString=function(start,length) {
    for(var i=0,v='';i<length;++i) {
        v+=this.getChar(start+i);
    }
    return v;
};
DataView.prototype.getInt=function(start) {
    return (this.getUint8(start) << 21) | (this.getUint8(start+1) << 14) | (this.getUint8(start+2) << 7) | this.getUint8(start+3);
};

function readID3(){
    var a=new DataView(this.result);
    // Quick parsing
    if ( a.getString(0,3)!="ID3" )
    {
        return false;
    }

    // Checking tag version
    var TagVersion = a.getUint8(3);

    if ( TagVersion < 0 || TagVersion > 4 )
    {
        return false;
    }

    // Get ID3 tag size and flags
    var tagsize = a.getInt(6)+10;

    var uses_synch = (a.getUint8(5) & 0x80) != 0 ? true : false;
    var has_extended_hdr = (a.getUint8(5) & 0x40) != 0 ? true : false;

    var headersize=0;         
    // Reading extended header length and skipping it
    if ( has_extended_hdr )
    {
        var headersize = a.getInt(10);
    }

    // Reading whole tag
    var buffer=new DataView(a.buffer.slice(10+headersize,tagsize));

    // Preparing to parse the tag
    var length = buffer.byteLength;

    // Recreating the tag if desynchronization is used
    if ( uses_synch )
    {
        var newpos = 0;
        var newbuffer = new DataView(new ArrayBuffer(tagsize));

        for ( var i = 0; i < tagsize; i++ )
        {
            if ( i < tagsize - 1 && (buffer.getUint8(i) & 0xFF) == 0xFF && buffer.getUint8(i+1) == 0 )
            {
                newbuffer.setUint8(newpos++,0xFF);
                i++;
                continue;
            }

            newbuffer.setUint8(newpos++,buffer.getUint8(i));                 
        }

        length = newpos;
        buffer = newbuffer;
    }

    // Setting params
    var pos = 0;
    var ID3FrameSize = TagVersion < 3 ? 6 : 10;
    var m_title;
    var m_artist;

    // Parsing tags
    while ( true )
    {
        var rembytes = length - pos;

        // Frame header check
        if ( rembytes < ID3FrameSize )
            break;

        // Frame existence check
        if ( buffer.getChar(pos) < 'A' || buffer.getChar(pos) > 'Z' )
            break;

        // Obtaining frame name and size
        var framename;
        var framesize;

        if ( TagVersion < 3 )
        {
            framename = buffer.getString(pos,3);
            framesize = ((buffer.getUint8(pos+5) & 0xFF) << 8 ) | ((buffer.getUint8(pos+4) & 0xFF) << 16 ) | ((buffer.getUint8(pos+3) & 0xFF) << 24 );
        }
        else
        {
            framename = buffer.getString(pos,4);
            framesize = buffer.getInt(pos+4);
        }

        if ( pos + framesize > length )
            break;

        if ( framename== "TPE1"  || framename== "TPE2"  || framename== "TPE3"  || framename== "TPE" )
        {
            if ( m_artist == null )
                m_artist = parseTextField( buffer, pos + ID3FrameSize, framesize );
        }

        if ( framename== "TIT2" || framename== "TIT" )
        {
            if ( m_title == null )
                m_title = parseTextField( buffer, pos + ID3FrameSize, framesize );
        }

        pos += framesize + ID3FrameSize;
        continue;
    }
    console.log(m_title,m_artist);
    return m_title != null || m_artist != null;
}

function parseTextField( buffer, pos, size )
{
    if ( size < 2 )
        return null;

    var charcode = buffer.getUint8(pos); 

    //TODO string decoding         
    /*if ( charcode == 0 )
        charset = Charset.forName( "ISO-8859-1" );
    else if ( charcode == 3 )
        charset = Charset.forName( "UTF-8" );
    else
        charset = Charset.forName( "UTF-16" );

    return charset.decode( ByteBuffer.wrap( buffer, pos + 1, size - 1) ).toString();*/
    return buffer.getString(pos+1,size-1);
}

If you check the console log, you should be able to see the title and artist information. Pay attention to the encoding part in the parse text function where the method of reading the string is determined (look for TODO). Additionally, please note that I haven't tested it with extended headers, uses_synch set as true, or tag version 3.

Answer №3

Partially Correct Answer (successfully interprets utf8 formatted id3v2.4.0 with cover image)

The issues addressed in my query seem to have been resolved.

I was looking for a basic function set specifically designed to handle only id3v2.4.0 and parse the included image as well.

Thanks to @Siderite Zackwehdex for providing the correct answer, I now grasp the crucial aspect of the code that was missing.

After experimenting with it, I made several adjustments to the code.

First off, apologies for the condensed script, but it gives me a better overall understanding of the code structure. It's more convenient for me, but feel free to ask if you have any questions about the code.

I removed the uses_synch... it's rare to find a file utilizing synch. The same goes for has_extended_hdr. I also eliminated support for id3v2.0.0 to id3v2.2.0. I added a version check that covers all id3v2 subversions.

The main function output includes an array containing all the tags, where the id3v2 Version is also available. Additionally, I introduced a custom FRAME object which contains specialized functions for FRAMES other than textFrames. The current function within converts the image/cover/APIC to an easy-to-use base64 string, enabling the array to be stored as a JSON string.

While compatibility may be important for some users, the extended header or sync mentioned earlier are actually minor concerns.

CHALLENGES

The encoding must be UTF-8; otherwise, weird text paddings occur and some images are only partially parsed - essentially rendering them broken.

I aim to steer clear of using external libraries or extensive functions solely for this purpose... there has to be a smart and simple solution to correctly manage the encoding. ISO-8859-1, UTF-8, UTF-16... big endian... whatever... #00 vs #00 00..

If this is achieved, the support can be significantly enhanced.

Hopefully, some of you possess a resolution for this issue.

CODE

DataView.prototype.str=function(a,b,c,d){//start,length,placeholder,placeholder
 b=b||1;c=0;d='';for(;c<b;)d+=String.fromCharCode(this.getUint8(a+c++));return d
}
DataView.prototype.int=function(a){//start
 return (this.getUint8(a)<<21)|(this.getUint8(a+1)<<14)|
 (this.getUint8(a+2)<<7)|this.getUint8(a+3)
}
var frID3={
 'APIC':function(x,y,z,q){
  var b=0,c=['',0,''],d=1,e,b64;
  while(b<3)e=x.getUint8(y+z+d++),c[b]+=String.fromCharCode(e),
  e!=0||(b+=b==0?(c[1]=x.getUint8(y+z+d),2):1);
  b64='data:'+c[0]+';base64,'+
  btoa(String.fromCharCode.apply(null,new Uint8Array(x.buffer.slice(y+z+++d,q))));
  return {mime:c[0],description:c[2],type:c[1],base64:b64}
 }
}
function readID3(a,b,c,d,e,f,g,h){
 if(!(a=new DataView(this.result))||a.str(0,3)!='ID3')return;
 g={Version:'ID3v2.'+a.getUint8(3)+'.'+a.getUint8(4)};
 a=new DataView(a.buffer.slice(10+((a.getUint8(5)&0x40)!=0?a.int(10):0),a.int(6)+10));
 b=a.byteLength;c=0;d=10;
 while(true){
  f=a.str(c);e=a.int(c+4);
  if(b-c<d||(f<'A'||f>'Z')||c+e>b)break;
  g[h=a.str(c,4)]=frID3[h]?frID3[h](a,c,d,e):a.str(c+d,e);
  c+=e+d;
 }
 console.log(g);
}

DEMO

https://jsfiddle.net/2awq6pz7/

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Tips for updating the css class for multiple DOM elements using jQuery

I am currently attempting to modify the class property of a specific DOM element (in this case, a label tag) using jQuery version 1.10.2. Here is the code snippet I have written: var MyNameSpace = MyNameSpace || {}; MyNameSpace.enableDisableLabels = (f ...

Guide on transforming Json information into the preferred layout and iterating through the loop

Currently, I am diving deep into the world of JSON and feeling a bit puzzled by data formats, arrays, objects, and strings. First things first, I'm in need of data structured like this (on a jQuery page). Would this be considered an object or an arra ...

What sets server-side development apart from API creation?

As I dive into the world of web development, I find myself facing some confusion in my course material. The instructor has introduced concepts like promise objects and fetch, followed by axios, and now we're delving into the "express" package for buil ...

The chrome extension is not displaying any output in the console, even though there are no errors

https://i.sstatic.net/YhEKl.png I am currently working on creating a browser extension that will monitor the xhr section of the devtools network tab. To start off, I have kept my background script as simple as possible. Even though there are no errors whe ...

Warning issued after numerous clicks

My goal is to implement an Alert System that notifies the user when they leave a field empty. Currently, if a field is left empty and the next button is clicked, an alert pops up. However, if the same field is left empty again and the button is pressed, mu ...

Utilizing an AngularJS custom filter twice

Experimenting with a custom Angular filter example found at: https://scotch.io/tutorials/building-custom-angularjs-filters#filters-that-actually-filter, my version looks like this: <!DOCTYPE html> <html> <script src="http://ajax.googleapi ...

Arranging group collection post query using aggregation operations

When I use the aggregate function to get all users with the same name, the results returned by the API are not sorted even though I specify to sort by the 'name' field. const aggregateQuery: any = [ { $group: { _id: '$name', ...

What is the best way to trigger the second function on a click event?

Is there a way to trigger my 20 second timer with the click of a button, followed by an alert after the timer ends? HTML <aside class="start-box"> <button type="submit" class="btn btn-primary btn-lg" id="toggleBtn" onclick="startCloc ...

Tips for toggling Bootstrap 5 tabs using JavaScript instead of the button version

I am looking to switch tabs programmatically using bootstrap 5. The Bootstrap documentation recommends using buttons for tabs instead of links for a dynamic change. Here is the code I have: $("#mybut").click(function() { var sel = document.querySelector( ...

Adding a class to a navigation item based on the route path can be achieved by following

I am currently working on a Vue.js project and I have a navigation component with multiple router-links within li elements like the example below <li class="m-menu__item m-menu__item--active" aria-haspopup="true" id="da ...

Display a fancy slideshow that transitions to five new images when the last one is reached

Here is a screenshot of my issue: https://i.stack.imgur.com/duhzn.png Incorporating Bootstrap 3 and FancyBox, I am facing an issue with displaying images in rows. When I reach the last image, clicking on the right arrow should result in sliding to anothe ...

Looking for a specific phrase in the data entered by the user

I am dealing with data in ckeditor that looks like this: <p>test 1</p> <p>test 2</p> <p><img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICw ...

Is it possible for a function parameter to utilize an array method?

Just starting to grasp ES6 and diving into my inaugural React App via an online course. Wanted to share a snag I hit along the way, along with a link to my git repository for any kind souls willing to lend a hand. This app is designed for book organization ...

Displaying properties of objects in javascript

Just starting with JavaScript and struggling to solve this problem. I attempted using map and filter but couldn't quite implement the if condition correctly. How can I find the records in the given array that have a gender of 0 and color is red? let s ...

When attempting to send an array value in JavaScript, it may mistakenly display as "[object Object]"

I queried the database to count the number of results and saved it as 'TotalItems'. mysql_crawl.query('SELECT COUNT(*) FROM `catalogsearch_fulltext` WHERE MATCH(data_index) AGAINST("'+n+'")', function(error, count) { var ...

Dynamically load npm modules in Webpack using variables for the require statement

Is there a way to dynamically require an NPM module using a variable? Below is the sample code I've been trying, everything seems to be working fine except for importing NPM modules dynamically. const firstModule = 'my-npm-module'; const s ...

Cover the entire screen with numerous DIV elements

Situation: I am currently tackling a web design project that involves filling the entire screen with 60px x 60px DIVs. These DIVs act as tiles on a virtual wall, each changing color randomly when hovered over. Issue: The challenge arises when the monitor ...

Is it possible for me to create a distinct component that can display numerous input fields at once?

Introducing the inputArea component, which collects data from input fields and sends it to the server: export default class InputArea extends React.Component { constructor(props){ super(props); this.state = { draw_number: '', ...

An issue occurs with the scope of a variable during the compilation of Relay Modern code, resulting in an

I have created the following simple Relay Modern code snippet: import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { QueryRenderer, graphql } from 'react-relay' import environment f ...

Encountering an issue while creating a controller in AdonisJS5

Whenever I attempt to execute the command node ace make:controller User an error is shown as follows Cannot locate module './commands/Make\Controller' Require stack: /Users/abiliocoelho/Desktop/socket/node_modules/@adonisjs/assembler/buil ...