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