Read MP3 ID3 Tag Script for PHP

Created: December 12, 2014
Last Modified: April 18, 2017
Subscribe to Internet Tips and Tools Feed

I created this PHP script to read the ID3 tag from MP3 files because I did not like having to install the PECL id3 library to use the id3_get_tag() function in PHP. Also many programmers have found it confusing to install the PECL id3 library. There are many forums with Fatal error: Call to undefined function: id3_get_tag() with no one giving complete instructions on how to fix the error. Also many other PHP libraries or functions on the Internet for getting mp3 id3 tags were either too big and messy, undocumented or uncommented, or incomplete (not able to read ID3v1 and ID3v2 tags) or incorrect.

Features of this PHP script:

  • Small script with comments
  • Read ID3v1, ID3v2.2, ID3v2.3 and ID3v2.4 MP3 tags
  • Creates extra custom tags with bitrate, duration etc...
  • New!! Reads tags from mp3s on local or remote servers.
With this script you can use the mp3_get_tags() function as follows:
$id3_tags = mp3_get_tags($file);
$id3_tags["title"]; // Title of mp3 file
$id3_tags["album"]; // Album Title of mp3 file
$id3_tags["artist"]; // Artist Tag
$id3_tags["year"]; // Year
$id3_tags["comment"]; // Title of mp3 file
$id3_tags["genre"]; // Genre can be text or numeric value representing genre

$id3_tags["genre_name"] = mp3_get_genre_name($id3_tags["genre"]); // Get genre name from number value

Also the following custom tags are created for convenience:

$id3_tags["id3_tag_version"]; // ID3 Tag Version; 1, 2.2, 2.3 or 2.4
$id3_tags["version"]; // MPEG Audio version ID
$id3_tags["layer"]; // MPEG Audio layer description
$id3_tags["crc"]; // 0 = Yes using CRC. 1 = No CRC
$id3_tags["bitrate"]; // Ex: 64, 128, 160, 320. CBR (Constant Bit Rate)
$id3_tags["frequency"]; // Sampling Rate Frequency
$id3_tags["filesize"]; // Size of mp3 file
$id3_tags["duration"]; // Duration of mp3 in seconds. Probably only correct for CBR not VBR
$id3_tags["formatted_time"]; // Duration of mp3 in i:s. Probably only correct for CBR not VBR

You can add more ID3 tags to the $tags_array() if you know the tag abbreviation and tag name which you can find at the following reference sites:
id3v2 tagshttp://id3.org/id3v2-00
id3v2.3 tagshttp://id3.org/id3v2.3.0#Declared_ID3v2_frames
id3v2.4 tagshttp://id3.org/id3v2.4.0-frames
id3v2.4 Structurehttp://id3.org/id3v2.4.0-structure

Click here for a list of all id3 tags and genres dlc_b

Download

Downloaded 0 times.
This download is a free trial. If you continue to use it then the cost is a one time fee of 25.00 USD.

PHP SOURCE:


<?PHP
/* 	Read MP3 ID3 Tag Script for PHP
	Created By Jeff Baker on December 12, 2014
	Copyright (C) 2014 Jeff Baker
	www.seabreezecomputers.com/tips/mp3_id3_tag.htm
	Version 2.0b - April 18, 2016	
*/

