<?php

// Note:
// MP3 lib from http://www.jawws.com/ seems to do a whole lot more then my
// little class. If you want something more then this go check it out.
// As of 3/6/2001 The above host seems to be down, an url to a mirror
// would be nice.

class id3
{
    /*
     * id3 - A Class for reading/writing MP3 ID3 tags
     * 
     * By Sandy McArthur, Jr. <Leknor@Leknor.com>
     * 
     * Copyright 2000 (c) All Rights Reserved, All Responsibility Yours
     *
     * This code is released under the GNU LGPL Go read it over here:
     * http://www.gnu.org/copyleft/lesser.html
     * 
     * I do make one optional request, I would like an account on or a
     * copy of where this code is used. If that is not possible then
     * an email would be cool.
     * 
     * Warning: I really hope this doesn't mess up your MP3s but you
     * are on your own if bad things happen, like don't blame me if
     * your first born dies while using this code.
     *
     * Note: This code doesn't try to deal with corrupt mp3s. So if you get
     * incorrect length times or something else it may be your mp3. To fix just
     * re-enocde from the CD. :~)
     * 
     * eg:
     * 	require('class.id3.php');
     *	$id3 = new id3('/path/to/our lady peace - middle of yesterday.mp3');
     *	$id3->comment = 'Go buy some OLP CDs, they rock!';
     *	$id3->write();
     * 
     * This was written from scratch from info freely available on
     * the web.
     * 
     * These site(s) were useful to me:
     *	http://www.php.net/manual/
     *	http://www.mpx.cz/mp3manager/tags.htm
     * 
     * The ID3 Tag format is as follows:
     * 
     * Start at the -128 byte from the end of the file.
     * byte   # of   field
     * range  bytes  description
     * ------------------------------------
     *  0-2    (3)   the tag identifier "TAG"
     *  3-32  (30)   the track name
     * 33-62  (30)   the artists name
     * 63-92  (30)   the album name
     * 93-96   (4)   the album year
     * 97-126 (30)   the comment
     * 127     (1)   the genre - look below
     * 
     * Change Log:
     *	0.93:	Better parsing of the id3 text fields.
     *	0.92:	Added a @ to silence a warning about divide by zero.
     *	0.91:	Added a @ to silence a warning about 2nd arg in
     *		str_repeat() being 0. And a type that probably messed up
     *		$id3->lengths;
     *	0.9:	Jorge, added code to get the bitrate, time, mpeg version
     *	0.82:	Superficial code clean up
     *	0.81:	first 'release' version
     *
     * Thanks To:
     *	Jorge Cisneros Flores <jorge@e-nexus.com.mx>
     *	H. Wade Minter <minter@lunenburg.org>
     *
     * The most recent version is available at:
     *	http://Leknor.com/code/
     *
     */

    var $_version = 0.93; // Version of the id3 class

