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.optimfrog.php // 12// module for analyzing OptimFROG audio files // 13// dependencies: module.audio.riff.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-video.riff.php', __FILE__, true); 21 22class getid3_optimfrog extends getid3_handler 23{ 24 /** 25 * @return bool 26 */ 27 public function Analyze() { 28 $info = &$this->getid3->info; 29 30 $info['fileformat'] = 'ofr'; 31 $info['audio']['dataformat'] = 'ofr'; 32 $info['audio']['bitrate_mode'] = 'vbr'; 33 $info['audio']['lossless'] = true; 34 35 $this->fseek($info['avdataoffset']); 36 $OFRheader = $this->fread(8); 37 if (substr($OFRheader, 0, 5) == '*RIFF') { 38 39 return $this->ParseOptimFROGheader42(); 40 41 } elseif (substr($OFRheader, 0, 3) == 'OFR') { 42 43 return $this->ParseOptimFROGheader45(); 44 45 } 46 47 $this->error('Expecting "*RIFF" or "OFR " at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($OFRheader).'"'); 48 unset($info['fileformat']); 49 return false; 50 } 51 52 /** 53 * @return bool 54 */ 55 public function ParseOptimFROGheader42() { 56 // for fileformat of v4.21 and older 57 58 $info = &$this->getid3->info; 59 $this->fseek($info['avdataoffset']); 60 $OptimFROGheaderData = $this->fread(45); 61 $info['avdataoffset'] = 45; 62 63 $OptimFROGencoderVersion_raw = getid3_lib::LittleEndian2Int(substr($OptimFROGheaderData, 0, 1)); 64 $OptimFROGencoderVersion_major = floor($OptimFROGencoderVersion_raw / 10); 65 $OptimFROGencoderVersion_minor = $OptimFROGencoderVersion_raw - ($OptimFROGencoderVersion_major * 10); 66 $RIFFdata = substr($OptimFROGheaderData, 1, 44); 67 $OrignalRIFFheaderSize = getid3_lib::LittleEndian2Int(substr($RIFFdata, 4, 4)) + 8; 68 $OrignalRIFFdataSize = getid3_lib::LittleEndian2Int(substr($RIFFdata, 40, 4)) + 44; 69 70 if ($OrignalRIFFheaderSize > $OrignalRIFFdataSize) { 71 $info['avdataend'] -= ($OrignalRIFFheaderSize - $OrignalRIFFdataSize); 72 $this->fseek($info['avdataend']); 73 $RIFFdata .= $this->fread($OrignalRIFFheaderSize - $OrignalRIFFdataSize); 74 } 75 76 // move the data chunk after all other chunks (if any) 77 // so that the RIFF parser doesn't see EOF when trying 78 // to skip over the data chunk 79 $RIFFdata = substr($RIFFdata, 0, 36).substr($RIFFdata, 44).substr($RIFFdata, 36, 8); 80 81 $getid3_temp = new getID3(); 82 $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); 83 $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; 84 $getid3_temp->info['avdataend'] = $info['avdataend']; 85 $getid3_riff = new getid3_riff($getid3_temp); 86 $getid3_riff->ParseRIFFdata($RIFFdata); 87 $info['riff'] = $getid3_temp->info['riff']; 88 89 $info['audio']['encoder'] = 'OptimFROG '.$OptimFROGencoderVersion_major.'.'.$OptimFROGencoderVersion_minor; 90 $info['audio']['channels'] = $info['riff']['audio'][0]['channels']; 91 $info['audio']['sample_rate'] = $info['riff']['audio'][0]['sample_rate']; 92 $info['audio']['bits_per_sample'] = $info['riff']['audio'][0]['bits_per_sample']; 93 $info['playtime_seconds'] = $OrignalRIFFdataSize / ($info['audio']['channels'] * $info['audio']['sample_rate'] * ($info['audio']['bits_per_sample'] / 8)); 94 $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; 95 96 unset($getid3_riff, $getid3_temp, $RIFFdata); 97 98 return true; 99 } 100 101 /** 102 * @return bool 103 */ 104 public function ParseOptimFROGheader45() { 105 // for fileformat of v4.50a and higher 106 107 $info = &$this->getid3->info; 108 $RIFFdata = ''; 109 $this->fseek($info['avdataoffset']); 110 while (!feof($this->getid3->fp) && ($this->ftell() < $info['avdataend'])) { 111 $BlockOffset = $this->ftell(); 112 $BlockData = $this->fread(8); 113 $offset = 8; 114 $BlockName = substr($BlockData, 0, 4); 115 $BlockSize = getid3_lib::LittleEndian2Int(substr($BlockData, 4, 4)); 116 117 if ($BlockName == 'OFRX') { 118 $BlockName = 'OFR '; 119 } 120 if (!isset($info['ofr'][$BlockName])) { 121 $info['ofr'][$BlockName] = array(); 122 } 123 $thisfile_ofr_thisblock = &$info['ofr'][$BlockName]; 124 125 switch ($BlockName) { 126 case 'OFR ': 127 128 // shortcut 129 $thisfile_ofr_thisblock['offset'] = $BlockOffset; 130 $thisfile_ofr_thisblock['size'] = $BlockSize; 131 132 $info['audio']['encoder'] = 'OptimFROG 4.50 alpha'; 133 switch ($BlockSize) { 134 case 12: 135 case 15: 136 // good 137 break; 138 139 default: 140 $this->warning('"'.$BlockName.'" contains more data than expected (expected 12 or 15 bytes, found '.$BlockSize.' bytes)'); 141 break; 142 } 143 $BlockData .= $this->fread($BlockSize); 144 145 $thisfile_ofr_thisblock['total_samples'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 6)); 146 $offset += 6; 147 $thisfile_ofr_thisblock['raw']['sample_type'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 1)); 148 $thisfile_ofr_thisblock['sample_type'] = $this->OptimFROGsampleTypeLookup($thisfile_ofr_thisblock['raw']['sample_type']); 149 $offset += 1; 150 $thisfile_ofr_thisblock['channel_config'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 1)); 151 $thisfile_ofr_thisblock['channels'] = $thisfile_ofr_thisblock['channel_config']; 152 $offset += 1; 153 $thisfile_ofr_thisblock['sample_rate'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 4)); 154 $offset += 4; 155 156 if ($BlockSize > 12) { 157 158 // OFR 4.504b or higher 159 $thisfile_ofr_thisblock['channels'] = $this->OptimFROGchannelConfigNumChannelsLookup($thisfile_ofr_thisblock['channel_config']); 160 $thisfile_ofr_thisblock['raw']['encoder_id'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 2)); 161 $thisfile_ofr_thisblock['encoder'] = $this->OptimFROGencoderNameLookup($thisfile_ofr_thisblock['raw']['encoder_id']); 162 $offset += 2; 163 $thisfile_ofr_thisblock['raw']['compression'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 1)); 164 $thisfile_ofr_thisblock['compression'] = $this->OptimFROGcompressionLookup($thisfile_ofr_thisblock['raw']['compression']); 165 $thisfile_ofr_thisblock['speedup'] = $this->OptimFROGspeedupLookup($thisfile_ofr_thisblock['raw']['compression']); 166 $offset += 1; 167 168 $info['audio']['encoder'] = 'OptimFROG '.$thisfile_ofr_thisblock['encoder']; 169 $info['audio']['encoder_options'] = '--mode '.$thisfile_ofr_thisblock['compression']; 170 171 if ((($thisfile_ofr_thisblock['raw']['encoder_id'] & 0xF0) >> 4) == 7) { // v4.507 172 if (strtolower(getid3_lib::fileextension($info['filename'])) == 'ofs') { 173 // OptimFROG DualStream format is lossy, but as of v4.507 there is no way to tell the difference 174 // between lossless and lossy other than the file extension. 175 $info['audio']['dataformat'] = 'ofs'; 176 $info['audio']['lossless'] = true; 177 } 178 } 179 180 } 181 182 $info['audio']['channels'] = $thisfile_ofr_thisblock['channels']; 183 $info['audio']['sample_rate'] = $thisfile_ofr_thisblock['sample_rate']; 184 $info['audio']['bits_per_sample'] = $this->OptimFROGbitsPerSampleTypeLookup($thisfile_ofr_thisblock['raw']['sample_type']); 185 break; 186 187 188 case 'COMP': 189 // unlike other block types, there CAN be multiple COMP blocks 190 191 $COMPdata['offset'] = $BlockOffset; 192 $COMPdata['size'] = $BlockSize; 193 194 if ($info['avdataoffset'] == 0) { 195 $info['avdataoffset'] = $BlockOffset; 196 } 197 198 // Only interested in first 14 bytes (only first 12 needed for v4.50 alpha), not actual audio data 199 $BlockData .= $this->fread(14); 200 $this->fseek($BlockSize - 14, SEEK_CUR); 201 202 $COMPdata['crc_32'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 4)); 203 $offset += 4; 204 $COMPdata['sample_count'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 4)); 205 $offset += 4; 206 $COMPdata['raw']['sample_type'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 1)); 207 $COMPdata['sample_type'] = $this->OptimFROGsampleTypeLookup($COMPdata['raw']['sample_type']); 208 $offset += 1; 209 $COMPdata['raw']['channel_configuration'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 1)); 210 $COMPdata['channel_configuration'] = $this->OptimFROGchannelConfigurationLookup($COMPdata['raw']['channel_configuration']); 211 $offset += 1; 212 $COMPdata['raw']['algorithm_id'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 2)); 213 //$COMPdata['algorithm'] = OptimFROGalgorithmNameLookup($COMPdata['raw']['algorithm_id']); 214 $offset += 2; 215 216 if ($info['ofr']['OFR ']['size'] > 12) { 217 218 // OFR 4.504b or higher 219 $COMPdata['raw']['encoder_id'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 2)); 220 $COMPdata['encoder'] = $this->OptimFROGencoderNameLookup($COMPdata['raw']['encoder_id']); 221 $offset += 2; 222 223 } 224 225 if ($COMPdata['crc_32'] == 0x454E4F4E) { 226 // ASCII value of 'NONE' - placeholder value in v4.50a 227 $COMPdata['crc_32'] = false; 228 } 229 230 $thisfile_ofr_thisblock[] = $COMPdata; 231 break; 232 233 case 'HEAD': 234 $thisfile_ofr_thisblock['offset'] = $BlockOffset; 235 $thisfile_ofr_thisblock['size'] = $BlockSize; 236 237 $RIFFdata .= $this->fread($BlockSize); 238 break; 239 240 case 'TAIL': 241 $thisfile_ofr_thisblock['offset'] = $BlockOffset; 242 $thisfile_ofr_thisblock['size'] = $BlockSize; 243 244 if ($BlockSize > 0) { 245 $RIFFdata .= $this->fread($BlockSize); 246 } 247 break; 248 249 case 'RECV': 250 // block contains no useful meta data - simply note and skip 251 252 $thisfile_ofr_thisblock['offset'] = $BlockOffset; 253 $thisfile_ofr_thisblock['size'] = $BlockSize; 254 255 $this->fseek($BlockSize, SEEK_CUR); 256 break; 257 258 259 case 'APET': 260 // APEtag v2 261 262 $thisfile_ofr_thisblock['offset'] = $BlockOffset; 263 $thisfile_ofr_thisblock['size'] = $BlockSize; 264 $this->warning('APEtag processing inside OptimFROG not supported in this version ('.$this->getid3->version().') of getID3()'); 265 266 $this->fseek($BlockSize, SEEK_CUR); 267 break; 268 269 270 case 'MD5 ': 271 // APEtag v2 272 273 $thisfile_ofr_thisblock['offset'] = $BlockOffset; 274 $thisfile_ofr_thisblock['size'] = $BlockSize; 275 276 if ($BlockSize == 16) { 277 278 $thisfile_ofr_thisblock['md5_binary'] = $this->fread($BlockSize); 279 $thisfile_ofr_thisblock['md5_string'] = getid3_lib::PrintHexBytes($thisfile_ofr_thisblock['md5_binary'], true, false, false); 280 $info['md5_data_source'] = $thisfile_ofr_thisblock['md5_string']; 281 282 } else { 283 284 $this->warning('Expecting block size of 16 in "MD5 " chunk, found '.$BlockSize.' instead'); 285 $this->fseek($BlockSize, SEEK_CUR); 286 287 } 288 break; 289 290 291 default: 292 $thisfile_ofr_thisblock['offset'] = $BlockOffset; 293 $thisfile_ofr_thisblock['size'] = $BlockSize; 294 295 $this->warning('Unhandled OptimFROG block type "'.$BlockName.'" at offset '.$thisfile_ofr_thisblock['offset']); 296 $this->fseek($BlockSize, SEEK_CUR); 297 break; 298 } 299 } 300 if (isset($info['ofr']['TAIL']['offset'])) { 301 $info['avdataend'] = $info['ofr']['TAIL']['offset']; 302 } 303 304 $info['playtime_seconds'] = (float) $info['ofr']['OFR ']['total_samples'] / ($info['audio']['channels'] * $info['audio']['sample_rate']); 305 $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; 306 307 // move the data chunk after all other chunks (if any) 308 // so that the RIFF parser doesn't see EOF when trying 309 // to skip over the data chunk 310 $RIFFdata = substr($RIFFdata, 0, 36).substr($RIFFdata, 44).substr($RIFFdata, 36, 8); 311 312 $getid3_temp = new getID3(); 313 $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); 314 $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; 315 $getid3_temp->info['avdataend'] = $info['avdataend']; 316 $getid3_riff = new getid3_riff($getid3_temp); 317 $getid3_riff->ParseRIFFdata($RIFFdata); 318 $info['riff'] = $getid3_temp->info['riff']; 319 320 unset($getid3_riff, $getid3_temp, $RIFFdata); 321 322 return true; 323 } 324 325 /** 326 * @param int $SampleType 327 * 328 * @return string|false 329 */ 330 public static function OptimFROGsampleTypeLookup($SampleType) { 331 static $OptimFROGsampleTypeLookup = array( 332 0 => 'unsigned int (8-bit)', 333 1 => 'signed int (8-bit)', 334 2 => 'unsigned int (16-bit)', 335 3 => 'signed int (16-bit)', 336 4 => 'unsigned int (24-bit)', 337 5 => 'signed int (24-bit)', 338 6 => 'unsigned int (32-bit)', 339 7 => 'signed int (32-bit)', 340 8 => 'float 0.24 (32-bit)', 341 9 => 'float 16.8 (32-bit)', 342 10 => 'float 24.0 (32-bit)' 343 ); 344 return (isset($OptimFROGsampleTypeLookup[$SampleType]) ? $OptimFROGsampleTypeLookup[$SampleType] : false); 345 } 346 347 /** 348 * @param int $SampleType 349 * 350 * @return int|false 351 */ 352 public static function OptimFROGbitsPerSampleTypeLookup($SampleType) { 353 static $OptimFROGbitsPerSampleTypeLookup = array( 354 0 => 8, 355 1 => 8, 356 2 => 16, 357 3 => 16, 358 4 => 24, 359 5 => 24, 360 6 => 32, 361 7 => 32, 362 8 => 32, 363 9 => 32, 364 10 => 32 365 ); 366 return (isset($OptimFROGbitsPerSampleTypeLookup[$SampleType]) ? $OptimFROGbitsPerSampleTypeLookup[$SampleType] : false); 367 } 368 369 /** 370 * @param int $ChannelConfiguration 371 * 372 * @return string|false 373 */ 374 public static function OptimFROGchannelConfigurationLookup($ChannelConfiguration) { 375 static $OptimFROGchannelConfigurationLookup = array( 376 0 => 'mono', 377 1 => 'stereo' 378 ); 379 return (isset($OptimFROGchannelConfigurationLookup[$ChannelConfiguration]) ? $OptimFROGchannelConfigurationLookup[$ChannelConfiguration] : false); 380 } 381 382 /** 383 * @param int $ChannelConfiguration 384 * 385 * @return int|false 386 */ 387 public static function OptimFROGchannelConfigNumChannelsLookup($ChannelConfiguration) { 388 static $OptimFROGchannelConfigNumChannelsLookup = array( 389 0 => 1, 390 1 => 2 391 ); 392 return (isset($OptimFROGchannelConfigNumChannelsLookup[$ChannelConfiguration]) ? $OptimFROGchannelConfigNumChannelsLookup[$ChannelConfiguration] : false); 393 } 394 395 396 // static function OptimFROGalgorithmNameLookup($AlgorithID) { 397 // static $OptimFROGalgorithmNameLookup = array(); 398 // return (isset($OptimFROGalgorithmNameLookup[$AlgorithID]) ? $OptimFROGalgorithmNameLookup[$AlgorithID] : false); 399 // } 400 401 402 /** 403 * @param int $EncoderID 404 * 405 * @return string 406 */ 407 public static function OptimFROGencoderNameLookup($EncoderID) { 408 // version = (encoderID >> 4) + 4500 409 // system = encoderID & 0xF 410 411 $EncoderVersion = number_format(((($EncoderID & 0xF0) >> 4) + 4500) / 1000, 3); 412 $EncoderSystemID = ($EncoderID & 0x0F); 413 414 static $OptimFROGencoderSystemLookup = array( 415 0x00 => 'Windows console', 416 0x01 => 'Linux console', 417 0x0F => 'unknown' 418 ); 419 return $EncoderVersion.' ('.(isset($OptimFROGencoderSystemLookup[$EncoderSystemID]) ? $OptimFROGencoderSystemLookup[$EncoderSystemID] : 'undefined encoder type (0x'.dechex($EncoderSystemID).')').')'; 420 } 421 422 /** 423 * @param int $CompressionID 424 * 425 * @return string 426 */ 427 public static function OptimFROGcompressionLookup($CompressionID) { 428 // mode = compression >> 3 429 // speedup = compression & 0x07 430 431 $CompressionModeID = ($CompressionID & 0xF8) >> 3; 432 //$CompressionSpeedupID = ($CompressionID & 0x07); 433 434 static $OptimFROGencoderModeLookup = array( 435 0x00 => 'fast', 436 0x01 => 'normal', 437 0x02 => 'high', 438 0x03 => 'extra', // extranew (some versions) 439 0x04 => 'best', // bestnew (some versions) 440 0x05 => 'ultra', 441 0x06 => 'insane', 442 0x07 => 'highnew', 443 0x08 => 'extranew', 444 0x09 => 'bestnew' 445 ); 446 return (isset($OptimFROGencoderModeLookup[$CompressionModeID]) ? $OptimFROGencoderModeLookup[$CompressionModeID] : 'undefined mode (0x'.str_pad(dechex($CompressionModeID), 2, '0', STR_PAD_LEFT).')'); 447 } 448 449 /** 450 * @param int $CompressionID 451 * 452 * @return string 453 */ 454 public static function OptimFROGspeedupLookup($CompressionID) { 455 // mode = compression >> 3 456 // speedup = compression & 0x07 457 458 //$CompressionModeID = ($CompressionID & 0xF8) >> 3; 459 $CompressionSpeedupID = ($CompressionID & 0x07); 460 461 static $OptimFROGencoderSpeedupLookup = array( 462 0x00 => '1x', 463 0x01 => '2x', 464 0x02 => '4x' 465 ); 466 return (isset($OptimFROGencoderSpeedupLookup[$CompressionSpeedupID]) ? $OptimFROGencoderSpeedupLookup[$CompressionSpeedupID] : 'undefined mode (0x'.dechex($CompressionSpeedupID)); 467 } 468 469} 470