function mp3_get_tags($file)
{
	// http://www.seabreezecomputers.com/tips/mp3_id3_tag.htm
	$id3_tags = array(); // Function returns an array of id3 mp3 tags for $file
	// See: http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm
	$versions = array("00" => "2.5", "01" => "x", "10" => "2", "11" => "1"); // MPEG Audio version ID
	$layers = array("00" => "x", "01" => "3", "10" => "2", "11" => "1"); // MPEG Audio layer description
	$bitrates = array(
		'V1L1'=>array(0,32,64,96,128,160,192,224,256,288,320,352,384,416,448),
        'V1L2'=>array(0,32,48,56, 64, 80, 96,112,128,160,192,224,256,320,384),
        'V1L3'=>array(0,32,40,48, 56, 64, 80, 96,112,128,160,192,224,256,320),
        'V2L1'=>array(0,32,48,56, 64, 80, 96,112,128,144,160,176,192,224,256),
        'V2L2'=>array(0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160),
        'V2L3'=>array(0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160),
        );  
    $sample_rates = array(
			'1'   => array(44100,48000,32000),
            '2'   => array(22050,24000,16000),
            '2.5' => array(11025,12000, 8000),
        );
	
	$handle = fopen($file, "r");
	 
	if (!$handle) return; // Version 2.0
	{
		// Look for ID3v2 - http://id3.org/id3v2.3.0 or http://id3.org/id3v2.4.0-frames
		$tags_array = array( // first ID3v2.3 and ID3v2.4 tags
							'TIT2' => 'title', 'TALB' => 'album', 'TPE1' => 'artist',
							'TYER' => 'year', 'COMM' => 'comment', 'TCON' => 'genre', 'TLEN' => 'length',
							// second ID3v2.2 tags - http://id3.org/id3v2-00
							'TT2' => 'title', 'TAL' => 'album', 'TP1' => 'artist',
							'TYE' => 'year', 'COM' => 'comment', 'TCO' => 'genre', 'TLE' => 'length'
							);
		$null = chr(0); // the stop bit or null in ID3 tags is the first ASCII character or chr(0)
		//fseek($handle, 0, SEEK_SET); // Read file from beginning // Version 2.0 - Removed all fseek for remote file support
		$data = fread($handle, 10); // 10 = Size of header - http://id3.org/id3v2.4.0-structure
		if (substr($data,0,3) == 'ID3') // If first 3 bytes == "ID3"
		{
			$id3_major_version = hexdec(bin2hex(substr($data,3,1)));
			$id3_tags["id3_tag_version"] = "2.".$id3_major_version;
			$id3_revision = hexdec(bin2hex(substr($data,4,1)));
			$id3_flags = decbin(ord(substr($data,5,1))); // 8 flag bits (first 4 may be set)
			$id3_flags = str_pad($id3_flags, 8, 0, STR_PAD_LEFT);
			$footer_flag = $id3_flags[3]; // footer flag is 4th flag bit
			// Calculate size of header including all tags and extended header and footer
			$mb_size = ord(substr($data,6,1)); // each number here is equal to 2 Megabytes
			$kb_size = ord(substr($data,7,1)); // each number here is equal to 16 Kilobytes
			$byte128_size = ord(substr($data,8,1)); // each number here is equal to 128 Bytes
			$byte_size = ord(substr($data,9,1)); // each number here is equal to 1 Byte
			$total_size = ($mb_size * 2097152) + ($kb_size * 16384) + ($byte128_size * 128) + $byte_size;
			//fseek($handle, 0, SEEK_SET); // Read file from beginning // Version 2.0 - Removed all fseek for remote file support
			//$data = fread($handle, 10 + $total_size + ($footer_flag * 10));
			$data .= stream_get_contents($handle, $total_size + ($footer_flag * 10)); // Version 2.0 - Using stream_get_contents instead of fread // Version 2.0a - Removed extra 10 + before $total_size
			foreach ($tags_array as $key => $value)
			{
				if ($id3_major_version == 3 || $id3_major_version == 4)
					$tag_header_length = 10; 
				else // if ($id3_major_version == 2)
					$tag_header_length = 6; 
				if ($tag_pos = strpos($data, $key.$null))
				{
					$tag_abbr = trim(substr($data, $tag_pos, 4)); // tag abbreviation
					$content_length = hexdec(bin2hex(substr($data, $tag_pos + ($tag_header_length/2),3)));
					$content = trim(substr($data, $tag_pos + $tag_header_length, $content_length));
					$tag_content = "";
					for ($i = 0; $i < strlen($content); $i++)
						if($content[$i] >= " " && $content[$i] <= "~") $tag_content .= $content[$i];
					$id3_tags[$value] = $tag_content; // Ex: $id3_tags['title'] = "Song Title";
				}
			}
			if ($id3_major_version != 2) // Version 2.0
				$data = ""; // wipe out data so we only add to it if it is ID3v1 below with: $data .= fread($handle, 10); 
		}
		//else // ID3v1
		//	fseek($handle, 0, SEEK_SET); // Read file from beginning // Version 2.0 - Removed all fseek for remote file support	
		$bits = null; // Version 2.0b - Declare $bits variable to avoid warning in PHP
		// Look for first mp3 frame.  Every frame begins with eleven 1s (bits)	
		while (!feof($handle))
		{
			//$data .= fread($handle, 10); // read 10 bytes of mp3 after header
			$data .= stream_get_contents($handle, 10); // Version 2.0 - Using stream_get_contents instead of fread
			for ($i = 0; $i < strlen($data); $i++)
				$bits .= str_pad(decbin(ord($data[$i])), 8, 0, STR_PAD_LEFT);
			$frame_pos = strpos($bits, "11111111111"); // Version 2.0 - Bug fix: Removed from if statement
			// http://mpgedit.org/mpgedit/mpeg_format/MP3Format.html
			if ($frame_pos !== false) // Version 2.0 - Bug fix: Having strpos in if statement always returned 1 or 0 instead of pos
			{
				//echo "<p>".substr($bits, $frame_pos);
				$id3_tags["version"] = $versions[substr($bits, $frame_pos + 11, 2)];
				$id3_tags["layer"] = $layers[substr($bits, $frame_pos + 13, 2)];
				$id3_tags["crc"] = substr($bits, $frame_pos + 15, 1); // 0 = Yes using CRC. 1 = No CRC
				$bitrate_index = bindec(substr($bits, $frame_pos + 16, 4));
				$id3_tags["bitrate"] = $bitrates["V".$id3_tags["version"][0]."L".$id3_tags["layer"]][$bitrate_index];
				$id3_tags["frequency"] = $sample_rates["1"][bindec(substr($bits, $frame_pos + 19, 2))]; // Sampling Rate Frequency
				if (preg_match("/^(https?|ftp):\/\//", $file))
					$id3_tags["filesize"] = get_headers($file,1)['Content-Length']; // Version 2.0 - Remote file
				else
					$id3_tags["filesize"] = filesize($file);
				//print_r(get_headers($file));
				$bps = ($id3_tags["bitrate"]*1000)/8;
        		// duration in seconds = filesize - total size of headers / bps
        		$id3_tags["duration"] = round(($id3_tags["filesize"] - $total_size) / $bps);
        		// Formatted time (i:s)
				//$mins = floor($id3_tags["duration"] / 60); // Version 2.0b - No longer used for formatted_time
				//$secs = time - ($mins * 60);	// Version 2.0b - No longer used for formatted_time
				//if ($secs < 10) $secs = "0".secs; // Version 2.0b - No longer used for formatted_time
				$id3_tags["formatted_time"] = gmdate("i:s", $id3_tags["duration"]);
				break;	// break while loop
			}
		}	
	}
	
	// Look for ID3v1 - http://id3.org/ID3v1
	if (!isset($id3_major_version)) // if we didn't alreay find ID3v2 v3 or v4 tags
	{
		$id3_tags["id3_tag_version"] = 1; // Version 2.0 
		while (!feof($handle))
		{
			//$data .= fread($handle, 128);
			$data .= stream_get_contents($handle, 128); // Version 2.0 - Using stream_get_contents instead of fread
			
		}
		$data = substr($data, -128);  // Get just last 128 bytes of file
		if(substr($data, 0, 3) == "TAG") // If first 3 bytes == "TAG"
		{
			$id3_tags["title"] = trim(substr($data, 3, 30));
			$id3_tags["artist"] = trim(substr($data, 33, 30));
			$id3_tags["album"] = trim(substr($data, 63, 30));
			$id3_tags["year"] = trim(substr($data, 93, 4));
			$id3_tags["comment"] = trim(substr($data, 97, 30));
			$id3_tags["genre"] = ord(trim(substr($data, 127, 1))); // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/ID3.html
		}
	}
	
	fclose($handle);
		
	return($id3_tags);
	
} // end function mp3_get_tags($file)