    // Anyone got a good way to prevent each instance of the
    // class from needing to allocate a genres array to save
    // a little memory? (I'm thinking of trying a function
    // with a switch) - Leknor
    var $genres = array(
	    0   => 'Blues',
	    1   => 'Classic Rock',
	    2   => 'Country',
	    3   => 'Dance',
	    4   => 'Disco',
	    5   => 'Funk',
	    6   => 'Grunge',
	    7   => 'Hip-Hop',
	    8   => 'Jazz',
	    9   => 'Metal',
	    10  => 'New Age',
	    11  => 'Oldies',
	    12  => 'Other',
	    13  => 'Pop',
	    14  => 'R&B',
	    15  => 'Rap',
	    16  => 'Reggae',
	    17  => 'Rock',
	    18  => 'Techno',
	    19  => 'Industrial',
	    20  => 'Alternative',
	    21  => 'Ska',
	    22  => 'Death Metal',
	    23  => 'Pranks',
	    24  => 'Soundtrack',
	    25  => 'Euro-Techno',
	    26  => 'Ambient',
	    27  => 'Trip-Hop',
	    28  => 'Vocal',
	    29  => 'Jazz+Funk',
	    30  => 'Fusion',
	    31  => 'Trance',
	    32  => 'Classical',
	    33  => 'Instrumental',
	    34  => 'Acid',
	    35  => 'House',
	    36  => 'Game',
	    37  => 'Sound Clip',
	    38  => 'Gospel',
	    39  => 'Noise',
	    40  => 'Alternative Rock',
	    41  => 'Bass',
	    42  => 'Soul',
	    43  => 'Punk',
	    44  => 'Space',
	    45  => 'Meditative',
	    46  => 'Instrumental Pop',
	    47  => 'Instrumental Rock',
	    48  => 'Ethnic',
	    49  => 'Gothic',
	    50  => 'Darkwave',
	    51  => 'Techno-Industrial',
	    52  => 'Electronic',
	    53  => 'Pop-Folk',
	    54  => 'Eurodance',
	    55  => 'Dream',
	    56  => 'Southern Rock',
	    57  => 'Comedy',
	    58  => 'Cult',
	    59  => 'Gangsta',
	    60  => 'Top 40',
	    61  => 'Christian Rap',
	    62  => 'Pop/Funk',
	    63  => 'Jungle',
	    64  => 'Native US',
	    65  => 'Cabaret',
	    66  => 'New Wave',
	    67  => 'Psychadelic',
	    68  => 'Rave',
	    69  => 'Showtunes',
	    70  => 'Trailer',
	    71  => 'Lo-Fi',
	    72  => 'Tribal',
	    73  => 'Acid Punk',
	    74  => 'Acid Jazz',
	    75  => 'Polka',
	    76  => 'Retro',
	    77  => 'Musical',
	    78  => 'Rock & Roll',
	    79  => 'Hard Rock',
	    80  => 'Folk',
	    81  => 'Folk-Rock',
	    82  => 'National Folk',
	    83  => 'Swing',
	    84  => 'Fast Fusion',
	    85  => 'Bebob',
	    86  => 'Latin',
	    87  => 'Revival',
	    88  => 'Celtic',
	    89  => 'Bluegrass',
	    90  => 'Avantgarde',
	    91  => 'Gothic Rock',
	    92  => 'Progressive Rock',
	    93  => 'Psychedelic Rock',
	    94  => 'Symphonic Rock',
	    95  => 'Slow Rock',
	    96  => 'Big Band',
	    97  => 'Chorus',
	    98  => 'Easy Listening',
	    99  => 'Acoustic',
	    100 => 'Humour',
	    101 => 'Speech',
	    102 => 'Chanson',
	    103 => 'Opera',
	    104 => 'Chamber Music',
	    105 => 'Sonata',
	    106 => 'Symphony',
	    107 => 'Booty Bass',
	    108 => 'Primus',
	    109 => 'Porn Groove',
	    110 => 'Satire',
	    111 => 'Slow Jam',
	    112 => 'Club',
	    113 => 'Tango',
	    114 => 'Samba',
	    115 => 'Folklore',
	    116 => 'Ballad',
	    117 => 'Power Ballad',
	    118 => 'Rhytmic Soul',
	    119 => 'Freestyle',
	    120 => 'Duet',
	    121 => 'Punk Rock',
	    122 => 'Drum Solo',
	    123 => 'Acapella',
	    124 => 'Euro-House',
	    125 => 'Dance Hall',
	    126 => 'Goa',
	    127 => 'Drum & Bass',
	    128 => 'Club-House',
	    129 => 'Hardcore',
	    130 => 'Terror',
	    131 => 'Indie',
	    132 => 'BritPop',
	    133 => 'Negerpunk',
	    134 => 'Polsk Punk',
	    135 => 'Beat',
	    136 => 'Christian Gangsta Rap',
	    137 => 'Heavy Metal',
	    138 => 'Black Metal',
	    139 => 'Crossover',
	    140 => 'Contemporary Christian',
	    141 => 'Christian Rock',
	    142 => 'Merengue',
	    143 => 'Salsa',
	    144 => 'Trash Metal',
	    145 => 'Anime',
	    146 => 'Jpop',
	    147 => 'Synthpop'
		);

    var $tag = False;		// id3 tag, usually "TAG" use this to test
							// if a tag was loaded and what type of tag
							// was loaded
    var $file = False;		// mp3 file name
    var $name = False;		// track name
    var $artists = False;	// artists
    var $album = False;		// album
    var $year = False;		// year
    var $comment = False;	// comment
    var $bitrate = False;	// bitrate
	var $sample = False;
    var $length = False;	// length of mp3 format hh:ss
    var $lengths = False;	// length of mp3 in seconds
    var $mpeg_ver = False;	// version of mpeg
    var $layer = False;		// version of layer
    var $genre = False;		// genre name
    var $genreno = False;	// genre number
	var $emphasis = False;	// emphasis
	var $copyright = False;	// copyright
	var $original = False;	// original
	var $crc = False;		// error correction
	var $chmode = False;	// channel mode
    var $error = False;		// if any errors they will be here

    var $rawtag = False;
    var $newtag = False;

