[PLUGINS] ~maj plugins-dist
[lhc/web/www.git] / www / plugins-dist / medias / lib / getid3 / module.audio.dsdiff.php
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
18 if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
19 exit;
20 }
21
22 class 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 }