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.wavpack.php // 12// module for analyzing WavPack v4.0+ Audio files // 13// dependencies: NONE // 14// /// 15///////////////////////////////////////////////////////////////// 16 17if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers 18 exit; 19} 20class getid3_wavpack extends getid3_handler 21{ 22 /** 23 * @return bool 24 */ 25 public function Analyze() { 26 $info = &$this->getid3->info; 27 28 $this->fseek($info['avdataoffset']); 29 30 while (true) { 31 32 $wavpackheader = $this->fread(32); 33 34 if ($this->ftell() >= $info['avdataend']) { 35 break; 36 } elseif (feof($this->getid3->fp)) { 37 break; 38 } elseif ( 39 isset($info['wavpack']['blockheader']['total_samples']) && 40 isset($info['wavpack']['blockheader']['block_samples']) && 41 ($info['wavpack']['blockheader']['total_samples'] > 0) && 42 ($info['wavpack']['blockheader']['block_samples'] > 0) && 43 (!isset($info['wavpack']['riff_trailer_size']) || ($info['wavpack']['riff_trailer_size'] <= 0)) && 44 ((isset($info['wavpack']['config_flags']['md5_checksum']) && ($info['wavpack']['config_flags']['md5_checksum'] === false)) || !empty($info['md5_data_source']))) { 45 break; 46 } 47 48 $blockheader_offset = $this->ftell() - 32; 49 $blockheader_magic = substr($wavpackheader, 0, 4); 50 $blockheader_size = getid3_lib::LittleEndian2Int(substr($wavpackheader, 4, 4)); 51 52 $magic = 'wvpk'; 53 if ($blockheader_magic != $magic) { 54 $this->error('Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$blockheader_offset.', found "'.getid3_lib::PrintHexBytes($blockheader_magic).'"'); 55 switch (isset($info['audio']['dataformat']) ? $info['audio']['dataformat'] : '') { 56 case 'wavpack': 57 case 'wvc': 58 break; 59 default: 60 unset($info['fileformat']); 61 unset($info['audio']); 62 unset($info['wavpack']); 63 break; 64 } 65 return false; 66 } 67 68 if (empty($info['wavpack']['blockheader']['block_samples']) || 69 empty($info['wavpack']['blockheader']['total_samples']) || 70 ($info['wavpack']['blockheader']['block_samples'] <= 0) || 71 ($info['wavpack']['blockheader']['total_samples'] <= 0)) { 72 // Also, it is possible that the first block might not have 73 // any samples (block_samples == 0) and in this case you should skip blocks 74 // until you find one with samples because the other information (like 75 // total_samples) are not guaranteed to be correct until (block_samples > 0) 76 77 // Finally, I have defined a format for files in which the length is not known 78 // (for example when raw files are created using pipes). In these cases 79 // total_samples will be -1 and you must seek to the final block to determine 80 // the total number of samples. 81 82 83 $info['audio']['dataformat'] = 'wavpack'; 84 $info['fileformat'] = 'wavpack'; 85 $info['audio']['lossless'] = true; 86 $info['audio']['bitrate_mode'] = 'vbr'; 87 88 $info['wavpack']['blockheader']['offset'] = $blockheader_offset; 89 $info['wavpack']['blockheader']['magic'] = $blockheader_magic; 90 $info['wavpack']['blockheader']['size'] = $blockheader_size; 91 92 if ($info['wavpack']['blockheader']['size'] >= 0x100000) { 93 $this->error('Expecting WavPack block size less than "0x100000", found "'.$info['wavpack']['blockheader']['size'].'" at offset '.$info['wavpack']['blockheader']['offset']); 94 switch (isset($info['audio']['dataformat']) ? $info['audio']['dataformat'] : '') { 95 case 'wavpack': 96 case 'wvc': 97 break; 98 default: 99 unset($info['fileformat']); 100 unset($info['audio']); 101 unset($info['wavpack']); 102 break; 103 } 104 return false; 105 } 106 107 $info['wavpack']['blockheader']['minor_version'] = ord($wavpackheader[8]); 108 $info['wavpack']['blockheader']['major_version'] = ord($wavpackheader[9]); 109 110 if (($info['wavpack']['blockheader']['major_version'] != 4) || 111 (($info['wavpack']['blockheader']['minor_version'] < 4) && 112 ($info['wavpack']['blockheader']['minor_version'] > 16))) { 113 $this->error('Expecting WavPack version between "4.2" and "4.16", found version "'.$info['wavpack']['blockheader']['major_version'].'.'.$info['wavpack']['blockheader']['minor_version'].'" at offset '.$info['wavpack']['blockheader']['offset']); 114 switch (isset($info['audio']['dataformat']) ? $info['audio']['dataformat'] : '') { 115 case 'wavpack': 116 case 'wvc': 117 break; 118 default: 119 unset($info['fileformat']); 120 unset($info['audio']); 121 unset($info['wavpack']); 122 break; 123 } 124 return false; 125 } 126 127 $info['wavpack']['blockheader']['track_number'] = ord($wavpackheader[10]); // unused 128 $info['wavpack']['blockheader']['index_number'] = ord($wavpackheader[11]); // unused 129 $info['wavpack']['blockheader']['total_samples'] = getid3_lib::LittleEndian2Int(substr($wavpackheader, 12, 4)); 130 $info['wavpack']['blockheader']['block_index'] = getid3_lib::LittleEndian2Int(substr($wavpackheader, 16, 4)); 131 $info['wavpack']['blockheader']['block_samples'] = getid3_lib::LittleEndian2Int(substr($wavpackheader, 20, 4)); 132 $info['wavpack']['blockheader']['flags_raw'] = getid3_lib::LittleEndian2Int(substr($wavpackheader, 24, 4)); 133 $info['wavpack']['blockheader']['crc'] = getid3_lib::LittleEndian2Int(substr($wavpackheader, 28, 4)); 134 135 $info['wavpack']['blockheader']['flags']['bytes_per_sample'] = 1 + ($info['wavpack']['blockheader']['flags_raw'] & 0x00000003); 136 $info['wavpack']['blockheader']['flags']['mono'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000004); 137 $info['wavpack']['blockheader']['flags']['hybrid'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000008); 138 $info['wavpack']['blockheader']['flags']['joint_stereo'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000010); 139 $info['wavpack']['blockheader']['flags']['cross_decorrelation'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000020); 140 $info['wavpack']['blockheader']['flags']['hybrid_noiseshape'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000040); 141 $info['wavpack']['blockheader']['flags']['ieee_32bit_float'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000080); 142 $info['wavpack']['blockheader']['flags']['int_32bit'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000100); 143 $info['wavpack']['blockheader']['flags']['hybrid_bitrate_noise'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000200); 144 $info['wavpack']['blockheader']['flags']['hybrid_balance_noise'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000400); 145 $info['wavpack']['blockheader']['flags']['multichannel_initial'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000800); 146 $info['wavpack']['blockheader']['flags']['multichannel_final'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00001000); 147 148 $info['audio']['lossless'] = !$info['wavpack']['blockheader']['flags']['hybrid']; 149 } 150 151 while (!feof($this->getid3->fp) && ($this->ftell() < ($blockheader_offset + $blockheader_size + 8))) { 152 153 $metablock = array('offset'=>$this->ftell()); 154 $metablockheader = $this->fread(2); 155 if (feof($this->getid3->fp)) { 156 break; 157 } 158 $metablock['id'] = ord($metablockheader[0]); 159 $metablock['function_id'] = ($metablock['id'] & 0x3F); 160 $metablock['function_name'] = $this->WavPackMetablockNameLookup($metablock['function_id']); 161 162 // The 0x20 bit in the id of the meta subblocks (which is defined as 163 // ID_OPTIONAL_DATA) is a permanent part of the id. The idea is that 164 // if a decoder encounters an id that it does not know about, it uses 165 // that "ID_OPTIONAL_DATA" flag to determine what to do. If it is set 166 // then the decoder simply ignores the metadata, but if it is zero 167 // then the decoder should quit because it means that an understanding 168 // of the metadata is required to correctly decode the audio. 169 $metablock['non_decoder'] = (bool) ($metablock['id'] & 0x20); 170 171 $metablock['padded_data'] = (bool) ($metablock['id'] & 0x40); 172 $metablock['large_block'] = (bool) ($metablock['id'] & 0x80); 173 if ($metablock['large_block']) { 174 $metablockheader .= $this->fread(2); 175 } 176 $metablock['size'] = getid3_lib::LittleEndian2Int(substr($metablockheader, 1)) * 2; // size is stored in words 177 $metablock['data'] = null; 178 $metablock['comments'] = array(); 179 180 if ($metablock['size'] > 0) { 181 182 switch ($metablock['function_id']) { 183 case 0x21: // ID_RIFF_HEADER 184 case 0x22: // ID_RIFF_TRAILER 185 case 0x23: // ID_REPLAY_GAIN 186 case 0x24: // ID_CUESHEET 187 case 0x25: // ID_CONFIG_BLOCK 188 case 0x26: // ID_MD5_CHECKSUM 189 $metablock['data'] = $this->fread($metablock['size']); 190 191 if ($metablock['padded_data']) { 192 // padded to the nearest even byte 193 $metablock['size']--; 194 $metablock['data'] = substr($metablock['data'], 0, -1); 195 } 196 break; 197 198 case 0x00: // ID_DUMMY 199 case 0x01: // ID_ENCODER_INFO 200 case 0x02: // ID_DECORR_TERMS 201 case 0x03: // ID_DECORR_WEIGHTS 202 case 0x04: // ID_DECORR_SAMPLES 203 case 0x05: // ID_ENTROPY_VARS 204 case 0x06: // ID_HYBRID_PROFILE 205 case 0x07: // ID_SHAPING_WEIGHTS 206 case 0x08: // ID_FLOAT_INFO 207 case 0x09: // ID_INT32_INFO 208 case 0x0A: // ID_WV_BITSTREAM 209 case 0x0B: // ID_WVC_BITSTREAM 210 case 0x0C: // ID_WVX_BITSTREAM 211 case 0x0D: // ID_CHANNEL_INFO 212 $this->fseek($metablock['offset'] + ($metablock['large_block'] ? 4 : 2) + $metablock['size']); 213 break; 214 215 default: 216 $this->warning('Unexpected metablock type "0x'.str_pad(dechex($metablock['function_id']), 2, '0', STR_PAD_LEFT).'" at offset '.$metablock['offset']); 217 $this->fseek($metablock['offset'] + ($metablock['large_block'] ? 4 : 2) + $metablock['size']); 218 break; 219 } 220 221 switch ($metablock['function_id']) { 222 case 0x21: // ID_RIFF_HEADER 223 getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); 224 $original_wav_filesize = getid3_lib::LittleEndian2Int(substr($metablock['data'], 4, 4)); 225 226 $getid3_temp = new getID3(); 227 $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); 228 $getid3_riff = new getid3_riff($getid3_temp); 229 $getid3_riff->ParseRIFFdata($metablock['data']); 230 $metablock['riff'] = $getid3_temp->info['riff']; 231 $info['audio']['sample_rate'] = $getid3_temp->info['riff']['raw']['fmt ']['nSamplesPerSec']; 232 unset($getid3_riff, $getid3_temp); 233 234 $metablock['riff']['original_filesize'] = $original_wav_filesize; 235 $info['wavpack']['riff_trailer_size'] = $original_wav_filesize - $metablock['riff']['WAVE']['data'][0]['size'] - $metablock['riff']['header_size']; 236 $info['playtime_seconds'] = $info['wavpack']['blockheader']['total_samples'] / $info['audio']['sample_rate']; 237 238 // Safe RIFF header in case there's a RIFF footer later 239 $metablockRIFFheader = $metablock['data']; 240 break; 241 242 243 case 0x22: // ID_RIFF_TRAILER 244 $metablockRIFFfooter = isset($metablockRIFFheader) ? $metablockRIFFheader : ''.$metablock['data']; 245 getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); 246 247 $startoffset = $metablock['offset'] + ($metablock['large_block'] ? 4 : 2); 248 $getid3_temp = new getID3(); 249 $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); 250 $getid3_temp->info['avdataend'] = $info['avdataend']; 251 //$getid3_temp->info['fileformat'] = 'riff'; 252 $getid3_riff = new getid3_riff($getid3_temp); 253 $metablock['riff'] = $getid3_riff->ParseRIFF($startoffset, $startoffset + $metablock['size']); 254 255 if (!empty($metablock['riff']['INFO'])) { 256 getid3_riff::parseComments($metablock['riff']['INFO'], $metablock['comments']); 257 $info['tags']['riff'] = $metablock['comments']; 258 } 259 unset($getid3_temp, $getid3_riff); 260 break; 261 262 263 case 0x23: // ID_REPLAY_GAIN 264 $this->warning('WavPack "Replay Gain" contents not yet handled by getID3() in metablock at offset '.$metablock['offset']); 265 break; 266 267 268 case 0x24: // ID_CUESHEET 269 $this->warning('WavPack "Cuesheet" contents not yet handled by getID3() in metablock at offset '.$metablock['offset']); 270 break; 271 272 273 case 0x25: // ID_CONFIG_BLOCK 274 $metablock['flags_raw'] = getid3_lib::LittleEndian2Int(substr($metablock['data'], 0, 3)); 275 276 $metablock['flags']['adobe_mode'] = (bool) ($metablock['flags_raw'] & 0x000001); // "adobe" mode for 32-bit floats 277 $metablock['flags']['fast_flag'] = (bool) ($metablock['flags_raw'] & 0x000002); // fast mode 278 $metablock['flags']['very_fast_flag'] = (bool) ($metablock['flags_raw'] & 0x000004); // double fast 279 $metablock['flags']['high_flag'] = (bool) ($metablock['flags_raw'] & 0x000008); // high quality mode 280 $metablock['flags']['very_high_flag'] = (bool) ($metablock['flags_raw'] & 0x000010); // double high (not used yet) 281 $metablock['flags']['bitrate_kbps'] = (bool) ($metablock['flags_raw'] & 0x000020); // bitrate is kbps, not bits / sample 282 $metablock['flags']['auto_shaping'] = (bool) ($metablock['flags_raw'] & 0x000040); // automatic noise shaping 283 $metablock['flags']['shape_override'] = (bool) ($metablock['flags_raw'] & 0x000080); // shaping mode specified 284 $metablock['flags']['joint_override'] = (bool) ($metablock['flags_raw'] & 0x000100); // joint-stereo mode specified 285 $metablock['flags']['copy_time'] = (bool) ($metablock['flags_raw'] & 0x000200); // copy file-time from source 286 $metablock['flags']['create_exe'] = (bool) ($metablock['flags_raw'] & 0x000400); // create executable 287 $metablock['flags']['create_wvc'] = (bool) ($metablock['flags_raw'] & 0x000800); // create correction file 288 $metablock['flags']['optimize_wvc'] = (bool) ($metablock['flags_raw'] & 0x001000); // maximize bybrid compression 289 $metablock['flags']['quality_mode'] = (bool) ($metablock['flags_raw'] & 0x002000); // psychoacoustic quality mode 290 $metablock['flags']['raw_flag'] = (bool) ($metablock['flags_raw'] & 0x004000); // raw mode (not implemented yet) 291 $metablock['flags']['calc_noise'] = (bool) ($metablock['flags_raw'] & 0x008000); // calc noise in hybrid mode 292 $metablock['flags']['lossy_mode'] = (bool) ($metablock['flags_raw'] & 0x010000); // obsolete (for information) 293 $metablock['flags']['extra_mode'] = (bool) ($metablock['flags_raw'] & 0x020000); // extra processing mode 294 $metablock['flags']['skip_wvx'] = (bool) ($metablock['flags_raw'] & 0x040000); // no wvx stream w/ floats & big ints 295 $metablock['flags']['md5_checksum'] = (bool) ($metablock['flags_raw'] & 0x080000); // compute & store MD5 signature 296 $metablock['flags']['quiet_mode'] = (bool) ($metablock['flags_raw'] & 0x100000); // don't report progress % 297 298 $info['wavpack']['config_flags'] = $metablock['flags']; 299 300 301 $info['audio']['encoder_options'] = ''; 302 if ($info['wavpack']['blockheader']['flags']['hybrid']) { 303 $info['audio']['encoder_options'] .= ' -b???'; 304 } 305 $info['audio']['encoder_options'] .= ($metablock['flags']['adobe_mode'] ? ' -a' : ''); 306 $info['audio']['encoder_options'] .= ($metablock['flags']['optimize_wvc'] ? ' -cc' : ''); 307 $info['audio']['encoder_options'] .= ($metablock['flags']['create_exe'] ? ' -e' : ''); 308 $info['audio']['encoder_options'] .= ($metablock['flags']['fast_flag'] ? ' -f' : ''); 309 $info['audio']['encoder_options'] .= ($metablock['flags']['joint_override'] ? ' -j?' : ''); 310 $info['audio']['encoder_options'] .= ($metablock['flags']['high_flag'] ? ' -h' : ''); 311 $info['audio']['encoder_options'] .= ($metablock['flags']['md5_checksum'] ? ' -m' : ''); 312 $info['audio']['encoder_options'] .= ($metablock['flags']['calc_noise'] ? ' -n' : ''); 313 $info['audio']['encoder_options'] .= ($metablock['flags']['shape_override'] ? ' -s?' : ''); 314 $info['audio']['encoder_options'] .= ($metablock['flags']['extra_mode'] ? ' -x?' : ''); 315 if (!empty($info['audio']['encoder_options'])) { 316 $info['audio']['encoder_options'] = trim($info['audio']['encoder_options']); 317 } elseif (isset($info['audio']['encoder_options'])) { 318 unset($info['audio']['encoder_options']); 319 } 320 break; 321 322 323 case 0x26: // ID_MD5_CHECKSUM 324 if (strlen($metablock['data']) == 16) { 325 $info['md5_data_source'] = strtolower(getid3_lib::PrintHexBytes($metablock['data'], true, false, false)); 326 } else { 327 $this->warning('Expecting 16 bytes of WavPack "MD5 Checksum" in metablock at offset '.$metablock['offset'].', but found '.strlen($metablock['data']).' bytes'); 328 } 329 break; 330 331 332 case 0x00: // ID_DUMMY 333 case 0x01: // ID_ENCODER_INFO 334 case 0x02: // ID_DECORR_TERMS 335 case 0x03: // ID_DECORR_WEIGHTS 336 case 0x04: // ID_DECORR_SAMPLES 337 case 0x05: // ID_ENTROPY_VARS 338 case 0x06: // ID_HYBRID_PROFILE 339 case 0x07: // ID_SHAPING_WEIGHTS 340 case 0x08: // ID_FLOAT_INFO 341 case 0x09: // ID_INT32_INFO 342 case 0x0A: // ID_WV_BITSTREAM 343 case 0x0B: // ID_WVC_BITSTREAM 344 case 0x0C: // ID_WVX_BITSTREAM 345 case 0x0D: // ID_CHANNEL_INFO 346 unset($metablock); 347 break; 348 } 349 350 } 351 if (!empty($metablock)) { 352 $info['wavpack']['metablocks'][] = $metablock; 353 } 354 355 } 356 357 } 358 359 $info['audio']['encoder'] = 'WavPack v'.$info['wavpack']['blockheader']['major_version'].'.'.str_pad($info['wavpack']['blockheader']['minor_version'], 2, '0', STR_PAD_LEFT); 360 $info['audio']['bits_per_sample'] = $info['wavpack']['blockheader']['flags']['bytes_per_sample'] * 8; 361 $info['audio']['channels'] = ($info['wavpack']['blockheader']['flags']['mono'] ? 1 : 2); 362 363 if (!empty($info['playtime_seconds'])) { 364 365 $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; 366 367 } else { 368 369 $info['audio']['dataformat'] = 'wvc'; 370 371 } 372 373 return true; 374 } 375 376 /** 377 * @param int $id 378 * 379 * @return string 380 */ 381 public function WavPackMetablockNameLookup(&$id) { 382 static $WavPackMetablockNameLookup = array( 383 0x00 => 'Dummy', 384 0x01 => 'Encoder Info', 385 0x02 => 'Decorrelation Terms', 386 0x03 => 'Decorrelation Weights', 387 0x04 => 'Decorrelation Samples', 388 0x05 => 'Entropy Variables', 389 0x06 => 'Hybrid Profile', 390 0x07 => 'Shaping Weights', 391 0x08 => 'Float Info', 392 0x09 => 'Int32 Info', 393 0x0A => 'WV Bitstream', 394 0x0B => 'WVC Bitstream', 395 0x0C => 'WVX Bitstream', 396 0x0D => 'Channel Info', 397 0x21 => 'RIFF header', 398 0x22 => 'RIFF trailer', 399 0x23 => 'Replay Gain', 400 0x24 => 'Cuesheet', 401 0x25 => 'Config Block', 402 0x26 => 'MD5 Checksum', 403 ); 404 return (isset($WavPackMetablockNameLookup[$id]) ? $WavPackMetablockNameLookup[$id] : ''); 405 } 406 407} 408