    var $id3format = 'a3TAG/a30NAME/a30ARTISTS/a30ALBUM/a4YEAR/a30COMMENT/C1GENRENO';
    var $id3pack = 'a3a30a30a30a4a30C1';
    // Format of the ID3 as understood by unpack
    var $id3unpack = 'a3TAG/a30NAME/a30ARTISTS/a30ALBUM/a4YEAR/a30COMMENT/C1GENRENO';

    /*
    $table = [2][
            [0,  8, 16, 24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,0],
            [0,  8, 16, 24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,0],
            [0, 32, 48, 56, 64, 80, 96,112,128,144,160,176,192,224,256,0] 
            ],[1]
            [0, 32, 40, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,0],
            [0, 32, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,384,0],
            [0, 32, 64, 96,128,160,192,224,256,288,320,352,384,416,448,0]
	    ];
     */

    var $debug = False;	// print debugging info?
    var $debugbeg = '<DIV STYLE="margin: 0.5 em; padding: 0.5 em; border-width: thin; border-color: black; border-style: solid">';
    var $debugend = '</DIV>';

    /*
     * id3 constructor - creates a new id3 object and maybe loads a tag
     * from a file.
     *
     * If a file is specified it will try to read it's tag.
     */
    function id3($file = False)
	{
		if ($this->debug) print($this->debugbeg . "id3($file)<HR>\n");
		if ($file)
		{
		    $this->file = $file;
		    $this->readhead();
		    $this->read();
		}
		//if ($this->debug) print_r($this->genres);
		if ($this->debug) print($this->debugend);
    }

    /*
     * read - read the tag from a file
     *
     * if a file is specified then it is read else the file specified
     * in $this->file is read.
     *
     * if there is an error it will return False and a message will be
     * put in $this->error
     */
    function read($file = False)
	{
		if ($this->debug) print($this->debugbeg . "read($file)<HR>\n");

		if ($file === False)
		{
		    $file = $this->file;
		}
		else
		{
		    $this->file = $file;
		}
		if (!$file)
		{
		    $this->error = 'File not specified';
		    return False;
		}

		if (! ($f = fopen($file, 'rb')) )
		{
		    $this->error = 'Unable to open ' . $file;
		    return False;
		}

		if (fseek($f, -128, SEEK_END) == -1)
		{
		    $this->error = 'Unable to see to end - 128 of ' . $file;
		    return False;
		}

		$r = fread($f, 128);
		fclose($f);

		$this->rawtag = &$r;
		if ($this->debug)
		{
			$unp = unpack('h*raw', $this->rawtag);
			print_r($unp);
		}

		$id3tag = $this->decode();

		$this->tag = $id3tag['TAG'];

		$tmp = explode(Chr(0), $id3tag['NAME']);
		$this->name = $tmp[0];

		$tmp = explode(Chr(0), $id3tag['ARTISTS']);
		$this->artists = $tmp[0];

		$tmp = explode(Chr(0), $id3tag['ALBUM']);
		$this->album = $tmp[0];

		// XXX: The year isn't really handled right:
		$this->year = $id3tag['YEAR'];

		$tmp = explode(Chr(0), $id3tag['COMMENT']);
		$this->comment = $tmp[0];

		$this->genreno = $id3tag['GENRENO'];
		$this->genre = $id3tag['GENRE'];
		if (!$this->genre)
		{
			$this->genre = 'Other';
		}

		if ($this->debug) print($this->debugend);
	}

