1<?php
2/////////////////////////////////////////////////////////////////
3/// getID3() by James Heinrich <info@getid3.org>               //
4//  available at http://getid3.sourceforge.net                 //
5//            or http://www.getid3.org                         //
6/////////////////////////////////////////////////////////////////
7// See readme.txt for more details                             //
8/////////////////////////////////////////////////////////////////
9//                                                             //
10// module.tag.id3v1.php                                        //
11// module for analyzing ID3v1 tags                             //
12// dependencies: NONE                                          //
13//                                                            ///
14/////////////////////////////////////////////////////////////////
15
16
17class getid3_id3v1
18{
19
20	function getid3_id3v1(&$fd, &$ThisFileInfo) {
21
22		fseek($fd, -256, SEEK_END);
23		$preid3v1 = fread($fd, 128);
24		$id3v1tag = fread($fd, 128);
25
26		if (substr($id3v1tag, 0, 3) == 'TAG') {
27
28			$ThisFileInfo['avdataend'] = $ThisFileInfo['filesize'] - 128;
29
30			$ParsedID3v1['title']   = $this->cutfield(substr($id3v1tag,   3, 30));
31			$ParsedID3v1['artist']  = $this->cutfield(substr($id3v1tag,  33, 30));
32			$ParsedID3v1['album']   = $this->cutfield(substr($id3v1tag,  63, 30));
33			$ParsedID3v1['year']    = $this->cutfield(substr($id3v1tag,  93,  4));
34			$ParsedID3v1['comment'] =                 substr($id3v1tag,  97, 30);  // can't remove nulls yet, track detection depends on them
35			$ParsedID3v1['genreid'] =             ord(substr($id3v1tag, 127,  1));
36
37			// If second-last byte of comment field is null and last byte of comment field is non-null
38			// then this is ID3v1.1 and the comment field is 28 bytes long and the 30th byte is the track number
39			if (($id3v1tag{125} === "\x00") && ($id3v1tag{126} !== "\x00")) {
40				$ParsedID3v1['track']   = ord(substr($ParsedID3v1['comment'], 29,  1));
41				$ParsedID3v1['comment'] =     substr($ParsedID3v1['comment'],  0, 28);
42			}
43			$ParsedID3v1['comment'] = $this->cutfield($ParsedID3v1['comment']);
44
45			$ParsedID3v1['genre'] = $this->LookupGenreName($ParsedID3v1['genreid']);
46			if (!empty($ParsedID3v1['genre'])) {
47				unset($ParsedID3v1['genreid']);
48			}
49			if (empty($ParsedID3v1['genre']) || (@$ParsedID3v1['genre'] == 'Unknown')) {
50				unset($ParsedID3v1['genre']);
51			}
52
53			foreach ($ParsedID3v1 as $key => $value) {
54				$ParsedID3v1['comments'][$key][0] = $value;
55			}
56
57			// ID3v1 data is supposed to be padded with NULL characters, but some taggers pad with spaces
58			$GoodFormatID3v1tag = $this->GenerateID3v1Tag(
59											$ParsedID3v1['title'],
60											$ParsedID3v1['artist'],
61											$ParsedID3v1['album'],
62											$ParsedID3v1['year'],
63											$this->LookupGenreID(@$ParsedID3v1['genre']),
64											$ParsedID3v1['comment'],
65											@$ParsedID3v1['track']);
66			$ParsedID3v1['padding_valid'] = true;
67			if ($id3v1tag !== $GoodFormatID3v1tag) {
68				$ParsedID3v1['padding_valid'] = false;
69				$ThisFileInfo['warning'][] = 'Some ID3v1 fields do not use NULL characters for padding';
70			}
71
72			$ParsedID3v1['tag_offset_end']   = $ThisFileInfo['filesize'];
73			$ParsedID3v1['tag_offset_start'] = $ParsedID3v1['tag_offset_end'] - 128;
74
75			$ThisFileInfo['id3v1'] = $ParsedID3v1;
76		}
77
78		if (substr($preid3v1, 0, 3) == 'TAG') {
79			// The way iTunes handles tags is, well, brain-damaged.
80			// It completely ignores v1 if ID3v2 is present.
81			// This goes as far as adding a new v1 tag *even if there already is one*
82
83			// A suspected double-ID3v1 tag has been detected, but it could be that
84			// the "TAG" identifier is a legitimate part of an APE or Lyrics3 tag
85			if (substr($preid3v1, 96, 8) == 'APETAGEX') {
86				// an APE tag footer was found before the last ID3v1, assume false "TAG" synch
87			} elseif (substr($preid3v1, 119, 6) == 'LYRICS') {
88				// a Lyrics3 tag footer was found before the last ID3v1, assume false "TAG" synch
89			} else {
90				// APE and Lyrics3 footers not found - assume double ID3v1
91				$ThisFileInfo['warning'][] = 'Duplicate ID3v1 tag detected - this has been known to happen with iTunes';
92				$ThisFileInfo['avdataend'] -= 128;
93			}
94		}
95
96		return true;
97	}
98
99	function cutfield($str) {
100		return trim(substr($str, 0, strcspn($str, "\x00")));
101	}
102
103	function ArrayOfGenres($allowSCMPXextended=false) {
104		static $GenreLookup = array(
105			0    => 'Blues',
106			1    => 'Classic Rock',
107			2    => 'Country',
108			3    => 'Dance',
109			4    => 'Disco',
110			5    => 'Funk',
111			6    => 'Grunge',
112			7    => 'Hip-Hop',
113			8    => 'Jazz',
114			9    => 'Metal',
115			10   => 'New Age',
116			11   => 'Oldies',
117			12   => 'Other',
118			13   => 'Pop',
119			14   => 'R&B',
120			15   => 'Rap',
121			16   => 'Reggae',
122			17   => 'Rock',
123			18   => 'Techno',
124			19   => 'Industrial',
125			20   => 'Alternative',
126			21   => 'Ska',
127			22   => 'Death Metal',
128			23   => 'Pranks',
129			24   => 'Soundtrack',
130			25   => 'Euro-Techno',
131			26   => 'Ambient',
132			27   => 'Trip-Hop',
133			28   => 'Vocal',
134			29   => 'Jazz+Funk',
135			30   => 'Fusion',
136			31   => 'Trance',
137			32   => 'Classical',
138			33   => 'Instrumental',
139			34   => 'Acid',
140			35   => 'House',
141			36   => 'Game',
142			37   => 'Sound Clip',
143			38   => 'Gospel',
144			39   => 'Noise',
145			40   => 'Alt. Rock',
146			41   => 'Bass',
147			42   => 'Soul',
148			43   => 'Punk',
149			44   => 'Space',
150			45   => 'Meditative',
151			46   => 'Instrumental Pop',
152			47   => 'Instrumental Rock',
153			48   => 'Ethnic',
154			49   => 'Gothic',
155			50   => 'Darkwave',
156			51   => 'Techno-Industrial',
157			52   => 'Electronic',
158			53   => 'Pop-Folk',
159			54   => 'Eurodance',
160			55   => 'Dream',
161			56   => 'Southern Rock',
162			57   => 'Comedy',
163			58   => 'Cult',
164			59   => 'Gangsta Rap',
165			60   => 'Top 40',
166			61   => 'Christian Rap',
167			62   => 'Pop/Funk',
168			63   => 'Jungle',
169			64   => 'Native American',
170			65   => 'Cabaret',
171			66   => 'New Wave',
172			67   => 'Psychedelic',
173			68   => 'Rave',
174			69   => 'Showtunes',
175			70   => 'Trailer',
176			71   => 'Lo-Fi',
177			72   => 'Tribal',
178			73   => 'Acid Punk',
179			74   => 'Acid Jazz',
180			75   => 'Polka',
181			76   => 'Retro',
182			77   => 'Musical',
183			78   => 'Rock & Roll',
184			79   => 'Hard Rock',
185			80   => 'Folk',
186			81   => 'Folk/Rock',
187			82   => 'National Folk',
188			83   => 'Swing',
189			84   => 'Fast-Fusion',
190			85   => 'Bebob',
191			86   => 'Latin',
192			87   => 'Revival',
193			88   => 'Celtic',
194			89   => 'Bluegrass',
195			90   => 'Avantgarde',
196			91   => 'Gothic Rock',
197			92   => 'Progressive Rock',
198			93   => 'Psychedelic Rock',
199			94   => 'Symphonic Rock',
200			95   => 'Slow Rock',
201			96   => 'Big Band',
202			97   => 'Chorus',
203			98   => 'Easy Listening',
204			99   => 'Acoustic',
205			100  => 'Humour',
206			101  => 'Speech',
207			102  => 'Chanson',
208			103  => 'Opera',
209			104  => 'Chamber Music',
210			105  => 'Sonata',
211			106  => 'Symphony',
212			107  => 'Booty Bass',
213			108  => 'Primus',
214			109  => 'Porn Groove',
215			110  => 'Satire',
216			111  => 'Slow Jam',
217			112  => 'Club',
218			113  => 'Tango',
219			114  => 'Samba',
220			115  => 'Folklore',
221			116  => 'Ballad',
222			117  => 'Power Ballad',
223			118  => 'Rhythmic Soul',
224			119  => 'Freestyle',
225			120  => 'Duet',
226			121  => 'Punk Rock',
227			122  => 'Drum Solo',
228			123  => 'A Cappella',
229			124  => 'Euro-House',
230			125  => 'Dance Hall',
231			126  => 'Goa',
232			127  => 'Drum & Bass',
233			128  => 'Club-House',
234			129  => 'Hardcore',
235			130  => 'Terror',
236			131  => 'Indie',
237			132  => 'BritPop',
238			133  => 'Negerpunk',
239			134  => 'Polsk Punk',
240			135  => 'Beat',
241			136  => 'Christian Gangsta Rap',
242			137  => 'Heavy Metal',
243			138  => 'Black Metal',
244			139  => 'Crossover',
245			140  => 'Contemporary Christian',
246			141  => 'Christian Rock',
247			142  => 'Merengue',
248			143  => 'Salsa',
249			144  => 'Trash Metal',
250			145  => 'Anime',
251			146  => 'JPop',
252			147  => 'Synthpop',
253
254			255  => 'Unknown',
255
256			'CR' => 'Cover',
257			'RX' => 'Remix'
258		);
259
260		static $GenreLookupSCMPX = array();
261		if ($allowSCMPXextended && empty($GenreLookupSCMPX)) {
262			$GenreLookupSCMPX = $GenreLookup;
263			// http://www.geocities.co.jp/SiliconValley-Oakland/3664/alittle.html#GenreExtended
264			// Extended ID3v1 genres invented by SCMPX
265			// Note that 255 "Japanese Anime" conflicts with standard "Unknown"
266			$GenreLookupSCMPX[240] = 'Sacred';
267			$GenreLookupSCMPX[241] = 'Northern Europe';
268			$GenreLookupSCMPX[242] = 'Irish & Scottish';
269			$GenreLookupSCMPX[243] = 'Scotland';
270			$GenreLookupSCMPX[244] = 'Ethnic Europe';
271			$GenreLookupSCMPX[245] = 'Enka';
272			$GenreLookupSCMPX[246] = 'Children\'s Song';
273			$GenreLookupSCMPX[247] = 'Japanese Sky';
274			$GenreLookupSCMPX[248] = 'Japanese Heavy Rock';
275			$GenreLookupSCMPX[249] = 'Japanese Doom Rock';
276			$GenreLookupSCMPX[250] = 'Japanese J-POP';
277			$GenreLookupSCMPX[251] = 'Japanese Seiyu';
278			$GenreLookupSCMPX[252] = 'Japanese Ambient Techno';
279			$GenreLookupSCMPX[253] = 'Japanese Moemoe';
280			$GenreLookupSCMPX[254] = 'Japanese Tokusatsu';
281			//$GenreLookupSCMPX[255] = 'Japanese Anime';
282		}
283
284		return ($allowSCMPXextended ? $GenreLookupSCMPX : $GenreLookup);
285	}
286
287	function LookupGenreName($genreid, $allowSCMPXextended=true) {
288		switch ($genreid) {
289			case 'RX':
290			case 'CR':
291				break;
292			default:
293				$genreid = intval($genreid); // to handle 3 or '3' or '03'
294				break;
295		}
296		$GenreLookup = getid3_id3v1::ArrayOfGenres($allowSCMPXextended);
297		return (isset($GenreLookup[$genreid]) ? $GenreLookup[$genreid] : false);
298	}
299
300	function LookupGenreID($genre, $allowSCMPXextended=false) {
301		$GenreLookup = getid3_id3v1::ArrayOfGenres($allowSCMPXextended);
302		$LowerCaseNoSpaceSearchTerm = strtolower(str_replace(' ', '', $genre));
303		foreach ($GenreLookup as $key => $value) {
304			foreach ($GenreLookup as $key => $value) {
305				if (strtolower(str_replace(' ', '', $value)) == $LowerCaseNoSpaceSearchTerm) {
306					return $key;
307				}
308			}
309			return false;
310		}
311		return (isset($GenreLookup[$genreid]) ? $GenreLookup[$genreid] : false);
312	}
313
314	function StandardiseID3v1GenreName($OriginalGenre) {
315		if (($GenreID = getid3_id3v1::LookupGenreID($OriginalGenre)) !== false) {
316			return getid3_id3v1::LookupGenreName($GenreID);
317		}
318		return $OriginalGenre;
319	}
320
321	function GenerateID3v1Tag($title, $artist, $album, $year, $genreid, $comment, $track='') {
322		$ID3v1Tag  = 'TAG';
323		$ID3v1Tag .= str_pad(trim(substr($title,  0, 30)), 30, "\x00", STR_PAD_RIGHT);
324		$ID3v1Tag .= str_pad(trim(substr($artist, 0, 30)), 30, "\x00", STR_PAD_RIGHT);
325		$ID3v1Tag .= str_pad(trim(substr($album,  0, 30)), 30, "\x00", STR_PAD_RIGHT);
326		$ID3v1Tag .= str_pad(trim(substr($year,   0,  4)),  4, "\x00", STR_PAD_LEFT);
327		if (!empty($track) && ($track > 0) && ($track <= 255)) {
328			$ID3v1Tag .= str_pad(trim(substr($comment, 0, 28)), 28, "\x00", STR_PAD_RIGHT);
329			$ID3v1Tag .= "\x00";
330			if (gettype($track) == 'string') {
331				$track = (int) $track;
332			}
333			$ID3v1Tag .= chr($track);
334		} else {
335			$ID3v1Tag .= str_pad(trim(substr($comment, 0, 30)), 30, "\x00", STR_PAD_RIGHT);
336		}
337		if (($genreid < 0) || ($genreid > 147)) {
338			$genreid = 255; // 'unknown' genre
339		}
340		switch (gettype($genreid)) {
341			case 'string':
342			case 'integer':
343				$ID3v1Tag .= chr(intval($genreid));
344				break;
345			default:
346				$ID3v1Tag .= chr(255); // 'unknown' genre
347				break;
348		}
349
350		return $ID3v1Tag;
351	}
352
353}
354
355
356?>