function mp3_get_genre_name($genre_id)
{
	// See: http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/ID3.html
	$genre_names = array("Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "Alt. Rock", "Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta Rap", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", "Psychedelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock", "National Folk", "Swing", "Fast-Fusion", "Bebop", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A Cappella", "Euro-House", "Dance Hall", "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Afro-Punk", "Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop", "Synthpop", "Abstract", "Art Rock", "Baroque", "Bhangra", "Big Beat", "Breakbeat", "Chillout", "Downtempo", "Dub", "EBM", "Eclectic", "Electro", "Electroclash", "Emo", "Experimental", "Garage", "Global", "IDM", "Illbient", "Industro-Goth", "Jam Band", "Krautrock", "Leftfield", "Lounge", "Math Rock", "New Romantic", "Nu-Breakz", "Post-Punk", "Post-Rock", "Psytrance", "Shoegaze", "Space Rock", "Trop Rock", "World Music", "Neoclassical", "Audiobook", "Audio Theatre", "Neue Deutsche Welle", "Podcast", "Indie Rock", "G-Funk", "Dubstep", "Garage Rock", "Psybient");
	/* According to http://id3.org/id3v2.3.0#Text_information_frames:
		"Several references can be made in the same frame, e.g. "(51)(39)" */
	$genres = explode(")", $genre_id);
	$n = 1;
	foreach($genres as $genre_num)
	{ 
			$genre_num = str_replace("(", "", str_replace(")", "", $genre_num)); // remove ( and )
			if ($n > 1 && !empty($genre_num))
				$genre_string .= ","; // Separate multiple genres by ,
			if (is_numeric($genre_num))
			{
				if ($genre_num >= 0 && $genre_num <= 191)
					$genre_string .= $genre_names[$genre_num];
				else
					$genre_string .= "None"; // 255 = None 
			}
			else
				$genre_string .= $genre_num;	
		$n++;
	}
	return($genre_string);
} // end function mp3_get_genre_name()



?>

HISTORY

4/18/2016 - Version 2.0b - Bug Fix: If PHP is set to display warnings then get "Variable $bits undefined on line 91" warning. Fixed by declaring variable $bits on line 84. Also undefined constant warning on line 113. Fixed by commenting line 112 and 113 because they were no longer being used. It was an old way to get formatted_time.

11/30/2015 - Version 2.0a - Bug Fix: Minor bug with getting correct bitrate and duration with some MP3 files and causing a Division by zero error in PHP. Fixed with line 61 by removing 10 + before total_size in the stream_get_contents statement.

10/19/2015 - Version 2.0 - Major Changes: Now the script can read mp3s on remote servers and not just the same server the script is on. Major bug fixes: There were many bugs with getting the bitrate, duration, etc. from some versions of mp3 files. Those bugs seem to have been fixed.

12/17/2014 - Version 1.0b - Bug Fix: Forgot to remove an echo for testing on line 102

12/12/2014 - Version 1.0 - Created function mp3_get_tags()

Back to www.seabreezecomputers.com
Subscribe to Internet Tips and Tools Feed        

User Comments

There are 14 comments.

Displaying first 50 comments.

1. Posted By: junior programmer - - August 28, 2015, 12:32 am
Sir, This script is working for local mp3 file, But how to get tags of remote file like "http://domain/folder/name.mp3"

Thank you.

2. Posted By: Jeff - - September 30, 2015, 1:09 pm
Hi Junior Programmer,

Sorry for the late response. I thought I had replied to you a month ago but I can't find my reply. I'm not sure what the problem could be without some kind of error. Do you get an error?

Jeff
www.seabreezecomputers.com/

3. Posted By: Jeff - - October 19, 2015, 2:11 pm
Hello junior programmer,

I have updated the script mp3_get_tags.php. It can now read mp3 tags of local and remote mp3 files.

Jeff
www.seabreezecomputers.com/tips/mp3_id3_tag.htm

4. Posted By: Mgeezy - - February 26, 2016, 1:18 am
Hello, can this work with ID3 of wav files and/or can it be easily converted?

5. Posted By: n0n0 - - April 18, 2016, 2:54 am
Hi, thanks for the script, it's really helpful, but i have a couple of problems when trying to use it :
I have two warnings telling me the variable "bits" is undefined (line 91) and same thing for the constant "time" (line 113).
Ultimately, it does not show any information on the tags of the file I'm trying to get.
Could you please help me ?=)