	function readhead($file = False)
	{
		if ($this->debug) print($this->debugbeg . "read($file)<HR>\n");

		if (!$file)
		{
			$file = $this->file;
		}
		else
		{
			$this->file = $file;
		}

		if (!$file)
		{
			$this->error = 'File not specified';
			return False;
		}

		if (!($f = fopen($file, 'rb')))
		{
			//$this->error = 'Unable to open ' . $file;
			return False;
		}

		// Loop to find the first frame
		while($test = fread($f,1))
		{
			//if ($this->debug) echo "Find... ";
			if ($test = "ÿ")
			{
				break;
			}
		}
		fseek($f,ftell($f)-1);
		$r = fread($f, 4); //Info Header
		fclose($f);

		$header="";
		for($z=0;$z<4;$z++)
		{
			// XXX: Added the @ to silence an warning abd 2nd argument being 0, should I care?
			$header.= @str_repeat("0",8-strlen(decbin(ord(substr($r,$z,1))))).decbin(ord(substr($r,$z,1)));
			//$header.=str_repeat("0",8-strlen(decbin(ord(substr($r,$z,1))))).decbin(ord(substr($r,$z,1)));
		}
		$this->header = $header;

		switch (substr($header,11,2))
		{
			case "00":
			$this->mpeg_ver = "2.5";
			$tbl_bit = array(
				"3"=> array(0,  8, 16, 24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,0),
				"2"=> array(0,  8, 16, 24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,0),
				"1"=> array(0, 32, 48, 56, 64, 80, 96,112,128,144,160,176,192,224,256,0));
			break;
			case "10":
			$this->mpeg_ver = "2";
			$tbl_bit = array(
				"3"=> array(0,  8, 16, 24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,0),
				"2"=> array(0,  8, 16, 24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,0),
				"1"=> array(0, 32, 48, 56, 64, 80, 96,112,128,144,160,176,192,224,256,0));
			break;
			case "11":
			$this->mpeg_ver = "1";
			$tbl_bit = array(
				"3"=> array(0, 32, 40, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,0),
				"2"=> array(0, 32, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,384,0),
				"1"=> array(0, 32, 64, 96,128,160,192,224,256,288,320,352,384,416,448,0));
			break;
		}

		switch (substr($header,13,2))
		{
			case "01":
				$this->layer = "3";
				break;
			case "10":
				$this->layer = "2";
				break;
			case "11":
				$this->layer = "1";
				break;
		}
		$this->bitrate = $tbl_bit[$this->layer][bindec(substr($header,16,4))];
		if ($this->bitrate == 0)
		{
			$second = -1;
		}
		else
		{
			$second = ((8*filesize($this->file))/1000)/$this->bitrate;
		}

		$this->filesize = filesize($this->file);

		$this->crc = substr($header,15,1);

		switch (substr($header,20,2))
		{
			case "00":
				switch ($this->mpeg_ver)
				{
					case "1":
						$this->sample = "44100";
						break;
					case "2":
						$this->sample = "22050";
						break;
					case "2.5":
						$this->sample = "11025";
						break;
				}
				break;
			case "01":
				switch ($this->mpeg_ver)
				{
					case "1":
						$this->sample = "48000";
						break;
					case "2":
						$this->sample = "24000";
						break;
					case "2.5":
						$this->sample = "12000";
						break;
				}
				break;
			case "10": 
				switch ($this->mpeg_ver)
				{
					case "1":
						$this->sample = "32000";
						break;
					case "2":
						$this->sample = "16000";
						break;
					case "2.5":
						$this->sample = "8000";
						break;
				}
				break;
			case "11":
				$this->sample = "Reserved";
				break;

		}

		switch (substr($header,24,2))
		{
			case "00":
				$this->chmode = "Stereo";
				break;
			case "01":
				$this->chmode = "Joint Stereo";
				break;
			case "10":
				$this->chmode = "Dual Channel";
				break;
			case "11":
				$this->chmode = "Mono";
				break;
		}

		$this->copyright = substr($header,28,1);
		$this->original  = substr($header,29,1);

		switch (substr($header,30,2))
		{
			case "00":
				$this->emphasis = "None";
				break;
			case "01":
				$this->emphasis = "50/15 ms";
				break;
			case "10":
				$this->emphasis = "Reserved";
				break;
			case "11":
				$this->emphasis = "CCIT J.17";
				break;
		}

		$this->length  = sprintf("%02d:%02d",floor($second/60),floor($second-(floor($second/60)*60)));
		$this->lengths = $second;
		$this->tag     = $id3tag['TAG'];
		$this->name    = $id3tag['NAME'];
		$this->artists = $id3tag['ARTISTS'];
		$this->album   = $id3tag['ALBUM'];
		$this->year    = $id3tag['YEAR'];
		$this->comment = $id3tag['COMMENT'];
		$this->genreno = $id3tag['GENRENO'];
		$this->genre   = $id3tag['GENRE'];
		if ($this->debug) print($this->debugend);
	}

	/*
	 * write - write the tag to a file
	 *
	 * if a file is specified then it is used else the file specified
	 * in $this->file is used.
	 *
	 * if there is an error it will return False and a message will be
	 * put in $this->error
	 */
	function write($file = False)
	{
		if ($this->debug) print($this->debugbeg . "write($file)<HR>\n");

		if ($file === False)
		{
			$file = $this->file;
		}

		if (!$file)
		{
			$this->error = 'File not specified';
			return False;
		}

		if (! ($f = fopen($file, 'r+b')) )
		{
			$this->error = 'Unable to open ' . $file;
			return False;
		}

		if (fseek($f, -128, SEEK_END) == -1)
		{
			$this->error = 'Unable to see to end - 128 of ' . $file;
			return False;
		}

		if (!$this->genreno) $this->genreno = 0xff;
		$this->genreno = $this->getgenreno($this->genre);

		$this->newtag = $this->encode();

		$r = fread($f, 128);

		if ($this->decode($r))
		{
			if (fseek($f, -128, SEEK_END) == -1)
			{
				$this->error = 'Unable to see to end - 128 of ' . $file;
				return False;
			}
			fwrite($f, $this->newtag);
		}
		else
		{
			if (fseek($f, 0, SEEK_END) == -1)
			{
				$this->error = 'Unable to see to end of ' . $file;
				return False;
			}
			fwrite($f, $this->newtag);
		}
		fclose($f);

		if ($this->debug) print($this->debugend);
    }

