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.dsdiff.php                                     //
12// module for analyzing Direct Stream Digital Interchange      //
13// File Format (DSDIFF) files                                  //
14// dependencies: NONE                                          //
15//                                                            ///
16/////////////////////////////////////////////////////////////////
17
18if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
19	exit;
20}
21
22class getid3_dsdiff extends getid3_handler
23{
24	/**
25	 * @return bool
26	 */
27	public function Analyze() {
28		$info = &$this->getid3->info;
29
30		$this->fseek($info['avdataoffset']);
31		$DSDIFFheader = $this->fread(4);
32
33		// https://dsd-guide.com/sites/default/files/white-papers/DSDIFF_1.5_Spec.pdf
34		if (substr($DSDIFFheader, 0, 4) != 'FRM8') {
35			$this->error('Expecting "FRM8" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($DSDIFFheader, 0, 4)).'"');
36			return false;
37		}
38		unset($DSDIFFheader);
39		$this->fseek($info['avdataoffset']);
40
41		$info['encoding']                 = 'ISO-8859-1'; // not certain, but assumed
42		$info['fileformat']               = 'dsdiff';
43		$info['mime_type']                = 'audio/dsd';
44		$info['audio']['dataformat']      = 'dsdiff';
45		$info['audio']['bitrate_mode']    = 'cbr';
46		$info['audio']['bits_per_sample'] = 1;
47
48		$info['dsdiff'] = array();
49		while (!$this->feof() && ($ChunkHeader = $this->fread(12))) {
50			if (strlen($ChunkHeader) < 12) {
51				$this->error('Expecting chunk header at offset '.$thisChunk['offset'].', found insufficient data in file, aborting parsing');
52				break;
53			}
54			$thisChunk = array();
55			$thisChunk['offset'] = $this->ftell() - 12;
56			$thisChunk['name'] = substr($ChunkHeader, 0, 4);
57			if (!preg_match('#^[\\x21-\\x7E]+ *$#', $thisChunk['name'])) {
58				// "a concatenation of four printable ASCII characters in the range ' ' (space, 0x20) through '~'(0x7E). Space (0x20) cannot precede printing characters; trailing spaces are allowed."
59				$this->error('Invalid chunk name "'.$thisChunk['name'].'" ('.getid3_lib::PrintHexBytes($thisChunk['name']).') at offset '.$thisChunk['offset'].', aborting parsing');
60			}
61			$thisChunk['size'] = getid3_lib::BigEndian2Int(substr($ChunkHeader, 4, 8));
62			$datasize = $thisChunk['size'] + ($thisChunk['size'] % 2); // "If the data is an odd number of bytes in length, a pad byte must be added at the end. The pad byte is not included in ckDataSize."
63
64			switch ($thisChunk['name']) {
65				case 'FRM8':
66					$thisChunk['form_type'] = $this->fread(4);
67					if ($thisChunk['form_type'] != 'DSD ') {
68						$this->error('Expecting "DSD " at offset '.($this->ftell() - 4).', found "'.getid3_lib::PrintHexBytes($thisChunk['form_type']).'", aborting parsing');
69						break 2;
70					}
71					// do nothing further, prevent skipping subchunks
72					break;
73				case 'PROP': // PROPerty chunk
74					$thisChunk['prop_type'] = $this->fread(4);
75					if ($thisChunk['prop_type'] != 'SND ') {
76						$this->error('Expecting "SND " at offset '.($this->ftell() - 4).', found "'.getid3_lib::PrintHexBytes($thisChunk['prop_type']).'", aborting parsing');
77						break 2;
78					}
79					// do nothing further, prevent skipping subchunks
80					break;
81				case 'DIIN': // eDIted master INformation chunk
82					// do nothing, just prevent skipping subchunks
83					break;
84
85				case 'FVER': // Format VERsion chunk
86					if ($thisChunk['size'] == 4) {
87						$FVER = $this->fread(4);
88						$info['dsdiff']['format_version'] = ord($FVER[0]).'.'.ord($FVER[1]).'.'.ord($FVER[2]).'.'.ord($FVER[3]);
89						unset($FVER);
90					} else {
91						$this->warning('Expecting "FVER" chunk to be 4 bytes, found '.$thisChunk['size'].' bytes, skipping chunk');
92						$this->fseek($datasize, SEEK_CUR);
93					}
94					break;
95				case 'FS  ': // sample rate chunk
96					if ($thisChunk['size'] == 4) {
97						$info['dsdiff']['sample_rate'] = getid3_lib::BigEndian2Int($this->fread(4));
98						$info['audio']['sample_rate'] = $info['dsdiff']['sample_rate'];
99					} else {
100						$this->warning('Expecting "FVER" chunk to be 4 bytes, found '.$thisChunk['size'].' bytes, skipping chunk');
101						$this->fseek($datasize, SEEK_CUR);
102					}
103					break;
104				case 'CHNL': // CHaNneLs chunk
105					$thisChunk['num_channels'] = getid3_lib::BigEndian2Int($this->fread(2));
106					if ($thisChunk['num_channels'] == 0) {
107						$this->warning('channel count should be greater than zero, skipping chunk');
108						$this->fseek($datasize - 2, SEEK_CUR);
109					}
110					for ($i = 0; $i < $thisChunk['num_channels']; $i++) {
111						$thisChunk['channels'][$i] = $this->fread(4);
112					}
113					$info['audio']['channels'] = $thisChunk['num_channels'];
114					break;
115				case 'CMPR': // CoMPRession type chunk
116					$thisChunk['compression_type'] = $this->fread(4);
117					$info['audio']['dataformat'] = trim($thisChunk['compression_type']);
118					$humanReadableByteLength = getid3_lib::BigEndian2Int($this->fread(1));
119					$thisChunk['compression_name'] = $this->fread($humanReadableByteLength);
120					if (($humanReadableByteLength % 2) == 0) {
121						// need to seek to multiple of 2 bytes, human-readable string length is only one byte long so if the string is an even number of bytes we need to seek past a padding byte after the string
122						$this->fseek(1, SEEK_CUR);
123					}
124					unset($humanReadableByteLength);
125					break;
126				case 'ABSS': // ABSolute Start time chunk
127					$ABSS = $this->fread(8);
128					$info['dsdiff']['absolute_start_time']['hours']   = getid3_lib::BigEndian2Int(substr($ABSS, 0, 2));
129					$info['dsdiff']['absolute_start_time']['minutes'] = getid3_lib::BigEndian2Int(substr($ABSS, 2, 1));
130					$info['dsdiff']['absolute_start_time']['seconds'] = getid3_lib::BigEndian2Int(substr($ABSS, 3, 1));
131					$info['dsdiff']['absolute_start_time']['samples'] = getid3_lib::BigEndian2Int(substr($ABSS, 4, 4));
132					unset($ABSS);
133					break;
134				case 'LSCO': // LoudSpeaker COnfiguration chunk
135					// 0 = 2-channel stereo set-up
136					// 3 = 5-channel set-up according to ITU-R BS.775-1 [ITU]
137					// 4 = 6-channel set-up, 5-channel set-up according to ITU-R BS.775-1 [ITU], plus additional Low Frequency Enhancement (LFE) loudspeaker. Also known as "5.1 configuration"
138					// 65535 = Undefined channel set-up
139					$thisChunk['loundspeaker_config_id'] = getid3_lib::BigEndian2Int($this->fread(2));
140					break;
141				case 'COMT': // COMmenTs chunk
142					$thisChunk['num_comments'] = getid3_lib::BigEndian2Int($this->fread(2));
143					for ($i = 0; $i < $thisChunk['num_comments']; $i++) {
144						$thisComment = array();
145						$COMT = $this->fread(14);
146						$thisComment['creation_year']   = getid3_lib::BigEndian2Int(substr($COMT,  0, 2));
147						$thisComment['creation_month']  = getid3_lib::BigEndian2Int(substr($COMT,  2, 1));
148						$thisComment['creation_day']    = getid3_lib::BigEndian2Int(substr($COMT,  3, 1));
149						$thisComment['creation_hour']   = getid3_lib::BigEndian2Int(substr($COMT,  4, 1));
150						$thisComment['creation_minute'] = getid3_lib::BigEndian2Int(substr($COMT,  5, 1));
151						$thisComment['comment_type_id'] = getid3_lib::BigEndian2Int(substr($COMT,  6, 2));
152						$thisComment['comment_ref_id']  = getid3_lib::BigEndian2Int(substr($COMT,  8, 2));
153						$thisComment['string_length']   = getid3_lib::BigEndian2Int(substr($COMT, 10, 4));
154						$thisComment['comment_text'] = $this->fread($thisComment['string_length']);
155						if ($thisComment['string_length'] % 2) {
156							// commentText[] is the description of the Comment. This text must be padded with a byte at the end, if needed, to make it an even number of bytes long. This pad byte, if present, is not included in count.
157							$this->fseek(1, SEEK_CUR);
158						}
159						$thisComment['comment_type']      = $this->DSDIFFcmtType($thisComment['comment_type_id']);
160						$thisComment['comment_reference'] = $this->DSDIFFcmtRef($thisComment['comment_type_id'], $thisComment['comment_ref_id']);
161						$thisComment['creation_unix'] = mktime($thisComment['creation_hour'], $thisComment['creation_minute'], 0, $thisComment['creation_month'], $thisComment['creation_day'], $thisComment['creation_year']);
162						$thisChunk['comments'][$i] = $thisComment;
163
164						$commentkey = ($thisComment['comment_reference'] ?: 'comment');
165						$info['dsdiff']['comments'][$commentkey][] = $thisComment['comment_text'];
166						unset($thisComment);
167					}
168					break;
169				case 'MARK': // MARKer chunk
170					$MARK = $this->fread(22);
171					$thisChunk['marker_hours']   = getid3_lib::BigEndian2Int(substr($MARK,  0, 2));
172					$thisChunk['marker_minutes'] = getid3_lib::BigEndian2Int(substr($MARK,  2, 1));
173					$thisChunk['marker_seconds'] = getid3_lib::BigEndian2Int(substr($MARK,  3, 1));
174					$thisChunk['marker_samples'] = getid3_lib::BigEndian2Int(substr($MARK,  4, 4));
175					$thisChunk['marker_offset']  = getid3_lib::BigEndian2Int(substr($MARK,  8, 4));
176					$thisChunk['marker_type_id'] = getid3_lib::BigEndian2Int(substr($MARK, 12, 2));
177					$thisChunk['marker_channel'] = getid3_lib::BigEndian2Int(substr($MARK, 14, 2));
178					$thisChunk['marker_flagraw'] = getid3_lib::BigEndian2Int(substr($MARK, 16, 2));
179					$thisChunk['string_length']  = getid3_lib::BigEndian2Int(substr($MARK, 18, 4));
180					$thisChunk['description'] = ($thisChunk['string_length'] ? $this->fread($thisChunk['string_length']) : '');
181					if ($thisChunk['string_length'] % 2) {
182						// markerText[] is the description of the marker. This text must be padded with a byte at the end, if needed, to make it an even number of bytes long. This pad byte, if present, is not included in count.
183						$this->fseek(1, SEEK_CUR);
184					}
185					$thisChunk['marker_type'] = $this->DSDIFFmarkType($thisChunk['marker_type_id']);
186					unset($MARK);
187					break;
188				case 'DIAR': // artist chunk
189				case 'DITI': // title chunk
190					$thisChunk['string_length']  = getid3_lib::BigEndian2Int($this->fread(4));
191					$thisChunk['description'] = ($thisChunk['string_length'] ? $this->fread($thisChunk['string_length']) : '');
192					if ($thisChunk['string_length'] % 2) {
193						// This text must be padded with a byte at the end, if needed, to make it an even number of bytes long. This pad byte, if present, is not included in count.
194						$this->fseek(1, SEEK_CUR);
195					}
196
197					if ($commentkey = (($thisChunk['name'] == 'DIAR') ? 'artist' : (($thisChunk['name'] == 'DITI') ? 'title' : ''))) {
198						@$info['dsdiff']['comments'][$commentkey][] = $thisChunk['description'];
199					}
200					break;
201				case 'EMID': // Edited Master ID chunk
202					if ($thisChunk['size']) {
203						$thisChunk['identifier'] = $this->fread($thisChunk['size']);
204					}
205					break;
206
207				case 'ID3 ':
208					$endOfID3v2 = $this->ftell() + $datasize; // we will need to reset the filepointer after parsing ID3v2
209
210					getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true);
211					$getid3_temp = new getID3();
212					$getid3_temp->openfile($this->getid3->filename, null, $this->getid3->fp);
213					$getid3_id3v2 = new getid3_id3v2($getid3_temp);
214					$getid3_id3v2->StartingOffset = $this->ftell();
215					if ($thisChunk['valid'] = $getid3_id3v2->Analyze()) {
216						$info['id3v2'] = $getid3_temp->info['id3v2'];
217					}
218					unset($getid3_temp, $getid3_id3v2);
219
220					$this->fseek($endOfID3v2);
221					break;
222
223				case 'DSD ': // DSD sound data chunk
224				case 'DST ': // DST sound data chunk
225					// actual audio data, we're not interested, skip
226					$this->fseek($datasize, SEEK_CUR);
227					break;
228				default:
229					$this->warning('Unhandled chunk "'.$thisChunk['name'].'"');
230					$this->fseek($datasize, SEEK_CUR);
231					break;
232			}
233
234			@$info['dsdiff']['chunks'][] = $thisChunk;
235			//break;
236		}
237		if (empty($info['audio']['bitrate']) && !empty($info['audio']['channels']) && !empty($info['audio']['sample_rate']) && !empty($info['audio']['bits_per_sample'])) {
238			$info['audio']['bitrate'] = $info['audio']['bits_per_sample'] * $info['audio']['sample_rate'] * $info['audio']['channels'];
239		}
240
241		return true;
242	}
243
244	/**
245	 * @param int $cmtType
246	 *
247	 * @return string
248	 */
249	public static function DSDIFFcmtType($cmtType) {
250		static $DSDIFFcmtType = array(
251			0 => 'General (album) Comment',
252			1 => 'Channel Comment',
253			2 => 'Sound Source',
254			3 => 'File History',
255		);
256		return (isset($DSDIFFcmtType[$cmtType]) ? $DSDIFFcmtType[$cmtType] : 'reserved');
257	}
258
259	/**
260	 * @param int $cmtType
261	 * @param int $cmtRef
262	 *
263	 * @return string
264	 */
265	public static function DSDIFFcmtRef($cmtType, $cmtRef) {
266		static $DSDIFFcmtRef = array(
267			2 => array(  // Sound Source
268				0 => 'DSD recording',
269				1 => 'Analogue recording',
270				2 => 'PCM recording',
271			),
272			3 => array( // File History
273				0 => 'comment',   // General Remark
274				1 => 'encodeby',  // Name of the operator
275				2 => 'encoder',   // Name or type of the creating machine
276				3 => 'timezone',  // Time zone information
277				4 => 'revision',  // Revision of the file
278			),
279		);
280		switch ($cmtType) {
281			case 0:
282				// If the comment type is General Comment the comment reference must be 0
283				return '';
284			case 1:
285				// If the comment type is Channel Comment, the comment reference defines the channel number to which the comment belongs
286				return ($cmtRef ? 'channel '.$cmtRef : 'all channels');
287			case 2:
288			case 3:
289				return (isset($DSDIFFcmtRef[$cmtType][$cmtRef]) ? $DSDIFFcmtRef[$cmtType][$cmtRef] : 'reserved');
290		}
291		return 'unsupported $cmtType='.$cmtType;
292	}
293
294	/**
295	 * @param int $cmtType
296	 *
297	 * @return string
298	 */
299	public static function DSDIFFmarkType($markType) {
300		static $DSDIFFmarkType = array(
301			0 => 'TrackStart',   // Entry point for a Track start
302			1 => 'TrackStop',    // Entry point for ending a Track
303			2 => 'ProgramStart', // Start point of 2-channel or multi-channel area
304			3 => 'Obsolete',     //
305			4 => 'Index',        // Entry point of an Index
306		);
307		return (isset($DSDIFFmarkType[$markType]) ? $DSDIFFmarkType[$markType] : 'reserved');
308	}
309
310}
311