1<?php
2
3/////////////////////////////////////////////////////////////////
4/// getID3() by James Heinrich <info@getid3.org>               //
5//  available at https://github.com/JamesHeinrich/getID3       //
6//            or https://www.getid3.org                        //
7//            or http://getid3.sourceforge.net                 //
8//  see readme.txt for more details                            //
9/////////////////////////////////////////////////////////////////
10//                                                             //
11// module.audio.flac.php                                       //
12// module for analyzing FLAC and OggFLAC audio files           //
13// dependencies: module.audio.ogg.php                          //
14//                                                            ///
15/////////////////////////////////////////////////////////////////
16
17if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
18	exit;
19}
20getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.ogg.php', __FILE__, true);
21
22/**
23* @tutorial http://flac.sourceforge.net/format.html
24*/
25class getid3_flac extends getid3_handler
26{
27	const syncword = 'fLaC';
28
29	/**
30	 * @return bool
31	 */
32	public function Analyze() {
33		$info = &$this->getid3->info;
34
35		$this->fseek($info['avdataoffset']);
36		$StreamMarker = $this->fread(4);
37		if ($StreamMarker != self::syncword) {
38			return $this->error('Expecting "'.getid3_lib::PrintHexBytes(self::syncword).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($StreamMarker).'"');
39		}
40		$info['fileformat']            = 'flac';
41		$info['audio']['dataformat']   = 'flac';
42		$info['audio']['bitrate_mode'] = 'vbr';
43		$info['audio']['lossless']     = true;
44
45		// parse flac container
46		return $this->parseMETAdata();
47	}
48
49	/**
50	 * @return bool
51	 */
52	public function parseMETAdata() {
53		$info = &$this->getid3->info;
54		do {
55			$BlockOffset   = $this->ftell();
56			$BlockHeader   = $this->fread(4);
57			$LBFBT         = getid3_lib::BigEndian2Int(substr($BlockHeader, 0, 1));  // LBFBT = LastBlockFlag + BlockType
58			$LastBlockFlag = (bool) ($LBFBT & 0x80);
59			$BlockType     =        ($LBFBT & 0x7F);
60			$BlockLength   = getid3_lib::BigEndian2Int(substr($BlockHeader, 1, 3));
61			$BlockTypeText = self::metaBlockTypeLookup($BlockType);
62
63			if (($BlockOffset + 4 + $BlockLength) > $info['avdataend']) {
64				$this->warning('METADATA_BLOCK_HEADER.BLOCK_TYPE ('.$BlockTypeText.') at offset '.$BlockOffset.' extends beyond end of file');
65				break;
66			}
67			if ($BlockLength < 1) {
68				if ($BlockTypeText != 'reserved') {
69					// probably supposed to be zero-length
70					$this->warning('METADATA_BLOCK_HEADER.BLOCK_LENGTH ('.$BlockTypeText.') at offset '.$BlockOffset.' is zero bytes');
71					continue;
72				}
73				$this->error('METADATA_BLOCK_HEADER.BLOCK_LENGTH ('.$BlockLength.') at offset '.$BlockOffset.' is invalid');
74				break;
75			}
76
77			$info['flac'][$BlockTypeText]['raw'] = array();
78			$BlockTypeText_raw = &$info['flac'][$BlockTypeText]['raw'];
79
80			$BlockTypeText_raw['offset']          = $BlockOffset;
81			$BlockTypeText_raw['last_meta_block'] = $LastBlockFlag;
82			$BlockTypeText_raw['block_type']      = $BlockType;
83			$BlockTypeText_raw['block_type_text'] = $BlockTypeText;
84			$BlockTypeText_raw['block_length']    = $BlockLength;
85			if ($BlockTypeText_raw['block_type'] != 0x06) { // do not read attachment data automatically
86				$BlockTypeText_raw['block_data']  = $this->fread($BlockLength);
87			}
88
89			switch ($BlockTypeText) {
90				case 'STREAMINFO':     // 0x00
91					if (!$this->parseSTREAMINFO($BlockTypeText_raw['block_data'])) {
92						return false;
93					}
94					break;
95
96				case 'PADDING':        // 0x01
97					unset($info['flac']['PADDING']); // ignore
98					break;
99
100				case 'APPLICATION':    // 0x02
101					if (!$this->parseAPPLICATION($BlockTypeText_raw['block_data'])) {
102						return false;
103					}
104					break;
105
106				case 'SEEKTABLE':      // 0x03
107					if (!$this->parseSEEKTABLE($BlockTypeText_raw['block_data'])) {
108						return false;
109					}
110					break;
111
112				case 'VORBIS_COMMENT': // 0x04
113					if (!$this->parseVORBIS_COMMENT($BlockTypeText_raw['block_data'])) {
114						return false;
115					}
116					break;
117
118				case 'CUESHEET':       // 0x05
119					if (!$this->parseCUESHEET($BlockTypeText_raw['block_data'])) {
120						return false;
121					}
122					break;
123
124				case 'PICTURE':        // 0x06
125					if (!$this->parsePICTURE()) {
126						return false;
127					}
128					break;
129
130				default:
131					$this->warning('Unhandled METADATA_BLOCK_HEADER.BLOCK_TYPE ('.$BlockType.') at offset '.$BlockOffset);
132			}
133
134			unset($info['flac'][$BlockTypeText]['raw']);
135			$info['avdataoffset'] = $this->ftell();
136		}
137		while ($LastBlockFlag === false);
138
139		// handle tags
140		if (!empty($info['flac']['VORBIS_COMMENT']['comments'])) {
141			$info['flac']['comments'] = $info['flac']['VORBIS_COMMENT']['comments'];
142		}
143		if (!empty($info['flac']['VORBIS_COMMENT']['vendor'])) {
144			$info['audio']['encoder'] = str_replace('reference ', '', $info['flac']['VORBIS_COMMENT']['vendor']);
145		}
146
147		// copy attachments to 'comments' array if nesesary
148		if (isset($info['flac']['PICTURE']) && ($this->getid3->option_save_attachments !== getID3::ATTACHMENTS_NONE)) {
149			foreach ($info['flac']['PICTURE'] as $entry) {
150				if (!empty($entry['data'])) {
151					if (!isset($info['flac']['comments']['picture'])) {
152						$info['flac']['comments']['picture'] = array();
153					}
154					$comments_picture_data = array();
155					foreach (array('data', 'image_mime', 'image_width', 'image_height', 'imagetype', 'picturetype', 'description', 'datalength') as $picture_key) {
156						if (isset($entry[$picture_key])) {
157							$comments_picture_data[$picture_key] = $entry[$picture_key];
158						}
159					}
160					$info['flac']['comments']['picture'][] = $comments_picture_data;
161					unset($comments_picture_data);
162				}
163			}
164		}
165
166		if (isset($info['flac']['STREAMINFO'])) {
167			if (!$this->isDependencyFor('matroska')) {
168				$info['flac']['compressed_audio_bytes'] = $info['avdataend'] - $info['avdataoffset'];
169			}
170			$info['flac']['uncompressed_audio_bytes'] = $info['flac']['STREAMINFO']['samples_stream'] * $info['flac']['STREAMINFO']['channels'] * ($info['flac']['STREAMINFO']['bits_per_sample'] / 8);
171			if ($info['flac']['uncompressed_audio_bytes'] == 0) {
172				return $this->error('Corrupt FLAC file: uncompressed_audio_bytes == zero');
173			}
174			if (!empty($info['flac']['compressed_audio_bytes'])) {
175				$info['flac']['compression_ratio'] = $info['flac']['compressed_audio_bytes'] / $info['flac']['uncompressed_audio_bytes'];
176			}
177		}
178
179		// set md5_data_source - built into flac 0.5+
180		if (isset($info['flac']['STREAMINFO']['audio_signature'])) {
181
182			if ($info['flac']['STREAMINFO']['audio_signature'] === str_repeat("\x00", 16)) {
183				$this->warning('FLAC STREAMINFO.audio_signature is null (known issue with libOggFLAC)');
184			}
185			else {
186				$info['md5_data_source'] = '';
187				$md5 = $info['flac']['STREAMINFO']['audio_signature'];
188				for ($i = 0; $i < strlen($md5); $i++) {
189					$info['md5_data_source'] .= str_pad(dechex(ord($md5[$i])), 2, '00', STR_PAD_LEFT);
190				}
191				if (!preg_match('/^[0-9a-f]{32}$/', $info['md5_data_source'])) {
192					unset($info['md5_data_source']);
193				}
194			}
195		}
196
197		if (isset($info['flac']['STREAMINFO']['bits_per_sample'])) {
198			$info['audio']['bits_per_sample'] = $info['flac']['STREAMINFO']['bits_per_sample'];
199			if ($info['audio']['bits_per_sample'] == 8) {
200				// special case
201				// must invert sign bit on all data bytes before MD5'ing to match FLAC's calculated value
202				// MD5sum calculates on unsigned bytes, but FLAC calculated MD5 on 8-bit audio data as signed
203				$this->warning('FLAC calculates MD5 data strangely on 8-bit audio, so the stored md5_data_source value will not match the decoded WAV file');
204			}
205		}
206
207		return true;
208	}
209
210
211	/**
212	 * @param string $BlockData
213	 *
214	 * @return array
215	 */
216	public static function parseSTREAMINFOdata($BlockData) {
217		$streaminfo = array();
218		$streaminfo['min_block_size']  = getid3_lib::BigEndian2Int(substr($BlockData, 0, 2));
219		$streaminfo['max_block_size']  = getid3_lib::BigEndian2Int(substr($BlockData, 2, 2));
220		$streaminfo['min_frame_size']  = getid3_lib::BigEndian2Int(substr($BlockData, 4, 3));
221		$streaminfo['max_frame_size']  = getid3_lib::BigEndian2Int(substr($BlockData, 7, 3));
222
223		$SRCSBSS                       = getid3_lib::BigEndian2Bin(substr($BlockData, 10, 8));
224		$streaminfo['sample_rate']     = getid3_lib::Bin2Dec(substr($SRCSBSS,  0, 20));
225		$streaminfo['channels']        = getid3_lib::Bin2Dec(substr($SRCSBSS, 20,  3)) + 1;
226		$streaminfo['bits_per_sample'] = getid3_lib::Bin2Dec(substr($SRCSBSS, 23,  5)) + 1;
227		$streaminfo['samples_stream']  = getid3_lib::Bin2Dec(substr($SRCSBSS, 28, 36));
228
229		$streaminfo['audio_signature'] =                           substr($BlockData, 18, 16);
230
231		return $streaminfo;
232	}
233
234	/**
235	 * @param string $BlockData
236	 *
237	 * @return bool
238	 */
239	private function parseSTREAMINFO($BlockData) {
240		$info = &$this->getid3->info;
241
242		$info['flac']['STREAMINFO'] = self::parseSTREAMINFOdata($BlockData);
243
244		if (!empty($info['flac']['STREAMINFO']['sample_rate'])) {
245
246			$info['audio']['bitrate_mode']    = 'vbr';
247			$info['audio']['sample_rate']     = $info['flac']['STREAMINFO']['sample_rate'];
248			$info['audio']['channels']        = $info['flac']['STREAMINFO']['channels'];
249			$info['audio']['bits_per_sample'] = $info['flac']['STREAMINFO']['bits_per_sample'];
250			$info['playtime_seconds']         = $info['flac']['STREAMINFO']['samples_stream'] / $info['flac']['STREAMINFO']['sample_rate'];
251			if ($info['playtime_seconds'] > 0) {
252				if (!$this->isDependencyFor('matroska')) {
253					$info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds'];
254				}
255				else {
256					$this->warning('Cannot determine audio bitrate because total stream size is unknown');
257				}
258			}
259
260		} else {
261			return $this->error('Corrupt METAdata block: STREAMINFO');
262		}
263
264		return true;
265	}
266
267	/**
268	 * @param string $BlockData
269	 *
270	 * @return bool
271	 */
272	private function parseAPPLICATION($BlockData) {
273		$info = &$this->getid3->info;
274
275		$ApplicationID = getid3_lib::BigEndian2Int(substr($BlockData, 0, 4));
276		$info['flac']['APPLICATION'][$ApplicationID]['name'] = self::applicationIDLookup($ApplicationID);
277		$info['flac']['APPLICATION'][$ApplicationID]['data'] = substr($BlockData, 4);
278
279		return true;
280	}
281
282	/**
283	 * @param string $BlockData
284	 *
285	 * @return bool
286	 */
287	private function parseSEEKTABLE($BlockData) {
288		$info = &$this->getid3->info;
289
290		$offset = 0;
291		$BlockLength = strlen($BlockData);
292		$placeholderpattern = str_repeat("\xFF", 8);
293		while ($offset < $BlockLength) {
294			$SampleNumberString = substr($BlockData, $offset, 8);
295			$offset += 8;
296			if ($SampleNumberString == $placeholderpattern) {
297
298				// placeholder point
299				getid3_lib::safe_inc($info['flac']['SEEKTABLE']['placeholders'], 1);
300				$offset += 10;
301
302			} else {
303
304				$SampleNumber                                        = getid3_lib::BigEndian2Int($SampleNumberString);
305				$info['flac']['SEEKTABLE'][$SampleNumber]['offset']  = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8));
306				$offset += 8;
307				$info['flac']['SEEKTABLE'][$SampleNumber]['samples'] = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 2));
308				$offset += 2;
309
310			}
311		}
312
313		return true;
314	}
315
316	/**
317	 * @param string $BlockData
318	 *
319	 * @return bool
320	 */
321	private function parseVORBIS_COMMENT($BlockData) {
322		$info = &$this->getid3->info;
323
324		$getid3_ogg = new getid3_ogg($this->getid3);
325		if ($this->isDependencyFor('matroska')) {
326			$getid3_ogg->setStringMode($this->data_string);
327		}
328		$getid3_ogg->ParseVorbisComments();
329		if (isset($info['ogg'])) {
330			unset($info['ogg']['comments_raw']);
331			$info['flac']['VORBIS_COMMENT'] = $info['ogg'];
332			unset($info['ogg']);
333		}
334
335		unset($getid3_ogg);
336
337		return true;
338	}
339
340	/**
341	 * @param string $BlockData
342	 *
343	 * @return bool
344	 */
345	private function parseCUESHEET($BlockData) {
346		$info = &$this->getid3->info;
347		$offset = 0;
348		$info['flac']['CUESHEET']['media_catalog_number'] =                              trim(substr($BlockData, $offset, 128), "\0");
349		$offset += 128;
350		$info['flac']['CUESHEET']['lead_in_samples']      =         getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8));
351		$offset += 8;
352		$info['flac']['CUESHEET']['flags']['is_cd']       = (bool) (getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1)) & 0x80);
353		$offset += 1;
354
355		$offset += 258; // reserved
356
357		$info['flac']['CUESHEET']['number_tracks']        =         getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
358		$offset += 1;
359
360		for ($track = 0; $track < $info['flac']['CUESHEET']['number_tracks']; $track++) {
361			$TrackSampleOffset = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8));
362			$offset += 8;
363			$TrackNumber       = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
364			$offset += 1;
365
366			$info['flac']['CUESHEET']['tracks'][$TrackNumber]['sample_offset']         = $TrackSampleOffset;
367
368			$info['flac']['CUESHEET']['tracks'][$TrackNumber]['isrc']                  =                           substr($BlockData, $offset, 12);
369			$offset += 12;
370
371			$TrackFlagsRaw                                                             = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
372			$offset += 1;
373			$info['flac']['CUESHEET']['tracks'][$TrackNumber]['flags']['is_audio']     = (bool) ($TrackFlagsRaw & 0x80);
374			$info['flac']['CUESHEET']['tracks'][$TrackNumber]['flags']['pre_emphasis'] = (bool) ($TrackFlagsRaw & 0x40);
375
376			$offset += 13; // reserved
377
378			$info['flac']['CUESHEET']['tracks'][$TrackNumber]['index_points']          = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
379			$offset += 1;
380
381			for ($index = 0; $index < $info['flac']['CUESHEET']['tracks'][$TrackNumber]['index_points']; $index++) {
382				$IndexSampleOffset = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8));
383				$offset += 8;
384				$IndexNumber       = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
385				$offset += 1;
386
387				$offset += 3; // reserved
388
389				$info['flac']['CUESHEET']['tracks'][$TrackNumber]['indexes'][$IndexNumber] = $IndexSampleOffset;
390			}
391		}
392
393		return true;
394	}
395
396	/**
397	 * Parse METADATA_BLOCK_PICTURE flac structure and extract attachment
398	 * External usage: audio.ogg
399	 *
400	 * @return bool
401	 */
402	public function parsePICTURE() {
403		$info = &$this->getid3->info;
404
405		$picture['typeid']         = getid3_lib::BigEndian2Int($this->fread(4));
406		$picture['picturetype']    = self::pictureTypeLookup($picture['typeid']);
407		$picture['image_mime']     = $this->fread(getid3_lib::BigEndian2Int($this->fread(4)));
408		$descr_length              = getid3_lib::BigEndian2Int($this->fread(4));
409		if ($descr_length) {
410			$picture['description'] = $this->fread($descr_length);
411		}
412		$picture['image_width']    = getid3_lib::BigEndian2Int($this->fread(4));
413		$picture['image_height']   = getid3_lib::BigEndian2Int($this->fread(4));
414		$picture['color_depth']    = getid3_lib::BigEndian2Int($this->fread(4));
415		$picture['colors_indexed'] = getid3_lib::BigEndian2Int($this->fread(4));
416		$picture['datalength']     = getid3_lib::BigEndian2Int($this->fread(4));
417
418		if ($picture['image_mime'] == '-->') {
419			$picture['data'] = $this->fread($picture['datalength']);
420		} else {
421			$picture['data'] = $this->saveAttachment(
422				str_replace('/', '_', $picture['picturetype']).'_'.$this->ftell(),
423				$this->ftell(),
424				$picture['datalength'],
425				$picture['image_mime']);
426		}
427
428		$info['flac']['PICTURE'][] = $picture;
429
430		return true;
431	}
432
433	/**
434	 * @param int $blocktype
435	 *
436	 * @return string
437	 */
438	public static function metaBlockTypeLookup($blocktype) {
439		static $lookup = array(
440			0 => 'STREAMINFO',
441			1 => 'PADDING',
442			2 => 'APPLICATION',
443			3 => 'SEEKTABLE',
444			4 => 'VORBIS_COMMENT',
445			5 => 'CUESHEET',
446			6 => 'PICTURE',
447		);
448		return (isset($lookup[$blocktype]) ? $lookup[$blocktype] : 'reserved');
449	}
450
451	/**
452	 * @param int $applicationid
453	 *
454	 * @return string
455	 */
456	public static function applicationIDLookup($applicationid) {
457		// http://flac.sourceforge.net/id.html
458		static $lookup = array(
459			0x41544348 => 'FlacFile',                                                                           // "ATCH"
460			0x42534F4C => 'beSolo',                                                                             // "BSOL"
461			0x42554753 => 'Bugs Player',                                                                        // "BUGS"
462			0x43756573 => 'GoldWave cue points (specification)',                                                // "Cues"
463			0x46696361 => 'CUE Splitter',                                                                       // "Fica"
464			0x46746F6C => 'flac-tools',                                                                         // "Ftol"
465			0x4D4F5442 => 'MOTB MetaCzar',                                                                      // "MOTB"
466			0x4D505345 => 'MP3 Stream Editor',                                                                  // "MPSE"
467			0x4D754D4C => 'MusicML: Music Metadata Language',                                                   // "MuML"
468			0x52494646 => 'Sound Devices RIFF chunk storage',                                                   // "RIFF"
469			0x5346464C => 'Sound Font FLAC',                                                                    // "SFFL"
470			0x534F4E59 => 'Sony Creative Software',                                                             // "SONY"
471			0x5351455A => 'flacsqueeze',                                                                        // "SQEZ"
472			0x54745776 => 'TwistedWave',                                                                        // "TtWv"
473			0x55495453 => 'UITS Embedding tools',                                                               // "UITS"
474			0x61696666 => 'FLAC AIFF chunk storage',                                                            // "aiff"
475			0x696D6167 => 'flac-image application for storing arbitrary files in APPLICATION metadata blocks',  // "imag"
476			0x7065656D => 'Parseable Embedded Extensible Metadata (specification)',                             // "peem"
477			0x71667374 => 'QFLAC Studio',                                                                       // "qfst"
478			0x72696666 => 'FLAC RIFF chunk storage',                                                            // "riff"
479			0x74756E65 => 'TagTuner',                                                                           // "tune"
480			0x78626174 => 'XBAT',                                                                               // "xbat"
481			0x786D6364 => 'xmcd',                                                                               // "xmcd"
482		);
483		return (isset($lookup[$applicationid]) ? $lookup[$applicationid] : 'reserved');
484	}
485
486	/**
487	 * @param int $type_id
488	 *
489	 * @return string
490	 */
491	public static function pictureTypeLookup($type_id) {
492		static $lookup = array (
493			 0 => 'Other',
494			 1 => '32x32 pixels \'file icon\' (PNG only)',
495			 2 => 'Other file icon',
496			 3 => 'Cover (front)',
497			 4 => 'Cover (back)',
498			 5 => 'Leaflet page',
499			 6 => 'Media (e.g. label side of CD)',
500			 7 => 'Lead artist/lead performer/soloist',
501			 8 => 'Artist/performer',
502			 9 => 'Conductor',
503			10 => 'Band/Orchestra',
504			11 => 'Composer',
505			12 => 'Lyricist/text writer',
506			13 => 'Recording Location',
507			14 => 'During recording',
508			15 => 'During performance',
509			16 => 'Movie/video screen capture',
510			17 => 'A bright coloured fish',
511			18 => 'Illustration',
512			19 => 'Band/artist logotype',
513			20 => 'Publisher/Studio logotype',
514		);
515		return (isset($lookup[$type_id]) ? $lookup[$type_id] : 'reserved');
516	}
517
518}
519