    /*
     * remove - removes the id3 tag from a file
     *
     * returns true if the tag was removed or none was found
     * else False if there was an error
     */
    function remove($file = False)
	{
		if ($this->debug) print($this->debugbeg . "remove()<HR>\n");

		if ($file === False)
		{
		    $file = $this->file;
		}

		if (!$file)
		{
		    $this->error = 'File not specified';
		    return False;
		}

		if (! ($f = fopen($file, 'r+b')) )
		{
		    $this->error = 'Unable to open ' . $file;
		    return False;
		}

		if (fseek($f, -128, SEEK_END) == -1)
		{
		    $this->error = 'Unable to see to end - 128 of ' . $file;
		    return False;
		}

		$r = fread($f, 128);

		$success = False;
		if ($this->decode($r))
		{
		    $size = filesize($this->file) - 128;
		    echo $size;
		    if ($size === False) echo 'NOPE!';
		    $success = ftruncate($f, $size);	
		}
		fclose($f);
		if ($this->debug) print($this->debugend);
		return $success;
    }

    /*
     * decode - decodes that ID3 tag
     *
     * False will be returned if there was an error decoding the tag
     * else an array will be returned
     */
    function decode($rawtag = False)
	{
		if ($this->debug) print($this->debugbeg . "decode($rawtag)<HR>\n");

		if ($rawtag === False)
		{
		    $rawtag = $this->rawtag;
		}

		$id3tag = unpack($this->id3unpack, $rawtag);
		if ($this->debug) print_r($id3tag);

		if ($id3tag['TAG'] == 'TAG')
		{
		    $id3tag['GENRE'] = $this->getgenre($id3tag['GENRENO']);
		}
		else
		{
		    $this->error = 'TAG not found';
		    $id3tag = False;
		}
		if ($this->debug) print($this->debugend);
		return $id3tag;
    }

    /*
     * encode - encode the ID3 tag
     *
     * the newly built tag will be returned
     */
    function encode()
	{
		if ($this->debug) print($this->debugbeg . "encode()<HR>\n");

		$this->tag = 'TAG'; // If other tags supported then change this
		$newtag = pack(
			$this->id3pack,
			$this->tag,
			$this->name,
			$this->artists,
			$this->album,
			$this->year,
			$this->comment,
			$this->genreno
		);

		if ($this->debug)
		{
		    if ($this->rawtag)
			{
				$unp = unpack('h*raw', $this->rawtag);
				print_r($unp);
		    }

		    $unp = unpack('h*new', $newtag);
		    print_r($unp);
		}

		if ($this->debug) print($this->debugend);
		return $newtag;
    }

    /*
     * getgenre - return the name of a genre number
     *
     * if no genre number is specified the genre number from
     * $this->genreno will be used.
     *
     * the genre is returned or False if an error or not found
     * no error message is ever returned
     */
    function getgenre($genreno = False)
	{
		if ($this->debug) print($this->debugbeg . "getgenre($genreno)<HR>\n");
		if ($genreno === False)
		{
		    $genreno = $this->genreno;
		}

		if (isset($this->genres[$genreno]))
		{
		    $genre = $this->genres[$genreno];
		    if ($this->debug) print($genre . "\n");
		}
		else
		{
		    $genre = False;
		}

		if ($this->debug) print($this->debugend);
		return $genre;
    }

    /*
     * getgenreno - return the number of the genre name
     *
     * if no genre name is specified the genre name from
     * $this->genre will be used.
     *
     * the genre number is returned or 0xff (255) if a match is not found
     * you can specify the default genreno to use if one is not found
     * no error message is ever returned
     */
    function getgenreno($genre = False, $default = 0xff)
	{
		if ($this->debug) print($this->debugbeg . "getgenreno($genreno)<HR>\n");

		if ($genreno === False)
		{
		    $genre = $this->genre;
		}

		$genreno = False;
		if ($genre)
		{
		    foreach ($this->genres as $no => $name)
			{
				if (strtolower($genre) == strtolower($name))
				{
				    $genreno = $no;
				}
		    }
		}
		if ($genreno === False) $genreno = $default;
		if ($this->debug) print($this->debugend);
		return $genreno;
    }
} // end of id3

?>
