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