6. Posted By: Jeff - - April 18, 2016, 11:31 am
Hi n0n0,

Your PHP server is set to display warnings. I think I fixed the script so that it will not display those warnings. Go ahead and download it again at the website. But for the future you may want to set your PHP to not display warnings, but only errors.

One way to do this: On your server there should be a php.ini file. Edit the file and look for a line that starts with error_reporting and change it to:

error_reporting=E_ALL & ~E_NOTICE


Jeff
www.seabreezecomputers.com/tips/mp3_id3_tag.htm

7. Posted By: n0n0 - - April 18, 2016, 11:59 pm
Thanks a lot ! It works perfectly ! :D

8. Posted By: Stephan - - June 15, 2016, 8:48 am
Nice tool!
how do I call the file and parse the tags in my php-script?
I am trying it like this:

require_once('mp3_get_tags.php'); // call the script
$file="http://url.com/to/the/audio.mp3"; // get the mp3
$id3_tags1=mp3_get_tags($file); // get the tags of the file
echo $id3_tags1["title"]; // parse the title

this doesnt work. I am sure I am doing something completely wrong..=(
(i am new to php)

Thanks for any suggestions!

Cheers.


9. Posted By: Stephan - - June 15, 2016, 9:19 am
Sorry about my previous comment. It works!!!!!
I just hat to adjust my php-server settings to allow content from external urls.
This is an amazing script!

Many many thanks!!!!!!

10. Posted By: John Kiernan - - April 7, 2017, 9:05 am
This script is great, except for two things:
My MP3 file is over 1 hour (1:17:58), but the array shows it only as 17:58. Is there any tweaking I can do to get it all?

Secondly, for some reason, when I add tags on my original files, the comment field looks like this:
'comment'=> string 'composer fieldTPOSdiscnumber fieldTCONspeechTIT2title fieldTRCK01TYER2001' (length=73)
However, when I open it in audacity and then save it, it looks like this:
'comment'=> string 'this is the comment field.' (length=26)

Any thoughts on how I could tweak this as well?

Thanks in advance for the reply and especially thanks for the script itself.
-- John Kiernan

11. Posted By: Jeff - - April 10, 2017, 1:38 pm
Hi John Kiernan,

For the formatted_time to display hours try changing line 115 of the script to:

$id3_tags["formatted_time"]=gmdate("H:i:s", $id3_tags["duration"]);

I'm not sure about the comment field problem. How are you adding the comments to the mp3 file?

Jeff
www.seabreezecomputers.com/

12. Posted By: Michael - - May 11, 2017, 6:57 am
Hi John Kiernan and Jeff,

the Reason for the Comment-Problem is maybe the v2.2-Tag 'COM' for comments and the v2.3-Tag 'TCOM' for Composer. Looking for the 'COM'-Tag the script finds the '(T)COM'-Data.
Maybe the $tags_array has to be divided in a v2.2- and a v2.3-Version and these parts have to be used depending on the $id3_major_version.

Greetings from Cologne
Michael

13. Posted By: John Kiernan - - May 18, 2017, 11:45 am
Dear Jeff -
Thanks so much for the tip on the time thing, that took care of it!

On the comment glitch, I personally use either Audacity or MP3Tag (both freeware) for stamped the metadata onto the MP3 file, but I'm not 100% sure what software was/is being used by the people uploading the files.

I did some more investigating and made a discovery. Using MP3Tag, if I modified the text fields (added a few letters here and there), it didn't affect the comments. However, if I changed the "Genre" with the "Genre" dropdown, then the comments field got corrupted.

14. Posted By: Jeff - - May 18, 2017, 11:56 am
Hi Michael,

Thanks for the comment. I have made a version of the script that separates the $tags_array by version like you stated. But I have not put it up on the website yet because there has not been enough testing. If you would like to test it you can add the following lines right at line 48 of the script:


if ($id3_major_version >=3) // Version 2.0c
$tags_array=array( // first ID3v2.3 and ID3v2.4 tags
'TIT2'=> 'title', 'TALB'=> 'album', 'TPE1'=> 'artist',
'TYER'=> 'year', 'COMM'=> 'comment', 'TCON'=> 'genre', 'TLEN'=> 'length');
else
$tags_array=array( // second ID3v2.2 tags - id3.org/id3v2-00
'TT2'=> 'title', 'TAL'=> 'album', 'TP1'=> 'artist',
'TYE'=> 'year', 'COM'=> 'comment', 'TCO'=> 'genre', 'TLE'=> 'length'
);


Thanks,

Jeff
www.seabreezecomputers.com/