public function RemoveID3v1() { // File MUST be writeable - CHMOD(646) at least if (!empty($this->filename) && is_readable($this->filename) && is_writable($this->filename) && is_file($this->filename)) { $this->setRealFileSize(); if ($this->filesize <= 0 || !Helper::intValueSupported($this->filesize)) { $this->errors[] = 'Unable to RemoveID3v1(' . $this->filename . ') because filesize (' . $this->filesize . ') is larger than ' . round(PHP_INT_MAX / 1073741824) . 'GB'; return false; } if ($fp_source = fopen($this->filename, 'r+b')) { fseek($fp_source, -128, SEEK_END); if (fread($fp_source, 3) == 'TAG') { ftruncate($fp_source, $this->filesize - 128); } else { // no ID3v1 tag to begin with - do nothing } fclose($fp_source); return true; } else { $this->errors[] = 'Could not fopen(' . $this->filename . ', "r+b")'; } } else { $this->errors[] = $this->filename . ' is not writeable'; } return false; }
/** * @return bool */ public function analyze() { $info =& $this->getid3->info; $info['fileformat'] = 'quicktime'; $info['quicktime']['hinting'] = false; $info['quicktime']['controller'] = 'standard'; // may be overridden if 'ctyp' atom is present fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); $offset = 0; $atomcounter = 0; while ($offset < $info['avdataend']) { if (!Helper::intValueSupported($offset)) { $info['error'][] = 'Unable to parse atom at offset ' . $offset . ' because beyond ' . round(PHP_INT_MAX / 1073741824) . 'GB limit of PHP filesystem functions'; break; } fseek($this->getid3->fp, $offset, SEEK_SET); $AtomHeader = fread($this->getid3->fp, 8); $atomsize = Helper::BigEndian2Int(substr($AtomHeader, 0, 4)); $atomname = substr($AtomHeader, 4, 4); // 64-bit MOV patch by jlegateØktnc*com if ($atomsize == 1) { $atomsize = Helper::BigEndian2Int(fread($this->getid3->fp, 8)); } $info['quicktime'][$atomname]['name'] = $atomname; $info['quicktime'][$atomname]['size'] = $atomsize; $info['quicktime'][$atomname]['offset'] = $offset; if ($offset + $atomsize > $info['avdataend']) { $info['error'][] = 'Atom at offset ' . $offset . ' claims to go beyond end-of-file (length: ' . $atomsize . ' bytes)'; return false; } if ($atomsize == 0) { // Furthermore, for historical reasons the list of atoms is optionally // terminated by a 32-bit integer set to 0. If you are writing a program // to read user data atoms, you should allow for the terminating 0. break; } switch ($atomname) { case 'mdat': // Media DATa atom // 'mdat' contains the actual data for the audio/video if ($atomsize > 8 && (!isset($info['avdataend_tmp']) || $info['quicktime'][$atomname]['size'] > $info['avdataend_tmp'] - $info['avdataoffset'])) { $info['avdataoffset'] = $info['quicktime'][$atomname]['offset'] + 8; $OldAVDataEnd = $info['avdataend']; $info['avdataend'] = $info['quicktime'][$atomname]['offset'] + $info['quicktime'][$atomname]['size']; $getid3_temp = new GetId3Core(); $getid3_temp->openfile($this->getid3->filename); $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; $getid3_temp->info['avdataend'] = $info['avdataend']; $getid3_mp3 = new Module\Audio\Mp3($getid3_temp); if ($getid3_mp3->MPEGaudioHeaderValid($getid3_mp3->MPEGaudioHeaderDecode(fread($this->getid3->fp, 4)))) { $getid3_mp3->getOnlyMPEGaudioInfo($getid3_temp->info['avdataoffset'], false); if (!empty($getid3_temp->info['warning'])) { foreach ($getid3_temp->info['warning'] as $value) { $info['warning'][] = $value; } } if (!empty($getid3_temp->info['mpeg'])) { $info['mpeg'] = $getid3_temp->info['mpeg']; if (isset($info['mpeg']['audio'])) { $info['audio']['dataformat'] = 'mp3'; $info['audio']['codec'] = !empty($info['mpeg']['audio']['encoder']) ? $info['mpeg']['audio']['encoder'] : (!empty($info['mpeg']['audio']['codec']) ? $info['mpeg']['audio']['codec'] : (!empty($info['mpeg']['audio']['LAME']) ? 'LAME' : 'mp3')); $info['audio']['sample_rate'] = $info['mpeg']['audio']['sample_rate']; $info['audio']['channels'] = $info['mpeg']['audio']['channels']; $info['audio']['bitrate'] = $info['mpeg']['audio']['bitrate']; $info['audio']['bitrate_mode'] = strtolower($info['mpeg']['audio']['bitrate_mode']); $info['bitrate'] = $info['audio']['bitrate']; } } } unset($getid3_mp3, $getid3_temp); $info['avdataend'] = $OldAVDataEnd; unset($OldAVDataEnd); } break; case 'free': // FREE space atom // FREE space atom case 'skip': // SKIP atom // SKIP atom case 'wide': // 64-bit expansion placeholder atom // 'free', 'skip' and 'wide' are just padding, contains no useful data at all break; default: $atomHierarchy = array(); $info['quicktime'][$atomname] = $this->QuicktimeParseAtom($atomname, $atomsize, fread($this->getid3->fp, $atomsize), $offset, $atomHierarchy, $this->ParseAllPossibleAtoms); break; } $offset += $atomsize; ++$atomcounter; } if (!empty($info['avdataend_tmp'])) { // this value is assigned to a temp value and then erased because // otherwise any atoms beyond the 'mdat' atom would not get parsed $info['avdataend'] = $info['avdataend_tmp']; unset($info['avdataend_tmp']); } if (!isset($info['bitrate']) && isset($info['playtime_seconds'])) { $info['bitrate'] = ($info['avdataend'] - $info['avdataoffset']) * 8 / $info['playtime_seconds']; } if (isset($info['bitrate']) && !isset($info['audio']['bitrate']) && !isset($info['quicktime']['video'])) { $info['audio']['bitrate'] = $info['bitrate']; } if (!empty($info['playtime_seconds']) && !isset($info['video']['frame_rate']) && !empty($info['quicktime']['stts_framecount'])) { foreach ($info['quicktime']['stts_framecount'] as $key => $samples_count) { $samples_per_second = $samples_count / $info['playtime_seconds']; if ($samples_per_second > 240) { // has to be audio samples } else { $info['video']['frame_rate'] = $samples_per_second; break; } } } if ($info['audio']['dataformat'] == 'mp4' && empty($info['video']['resolution_x'])) { $info['fileformat'] = 'mp4'; $info['mime_type'] = 'audio/mp4'; unset($info['video']['dataformat']); } if (!$this->ReturnAtomData) { unset($info['quicktime']['moov']); } if (empty($info['audio']['dataformat']) && !empty($info['quicktime']['audio'])) { $info['audio']['dataformat'] = 'quicktime'; } if (empty($info['video']['dataformat']) && !empty($info['quicktime']['video'])) { $info['video']['dataformat'] = 'quicktime'; } return true; }
/** * @return bool */ public function analyze() { $info =& $this->getid3->info; // shortcut $info['bonk'] = array(); $thisfile_bonk =& $info['bonk']; $thisfile_bonk['dataoffset'] = $info['avdataoffset']; $thisfile_bonk['dataend'] = $info['avdataend']; if (!Helper::intValueSupported($thisfile_bonk['dataend'])) { $info['warning'][] = 'Unable to parse BONK file from end (v0.6+ preferred method) because PHP filesystem functions only support up to ' . round(PHP_INT_MAX / 1073741824) . 'GB'; } else { // scan-from-end method, for v0.6 and higher fseek($this->getid3->fp, $thisfile_bonk['dataend'] - 8, SEEK_SET); $PossibleBonkTag = fread($this->getid3->fp, 8); while ($this->BonkIsValidTagName(substr($PossibleBonkTag, 4, 4), true)) { $BonkTagSize = Helper::LittleEndian2Int(substr($PossibleBonkTag, 0, 4)); fseek($this->getid3->fp, 0 - $BonkTagSize, SEEK_CUR); $BonkTagOffset = ftell($this->getid3->fp); $TagHeaderTest = fread($this->getid3->fp, 5); if ($TagHeaderTest[0] != "" || substr($PossibleBonkTag, 4, 4) != strtolower(substr($PossibleBonkTag, 4, 4))) { $info['error'][] = 'Expecting "' . Helper::PrintHexBytes("" . strtoupper(substr($PossibleBonkTag, 4, 4))) . '" at offset ' . $BonkTagOffset . ', found "' . Helper::PrintHexBytes($TagHeaderTest) . '"'; return false; } $BonkTagName = substr($TagHeaderTest, 1, 4); $thisfile_bonk[$BonkTagName]['size'] = $BonkTagSize; $thisfile_bonk[$BonkTagName]['offset'] = $BonkTagOffset; $this->HandleBonkTags($BonkTagName); $NextTagEndOffset = $BonkTagOffset - 8; if ($NextTagEndOffset < $thisfile_bonk['dataoffset']) { if (empty($info['audio']['encoder'])) { $info['audio']['encoder'] = 'Extended BONK v0.9+'; } return true; } fseek($this->getid3->fp, $NextTagEndOffset, SEEK_SET); $PossibleBonkTag = fread($this->getid3->fp, 8); } } // seek-from-beginning method for v0.4 and v0.5 if (empty($thisfile_bonk['BONK'])) { fseek($this->getid3->fp, $thisfile_bonk['dataoffset'], SEEK_SET); do { $TagHeaderTest = fread($this->getid3->fp, 5); switch ($TagHeaderTest) { case "" . 'BONK': if (empty($info['audio']['encoder'])) { $info['audio']['encoder'] = 'BONK v0.4'; } break; case "" . 'INFO': $info['audio']['encoder'] = 'Extended BONK v0.5'; break; default: break 2; } $BonkTagName = substr($TagHeaderTest, 1, 4); $thisfile_bonk[$BonkTagName]['size'] = $thisfile_bonk['dataend'] - $thisfile_bonk['dataoffset']; $thisfile_bonk[$BonkTagName]['offset'] = $thisfile_bonk['dataoffset']; $this->HandleBonkTags($BonkTagName); } while (true); } // parse META block for v0.6 - v0.8 if (empty($thisfile_bonk['INFO']) && isset($thisfile_bonk['META']['tags']['info'])) { fseek($this->getid3->fp, $thisfile_bonk['META']['tags']['info'], SEEK_SET); $TagHeaderTest = fread($this->getid3->fp, 5); if ($TagHeaderTest == "" . 'INFO') { $info['audio']['encoder'] = 'Extended BONK v0.6 - v0.8'; $BonkTagName = substr($TagHeaderTest, 1, 4); $thisfile_bonk[$BonkTagName]['size'] = $thisfile_bonk['dataend'] - $thisfile_bonk['dataoffset']; $thisfile_bonk[$BonkTagName]['offset'] = $thisfile_bonk['dataoffset']; $this->HandleBonkTags($BonkTagName); } } if (empty($info['audio']['encoder'])) { $info['audio']['encoder'] = 'Extended BONK v0.9+'; } if (empty($thisfile_bonk['BONK'])) { unset($info['bonk']); } return true; }
/** * @param type $startoffset * @param type $maxoffset * * @return bool */ public function ParseRIFF($startoffset, $maxoffset) { $info =& $this->getid3->info; $maxoffset = min($maxoffset, $info['avdataend']); $RIFFchunk = false; $FoundAllChunksWeNeed = false; if ($startoffset < 0 || !Helper::intValueSupported($startoffset)) { $info['warning'][] = 'Unable to ParseRIFF() at ' . $startoffset . ' because beyond ' . round(PHP_INT_MAX / 1073741824) . 'GB limit of PHP filesystem functions'; return false; } $max_usable_offset = min(PHP_INT_MAX - 1024, $maxoffset); if ($maxoffset > $max_usable_offset) { $info['warning'][] = 'ParseRIFF() may return incomplete data for chunk starting at ' . $startoffset . ' because beyond it extends to ' . $maxoffset . ', which is beyond the ' . round(PHP_INT_MAX / 1073741824) . 'GB limit of PHP filesystem functions'; } fseek($this->getid3->fp, $startoffset, SEEK_SET); while (ftell($this->getid3->fp) < $max_usable_offset) { $chunknamesize = fread($this->getid3->fp, 8); $chunkname = substr($chunknamesize, 0, 4); $chunksize = $this->EitherEndian2Int(substr($chunknamesize, 4, 4)); if (strlen($chunkname) < 4) { $info['error'][] = 'Expecting chunk name at offset ' . (ftell($this->getid3->fp) - 4) . ' but found nothing. Aborting RIFF parsing.'; break; } if ($chunksize == 0) { if ($chunkname == 'JUNK') { // we'll allow zero-size JUNK frames } else { $info['warning'][] = 'Chunk size at offset ' . (ftell($this->getid3->fp) - 4) . ' is zero. Aborting RIFF parsing.'; break; } } if ($chunksize % 2 != 0) { // all structures are packed on word boundaries ++$chunksize; } switch ($chunkname) { case 'LIST': $listname = fread($this->getid3->fp, 4); if (preg_match('#^(movi|rec )$#i', $listname)) { $RIFFchunk[$listname]['offset'] = ftell($this->getid3->fp) - 4; $RIFFchunk[$listname]['size'] = $chunksize; if ($FoundAllChunksWeNeed) { // skip over } else { $WhereWeWere = ftell($this->getid3->fp); $AudioChunkHeader = fread($this->getid3->fp, 12); $AudioChunkStreamNum = substr($AudioChunkHeader, 0, 2); $AudioChunkStreamType = substr($AudioChunkHeader, 2, 2); $AudioChunkSize = Helper::LittleEndian2Int(substr($AudioChunkHeader, 4, 4)); if ($AudioChunkStreamType == 'wb') { $FirstFourBytes = substr($AudioChunkHeader, 8, 4); if (preg_match('/^\\xFF[\\xE2-\\xE7\\xF2-\\xF7\\xFA-\\xFF][\\x00-\\xEB]/s', $FirstFourBytes)) { // MP3 if (Mp3::MPEGaudioHeaderBytesValid($FirstFourBytes)) { $getid3_temp = new GetId3Core(); $getid3_temp->openfile($this->getid3->filename); $getid3_temp->info['avdataoffset'] = ftell($this->getid3->fp) - 4; $getid3_temp->info['avdataend'] = ftell($this->getid3->fp) + $AudioChunkSize; $getid3_mp3 = new Mp3($getid3_temp); $getid3_mp3->getOnlyMPEGaudioInfo($getid3_temp->info['avdataoffset'], false); if (isset($getid3_temp->info['mpeg']['audio'])) { $info['mpeg']['audio'] = $getid3_temp->info['mpeg']['audio']; $info['audio'] = $getid3_temp->info['audio']; $info['audio']['dataformat'] = 'mp' . $info['mpeg']['audio']['layer']; $info['audio']['sample_rate'] = $info['mpeg']['audio']['sample_rate']; $info['audio']['channels'] = $info['mpeg']['audio']['channels']; $info['audio']['bitrate'] = $info['mpeg']['audio']['bitrate']; $info['audio']['bitrate_mode'] = strtolower($info['mpeg']['audio']['bitrate_mode']); //$info['bitrate'] = $info['audio']['bitrate']; } unset($getid3_temp, $getid3_mp3); } } elseif (preg_match('/^\\x0B\\x77/s', $FirstFourBytes)) { // AC3 if (class_exists('Helpers\\GetId3\\Module\\Audio\\Ac3')) { $getid3_temp = new GetId3Core(); $getid3_temp->openfile($this->getid3->filename); $getid3_temp->info['avdataoffset'] = ftell($this->getid3->fp) - 4; $getid3_temp->info['avdataend'] = ftell($this->getid3->fp) + $AudioChunkSize; $getid3_ac3 = new Ac3($getid3_temp); $getid3_ac3->analyze(); if (empty($getid3_temp->info['error'])) { $info['audio'] = $getid3_temp->info['audio']; $info['ac3'] = $getid3_temp->info['ac3']; if (!empty($getid3_temp->info['warning'])) { foreach ($getid3_temp->info['warning'] as $key => $value) { $info['warning'][] = $value; } } } unset($getid3_temp, $getid3_ac3); } } } $FoundAllChunksWeNeed = true; fseek($this->getid3->fp, $WhereWeWere, SEEK_SET); } fseek($this->getid3->fp, $chunksize - 4, SEEK_CUR); //} elseif (preg_match('#^[0-9]{2}(wb|pc|dc|db)$#i', $listname)) { // // // data chunk, ignore // } else { if (!isset($RIFFchunk[$listname])) { $RIFFchunk[$listname] = array(); } $LISTchunkParent = $listname; $LISTchunkMaxOffset = ftell($this->getid3->fp) - 4 + $chunksize; if ($parsedChunk = $this->ParseRIFF(ftell($this->getid3->fp), ftell($this->getid3->fp) + $chunksize - 4)) { $RIFFchunk[$listname] = array_merge_recursive($RIFFchunk[$listname], $parsedChunk); } } break; default: if (preg_match('#^[0-9]{2}(wb|pc|dc|db)$#', $chunkname)) { $nextoffset = ftell($this->getid3->fp) + $chunksize; if ($nextoffset < 0 || !Helper::intValueSupported($nextoffset)) { $info['warning'][] = 'Unable to parse chunk at offset ' . $nextoffset . ' because beyond ' . round(PHP_INT_MAX / 1073741824) . 'GB limit of PHP filesystem functions'; break 2; } fseek($this->getid3->fp, $nextoffset, SEEK_SET); break; } $thisindex = 0; if (isset($RIFFchunk[$chunkname]) && is_array($RIFFchunk[$chunkname])) { $thisindex = count($RIFFchunk[$chunkname]); } $RIFFchunk[$chunkname][$thisindex]['offset'] = ftell($this->getid3->fp) - 8; $RIFFchunk[$chunkname][$thisindex]['size'] = $chunksize; switch ($chunkname) { case 'data': $info['avdataoffset'] = ftell($this->getid3->fp); $info['avdataend'] = $info['avdataoffset'] + $chunksize; $RIFFdataChunkContentsTest = fread($this->getid3->fp, 36); if (strlen($RIFFdataChunkContentsTest) > 0 && preg_match('/^\\xFF[\\xE2-\\xE7\\xF2-\\xF7\\xFA-\\xFF][\\x00-\\xEB]/s', substr($RIFFdataChunkContentsTest, 0, 4))) { // Probably is MP3 data if (Mp3::MPEGaudioHeaderBytesValid(substr($RIFFdataChunkContentsTest, 0, 4))) { $getid3_temp = new GetId3Core(); $getid3_temp->openfile($this->getid3->filename); $getid3_temp->info['avdataoffset'] = $RIFFchunk[$chunkname][$thisindex]['offset']; $getid3_temp->info['avdataend'] = $RIFFchunk[$chunkname][$thisindex]['offset'] + $RIFFchunk[$chunkname][$thisindex]['size']; $getid3_mp3 = new Mp3($getid3_temp); $getid3_mp3->getOnlyMPEGaudioInfo($RIFFchunk[$chunkname][$thisindex]['offset'], false); if (empty($getid3_temp->info['error'])) { $info['mpeg'] = $getid3_temp->info['mpeg']; $info['audio'] = $getid3_temp->info['audio']; } unset($getid3_temp, $getid3_mp3); } } elseif (strlen($RIFFdataChunkContentsTest) > 0 && substr($RIFFdataChunkContentsTest, 0, 2) == "\vw") { // This is probably AC-3 data if (class_exists('Helpers\\GetId3\\Module\\Audio\\Ac3')) { $getid3_temp = new GetId3Core(); $getid3_temp->openfile($this->getid3->filename); $getid3_temp->info['avdataoffset'] = $RIFFchunk[$chunkname][$thisindex]['offset']; $getid3_temp->info['avdataend'] = $RIFFchunk[$chunkname][$thisindex]['offset'] + $RIFFchunk[$chunkname][$thisindex]['size']; $getid3_ac3 = new Ac3($getid3_temp); $getid3_ac3->analyze(); if (empty($getid3_temp->info['error'])) { $info['audio'] = $getid3_temp->info['audio']; $info['ac3'] = $getid3_temp->info['ac3']; $info['warning'] = $getid3_temp->info['warning']; } unset($getid3_temp, $getid3_ac3); } } elseif (strlen($RIFFdataChunkContentsTest) > 0 && substr($RIFFdataChunkContentsTest, 8, 2) == "w\v") { // Dolby Digital WAV // AC-3 content, but not encoded in same format as normal AC-3 file // For one thing, byte order is swapped if (class_exists('Helpers\\GetId3\\Module\\Audio\\Ac3')) { // ok to use tmpfile here - only 56 bytes if ($RIFFtempfilename = tempnam(GetId3Core::getTempDir(), 'id3')) { if ($fd_temp = fopen($RIFFtempfilename, 'wb')) { for ($i = 0; $i < 28; $i += 2) { // swap byte order fwrite($fd_temp, substr($RIFFdataChunkContentsTest, 8 + $i + 1, 1)); fwrite($fd_temp, substr($RIFFdataChunkContentsTest, 8 + $i + 0, 1)); } fclose($fd_temp); $getid3_temp = new GetId3Core(); $getid3_temp->openfile($RIFFtempfilename); $getid3_temp->info['avdataend'] = 20; $getid3_ac3 = new Ac3($getid3_temp); $getid3_ac3->analyze(); if (empty($getid3_temp->info['error'])) { $info['audio'] = $getid3_temp->info['audio']; $info['ac3'] = $getid3_temp->info['ac3']; $info['warning'] = $getid3_temp->info['warning']; } else { $info['error'][] = 'Error parsing Dolby Digital WAV (AC3-in-RIFF): ' . implode(';', $getid3_temp->info['error']); } unset($getid3_ac3, $getid3_temp); } else { $info['error'][] = 'Error parsing Dolby Digital WAV (AC3-in-RIFF): failed to write temp file'; } unlink($RIFFtempfilename); } else { $info['error'][] = 'Error parsing Dolby Digital WAV (AC3-in-RIFF): failed to write temp file'; } } } elseif (strlen($RIFFdataChunkContentsTest) > 0 && substr($RIFFdataChunkContentsTest, 0, 4) == 'wvpk') { // This is WavPack data $info['wavpack']['offset'] = $RIFFchunk[$chunkname][$thisindex]['offset']; $info['wavpack']['size'] = Helper::LittleEndian2Int(substr($RIFFdataChunkContentsTest, 4, 4)); $this->RIFFparseWavPackHeader(substr($RIFFdataChunkContentsTest, 8, 28)); } else { // This is some other kind of data (quite possibly just PCM) // do nothing special, just skip it } $nextoffset = $RIFFchunk[$chunkname][$thisindex]['offset'] + 8 + $chunksize; if ($nextoffset < 0 || !Helper::intValueSupported($nextoffset)) { $info['warning'][] = 'Unable to parse chunk at offset ' . $nextoffset . ' because beyond ' . round(PHP_INT_MAX / 1073741824) . 'GB limit of PHP filesystem functions'; break 3; } fseek($this->getid3->fp, $RIFFchunk[$chunkname][$thisindex]['offset'] + 8 + $chunksize, SEEK_SET); break; case 'iXML': case 'bext': case 'cart': case 'fmt ': case 'strh': case 'strf': case 'indx': case 'MEXT': case 'DISP': // always read data in // always read data in case 'JUNK': // should be: never read data in // but some programs write their version strings in a JUNK chunk (e.g. VirtualDub, AVIdemux, etc) if ($chunksize < 1048576) { if ($chunksize > 0) { $RIFFchunk[$chunkname][$thisindex]['data'] = fread($this->getid3->fp, $chunksize); if ($chunkname == 'JUNK') { if (preg_match('#^([\\x20-\\x7F]+)#', $RIFFchunk[$chunkname][$thisindex]['data'], $matches)) { // only keep text characters [chr(32)-chr(127)] $info['riff']['comments']['junk'][] = trim($matches[1]); } // but if nothing there, ignore // remove the key in either case unset($RIFFchunk[$chunkname][$thisindex]['data']); } } } else { $info['warning'][] = 'chunk "' . $chunkname . '" at offset ' . ftell($this->getid3->fp) . ' is unexpectedly larger than 1MB (claims to be ' . number_format($chunksize) . ' bytes), skipping data'; $nextoffset = ftell($this->getid3->fp) + $chunksize; if ($nextoffset < 0 || !Helper::intValueSupported($nextoffset)) { $info['warning'][] = 'Unable to parse chunk at offset ' . $nextoffset . ' because beyond ' . round(PHP_INT_MAX / 1073741824) . 'GB limit of PHP filesystem functions'; break 3; } fseek($this->getid3->fp, $nextoffset, SEEK_SET); } break; default: if (!preg_match('#^[0-9]{2}(wb|pc|dc|db)$#', $chunkname) && !empty($LISTchunkParent) && $RIFFchunk[$chunkname][$thisindex]['offset'] + $RIFFchunk[$chunkname][$thisindex]['size'] <= $LISTchunkMaxOffset) { $RIFFchunk[$LISTchunkParent][$chunkname][$thisindex]['offset'] = $RIFFchunk[$chunkname][$thisindex]['offset']; $RIFFchunk[$LISTchunkParent][$chunkname][$thisindex]['size'] = $RIFFchunk[$chunkname][$thisindex]['size']; unset($RIFFchunk[$chunkname][$thisindex]['offset']); unset($RIFFchunk[$chunkname][$thisindex]['size']); if (isset($RIFFchunk[$chunkname][$thisindex]) && empty($RIFFchunk[$chunkname][$thisindex])) { unset($RIFFchunk[$chunkname][$thisindex]); } if (isset($RIFFchunk[$chunkname]) && empty($RIFFchunk[$chunkname])) { unset($RIFFchunk[$chunkname]); } $RIFFchunk[$LISTchunkParent][$chunkname][$thisindex]['data'] = fread($this->getid3->fp, $chunksize); //} elseif (in_array($chunkname, array('ID3 ')) || (($chunksize > 0) && ($chunksize < 2048))) { } elseif ($chunksize > 0 && $chunksize < 2048) { // only read data in if smaller than 2kB $RIFFchunk[$chunkname][$thisindex]['data'] = fread($this->getid3->fp, $chunksize); } else { $nextoffset = ftell($this->getid3->fp) + $chunksize; if ($nextoffset < 0 || !Helper::intValueSupported($nextoffset)) { $info['warning'][] = 'Unable to parse chunk at offset ' . $nextoffset . ' because beyond ' . round(PHP_INT_MAX / 1073741824) . 'GB limit of PHP filesystem functions'; break 3; } fseek($this->getid3->fp, $nextoffset, SEEK_SET); } break; } break; } } return $RIFFchunk; }
/** * @param type $filename * * @return bool * * @throws DefaultException */ public function openfile($filename) { try { if ($this->hasStartupError()) { throw new DefaultException($this->getStartupError()); } if ($this->hasStartupWarning()) { $this->warning($this->getStartupWarning()); } // init result array and set parameters $this->setFilename($filename); $this->info = array(); $this->info['GETID3_VERSION'] = $this->version(); $this->info['php_memory_limit'] = $this->getMemoryLimit(); // remote files not supported if (preg_match('/^(ht|f)tp:\\/\\//', $filename)) { throw new DefaultException('Remote files are not supported - please copy the file locally first'); } $filename = str_replace('/', DIRECTORY_SEPARATOR, $filename); $filename = preg_replace('#(.+)' . preg_quote(DIRECTORY_SEPARATOR) . '{2,}#U', '\\1' . DIRECTORY_SEPARATOR, $filename); // open local file if (is_readable($filename) && is_file($filename) && $this->setFp(fopen($filename, 'rb'))) { // great } else { throw new DefaultException('Could not open "' . $filename . '" (does not exist, or is not a file)'); } $this->info['filesize'] = filesize($filename); // set redundant parameters - might be needed in some include file $this->info['filename'] = basename($filename); $this->info['filepath'] = str_replace('\\', '/', realpath(dirname($filename))); $this->info['filenamepath'] = $this->info['filepath'] . '/' . $this->info['filename']; // option_max_2gb_check if ($this->getOptionMax2gbCheck()) { // PHP (32-bit all, and 64-bit Windows) doesn't support integers larger than 2^31 (~2GB) // filesize() simply returns (filesize % (pow(2, 32)), no matter the actual filesize // ftell() returns 0 if seeking to the end is beyond the range of unsigned integer $fseek = fseek($this->getFp(), 0, SEEK_END); if ($fseek < 0 || $this->info['filesize'] != 0 && ftell($this->getFp()) == 0 || $this->info['filesize'] < 0 || ftell($this->getFp()) < 0) { $real_filesize = false; if (self::$EnvironmentIsWindows) { $commandline = 'dir /-C "' . str_replace('/', DIRECTORY_SEPARATOR, $filename) . '"'; $dir_output = `{$commandline}`; if (preg_match('#1 File\\(s\\)[ ]+([0-9]+) bytes#i', $dir_output, $matches)) { $real_filesize = (double) $matches[1]; } } else { $commandline = 'ls -o -g -G --time-style=long-iso ' . escapeshellarg($filename); $dir_output = `{$commandline}`; if (preg_match('#([0-9]+) ([0-9]{4}-[0-9]{2}\\-[0-9]{2} [0-9]{2}:[0-9]{2}) ' . str_replace('#', '\\#', preg_quote($filename)) . '$#', $dir_output, $matches)) { $real_filesize = (double) $matches[1]; } } if (false === $real_filesize) { unset($this->info['filesize']); fclose($this->getFp()); throw new DefaultException('Unable to determine actual filesize. File is most likely larger than ' . round(PHP_INT_MAX / 1073741824) . 'GB and is not supported by PHP.'); } elseif (Helper::intValueSupported($real_filesize)) { unset($this->info['filesize']); fclose($this->getFp()); throw new DefaultException('PHP seems to think the file is larger than ' . round(PHP_INT_MAX / 1073741824) . 'GB, but filesystem reports it as ' . number_format($real_filesize, 3) . 'GB, please report to info@getid3.org'); } $this->info['filesize'] = $real_filesize; $this->error('File is larger than ' . round(PHP_INT_MAX / 1073741824) . 'GB (filesystem reports it as ' . number_format($real_filesize, 3) . 'GB) and is not properly supported by PHP.'); } } // set more parameters $this->info['avdataoffset'] = 0; $this->info['avdataend'] = $this->info['filesize']; $this->info['fileformat'] = ''; // filled in later $this->info['audio']['dataformat'] = ''; // filled in later, unset if not used $this->info['video']['dataformat'] = ''; // filled in later, unset if not used $this->info['tags'] = array(); // filled in later, unset if not used $this->info['error'] = array(); // filled in later, unset if not used $this->info['warning'] = array(); // filled in later, unset if not used $this->info['comments'] = array(); // filled in later, unset if not used $this->info['encoding'] = $this->getEncoding(); // required by id3v2 and iso modules - can be unset at the end if desired return true; } catch (DefaultException $e) { $this->error($e->getMessage()); } return false; }
/** * @return bool */ public function analyze() { $info =& $this->getid3->info; $info['fileformat'] = 'ogg'; // Warn about illegal tags - only vorbiscomments are allowed if (isset($info['id3v2'])) { $info['warning'][] = 'Illegal ID3v2 tag present.'; } if (isset($info['id3v1'])) { $info['warning'][] = 'Illegal ID3v1 tag present.'; } if (isset($info['ape'])) { $info['warning'][] = 'Illegal APE tag present.'; } // Page 1 - Stream Header $this->fseek($info['avdataoffset']); $oggpageinfo = $this->ParseOggPageHeader(); $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo; if ($this->ftell() >= $this->getid3->fread_buffer_size()) { $info['error'][] = 'Could not find start of Ogg page in the first ' . $this->getid3->fread_buffer_size() . ' bytes (this might not be an Ogg-Vorbis file?)'; unset($info['fileformat']); unset($info['ogg']); return false; } $filedata = $this->fread($oggpageinfo['page_length']); $filedataoffset = 0; if (substr($filedata, 0, 4) == 'fLaC') { $info['audio']['dataformat'] = 'flac'; $info['audio']['bitrate_mode'] = 'vbr'; $info['audio']['lossless'] = true; } elseif (substr($filedata, 1, 6) == 'vorbis') { $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo); } elseif (substr($filedata, 0, 8) == 'Speex ') { // http://www.speex.org/manual/node10.html $info['audio']['dataformat'] = 'speex'; $info['mime_type'] = 'audio/speex'; $info['audio']['bitrate_mode'] = 'abr'; $info['audio']['lossless'] = false; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_string'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'Speex ' $filedataoffset += 8; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version'] = substr($filedata, $filedataoffset, 20); $filedataoffset += 20; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version_id'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['header_size'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode_bitstream_version'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['bitrate'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['framesize'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['frames_per_packet'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['extra_headers'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved1'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved2'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['speex']['speex_version'] = trim($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']); $info['speex']['sample_rate'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate']; $info['speex']['channels'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels']; $info['speex']['vbr'] = (bool) $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr']; $info['speex']['band_type'] = $this->SpeexBandModeLookup($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']); $info['audio']['sample_rate'] = $info['speex']['sample_rate']; $info['audio']['channels'] = $info['speex']['channels']; if ($info['speex']['vbr']) { $info['audio']['bitrate_mode'] = 'vbr'; } } elseif (substr($filedata, 0, 8) == "fishead") { // Ogg Skeleton version 3.0 Format Specification // http://xiph.org/ogg/doc/skeleton.html $filedataoffset += 8; $info['ogg']['skeleton']['fishead']['raw']['version_major'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 2)); $filedataoffset += 2; $info['ogg']['skeleton']['fishead']['raw']['version_minor'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 2)); $filedataoffset += 2; $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); $filedataoffset += 8; $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); $filedataoffset += 8; $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); $filedataoffset += 8; $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); $filedataoffset += 8; $info['ogg']['skeleton']['fishead']['raw']['utc'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 20)); $filedataoffset += 20; $info['ogg']['skeleton']['fishead']['version'] = $info['ogg']['skeleton']['fishead']['raw']['version_major'] . '.' . $info['ogg']['skeleton']['fishead']['raw']['version_minor']; $info['ogg']['skeleton']['fishead']['presentationtime'] = $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator']; $info['ogg']['skeleton']['fishead']['basetime'] = $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator']; $info['ogg']['skeleton']['fishead']['utc'] = $info['ogg']['skeleton']['fishead']['raw']['utc']; $counter = 0; do { $oggpageinfo = $this->ParseOggPageHeader(); $info['ogg']['pageheader'][$oggpageinfo['page_seqno'] . '.' . $counter++] = $oggpageinfo; $filedata = $this->fread($oggpageinfo['page_length']); $this->fseek($oggpageinfo['page_end_offset']); if (substr($filedata, 0, 8) == "fisbone") { $filedataoffset = 8; $info['ogg']['skeleton']['fisbone']['raw']['message_header_offset'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['skeleton']['fisbone']['raw']['serial_number'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['skeleton']['fisbone']['raw']['number_header_packets'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['skeleton']['fisbone']['raw']['granulerate_numerator'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); $filedataoffset += 8; $info['ogg']['skeleton']['fisbone']['raw']['granulerate_denominator'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); $filedataoffset += 8; $info['ogg']['skeleton']['fisbone']['raw']['basegranule'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); $filedataoffset += 8; $info['ogg']['skeleton']['fisbone']['raw']['preroll'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; $info['ogg']['skeleton']['fisbone']['raw']['granuleshift'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); $filedataoffset += 1; $info['ogg']['skeleton']['fisbone']['raw']['padding'] = substr($filedata, $filedataoffset, 3); $filedataoffset += 3; } elseif (substr($filedata, 1, 6) == 'theora') { $info['video']['dataformat'] = 'theora'; $info['error'][] = 'Ogg Theora not correctly handled in this version of GetId3 [' . $this->getid3->version() . ']'; //break; } elseif (substr($filedata, 1, 6) == 'vorbis') { $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo); } else { $info['error'][] = 'unexpected'; //break; } //} while ($oggpageinfo['page_seqno'] == 0); } while ($oggpageinfo['page_seqno'] == 0 && substr($filedata, 0, 8) != "fisbone"); $this->fseek($oggpageinfo['page_start_offset']); $info['error'][] = 'Ogg Skeleton not correctly handled in this version of GetId3 [' . $this->getid3->version() . ']'; //return false; } else { $info['error'][] = 'Expecting either "Speex " or "vorbis" identifier strings, found "' . substr($filedata, 0, 8) . '"'; unset($info['ogg']); unset($info['mime_type']); return false; } // Page 2 - Comment Header $oggpageinfo = $this->ParseOggPageHeader(); $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo; switch ($info['audio']['dataformat']) { case 'vorbis': $filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']); $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = Helper::LittleEndian2Int(substr($filedata, 0, 1)); $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, 1, 6); // hard-coded to 'vorbis' $this->ParseVorbisComments(); break; case 'flac': $getid3_flac = new Flac($this->getid3); if (!$getid3_flac->parseMETAdata()) { $info['error'][] = 'Failed to parse FLAC headers'; return false; } unset($getid3_flac); break; case 'speex': $this->fseek($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length'], SEEK_CUR); $this->ParseVorbisComments(); break; } // Last Page - Number of Samples if (!Helper::intValueSupported($info['avdataend'])) { $info['warning'][] = 'Unable to parse Ogg end chunk file (PHP does not support file operations beyond ' . round(PHP_INT_MAX / 1073741824) . 'GB)'; } else { $this->fseek(max($info['avdataend'] - $this->getid3->fread_buffer_size(), 0)); $LastChunkOfOgg = strrev($this->fread($this->getid3->fread_buffer_size())); if ($LastOggSpostion = strpos($LastChunkOfOgg, 'SggO')) { $this->fseek($info['avdataend'] - ($LastOggSpostion + strlen('SggO'))); $info['avdataend'] = $this->ftell(); $info['ogg']['pageheader']['eos'] = $this->ParseOggPageHeader(); $info['ogg']['samples'] = $info['ogg']['pageheader']['eos']['pcm_abs_position']; if ($info['ogg']['samples'] == 0) { $info['error'][] = 'Corrupt Ogg file: eos.number of samples == zero'; return false; } if (!empty($info['audio']['sample_rate'])) { $info['ogg']['bitrate_average'] = ($info['avdataend'] - $info['avdataoffset']) * 8 / ($info['ogg']['samples'] / $info['audio']['sample_rate']); } } } if (!empty($info['ogg']['bitrate_average'])) { $info['audio']['bitrate'] = $info['ogg']['bitrate_average']; } elseif (!empty($info['ogg']['bitrate_nominal'])) { $info['audio']['bitrate'] = $info['ogg']['bitrate_nominal']; } elseif (!empty($info['ogg']['bitrate_min']) && !empty($info['ogg']['bitrate_max'])) { $info['audio']['bitrate'] = ($info['ogg']['bitrate_min'] + $info['ogg']['bitrate_max']) / 2; } if (isset($info['audio']['bitrate']) && !isset($info['playtime_seconds'])) { if ($info['audio']['bitrate'] == 0) { $info['error'][] = 'Corrupt Ogg file: bitrate_audio == zero'; return false; } $info['playtime_seconds'] = (double) (($info['avdataend'] - $info['avdataoffset']) * 8 / $info['audio']['bitrate']); } if (isset($info['ogg']['vendor'])) { $info['audio']['encoder'] = preg_replace('/^Encoded with /', '', $info['ogg']['vendor']); // Vorbis only if ($info['audio']['dataformat'] == 'vorbis') { // Vorbis 1.0 starts with Xiph.Org if (preg_match('/^Xiph.Org/', $info['audio']['encoder'])) { if ($info['audio']['bitrate_mode'] == 'abr') { // Set -b 128 on abr files $info['audio']['encoder_options'] = '-b ' . round($info['ogg']['bitrate_nominal'] / 1000); } elseif ($info['audio']['bitrate_mode'] == 'vbr' && $info['audio']['channels'] == 2 && $info['audio']['sample_rate'] >= 44100 && $info['audio']['sample_rate'] <= 48000) { // Set -q N on vbr files $info['audio']['encoder_options'] = '-q ' . $this->get_quality_from_nominal_bitrate($info['ogg']['bitrate_nominal']); } } if (empty($info['audio']['encoder_options']) && !empty($info['ogg']['bitrate_nominal'])) { $info['audio']['encoder_options'] = 'Nominal bitrate: ' . intval(round($info['ogg']['bitrate_nominal'] / 1000)) . 'kbps'; } } } return true; }
/** * @return bool */ public function analyze() { $info =& $this->getid3->info; if (!Helper::intValueSupported($info['filesize'])) { $info['warning'][] = 'Unable to check for ID3v1 because file is larger than ' . round(PHP_INT_MAX / 1073741824) . 'GB'; return false; } fseek($this->getid3->fp, -256, SEEK_END); $preid3v1 = fread($this->getid3->fp, 128); $id3v1tag = fread($this->getid3->fp, 128); if (substr($id3v1tag, 0, 3) == 'TAG') { $info['avdataend'] = $info['filesize'] - 128; $ParsedID3v1['title'] = $this->cutfield(substr($id3v1tag, 3, 30)); $ParsedID3v1['artist'] = $this->cutfield(substr($id3v1tag, 33, 30)); $ParsedID3v1['album'] = $this->cutfield(substr($id3v1tag, 63, 30)); $ParsedID3v1['year'] = $this->cutfield(substr($id3v1tag, 93, 4)); $ParsedID3v1['comment'] = substr($id3v1tag, 97, 30); // can't remove nulls yet, track detection depends on them $ParsedID3v1['genreid'] = ord(substr($id3v1tag, 127, 1)); // If second-last byte of comment field is null and last byte of comment field is non-null // then this is ID3v1.1 and the comment field is 28 bytes long and the 30th byte is the track number if ($id3v1tag[125] === "" && $id3v1tag[126] !== "") { $ParsedID3v1['track'] = ord(substr($ParsedID3v1['comment'], 29, 1)); $ParsedID3v1['comment'] = substr($ParsedID3v1['comment'], 0, 28); } $ParsedID3v1['comment'] = $this->cutfield($ParsedID3v1['comment']); $ParsedID3v1['genre'] = $this->LookupGenreName($ParsedID3v1['genreid']); if (!empty($ParsedID3v1['genre'])) { unset($ParsedID3v1['genreid']); } if (isset($ParsedID3v1['genre']) && (empty($ParsedID3v1['genre']) || $ParsedID3v1['genre'] == 'Unknown')) { unset($ParsedID3v1['genre']); } foreach ($ParsedID3v1 as $key => $value) { $ParsedID3v1['comments'][$key][0] = $value; } // ID3v1 data is supposed to be padded with NULL characters, but some taggers pad with spaces $GoodFormatID3v1tag = $this->GenerateID3v1Tag($ParsedID3v1['title'], $ParsedID3v1['artist'], $ParsedID3v1['album'], $ParsedID3v1['year'], isset($ParsedID3v1['genre']) ? $this->LookupGenreID($ParsedID3v1['genre']) : false, $ParsedID3v1['comment'], !empty($ParsedID3v1['track']) ? $ParsedID3v1['track'] : ''); $ParsedID3v1['padding_valid'] = true; if ($id3v1tag !== $GoodFormatID3v1tag) { $ParsedID3v1['padding_valid'] = false; $info['warning'][] = 'Some ID3v1 fields do not use NULL characters for padding'; } $ParsedID3v1['tag_offset_end'] = $info['filesize']; $ParsedID3v1['tag_offset_start'] = $ParsedID3v1['tag_offset_end'] - 128; $info['id3v1'] = $ParsedID3v1; } if (substr($preid3v1, 0, 3) == 'TAG') { // The way iTunes handles tags is, well, brain-damaged. // It completely ignores v1 if ID3v2 is present. // This goes as far as adding a new v1 tag *even if there already is one* // A suspected double-ID3v1 tag has been detected, but it could be that // the "TAG" identifier is a legitimate part of an APE or Lyrics3 tag if (substr($preid3v1, 96, 8) == 'APETAGEX') { // an APE tag footer was found before the last ID3v1, assume false "TAG" synch } elseif (substr($preid3v1, 119, 6) == 'LYRICS') { // a Lyrics3 tag footer was found before the last ID3v1, assume false "TAG" synch } else { // APE and Lyrics3 footers not found - assume double ID3v1 $info['warning'][] = 'Duplicate ID3v1 tag detected - this has been known to happen with iTunes'; $info['avdataend'] -= 128; } } return true; }
/** * @param type $ThisFileInfoIndex * @param type $filename * @param type $offset * @param type $length * * @return bool * * @throws Exception */ public function saveAttachment(&$ThisFileInfoIndex, $filename, $offset, $length) { try { if (!Helper::intValueSupported($offset + $length)) { throw new DefaultException('it extends beyond the ' . round(PHP_INT_MAX / 1073741824) . 'GB limit'); } if ($this->getGetId3()->getOptionSaveAttachments() === GetId3Core::ATTACHMENTS_NONE) { // do not extract at all unset($ThisFileInfoIndex); // do not set any } elseif ($this->getGetId3()->getOptionSaveAttachments() === GetId3Core::ATTACHMENTS_INLINE) { // extract to return array // get whole data in one pass, till it is anyway stored in memory $this->fseek($offset); $ThisFileInfoIndex = $this->fread($length); if ($ThisFileInfoIndex === false || strlen($ThisFileInfoIndex) != $length) { // verify throw new DefaultException('failed to read attachment data'); } } else { // assume directory path is given // set up destination path $dir = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->getGetId3()->getOptionSaveAttachments()), DIRECTORY_SEPARATOR); if (!is_dir($dir) || !is_writable($dir)) { // check supplied directory throw new DefaultException('supplied path (' . $dir . ') does not exist, or is not writable'); } $dest = $dir . DIRECTORY_SEPARATOR . $filename; // create dest file if (false == ($fp_dest = fopen($dest, 'wb'))) { throw new DefaultException('failed to create file ' . $dest); } // copy data $this->fseek($offset); $buffersize = $this->getDataStringFlag() ? $length : $this->getGetId3()->fread_buffer_size(); $bytesleft = $length; while ($bytesleft > 0) { if (false === ($buffer = $this->fread(min($buffersize, $bytesleft))) || false === ($byteswritten = fwrite($fp_dest, $buffer))) { fclose($fp_dest); unlink($dest); throw new DefaultException(false === $buffer ? 'not enough data to read' : 'failed to write to destination file, may be not enough disk space'); } $bytesleft -= $byteswritten; } fclose($fp_dest); $ThisFileInfoIndex = $dest; } } catch (DefaultException $e) { unset($ThisFileInfoIndex); // do not set any in case of error $this->warning('Failed to extract attachment ' . $filename . ': ' . $e->getMessage()); return false; } return true; }
/** * @return bool */ public function analyze() { $info =& $this->getid3->info; if (!Helper::intValueSupported($info['filesize'])) { $info['warning'][] = 'Unable to check for APEtags because file is larger than ' . round(PHP_INT_MAX / 1073741824) . 'GB'; return false; } $id3v1tagsize = 128; $apetagheadersize = 32; $lyrics3tagsize = 10; if ($this->overrideendoffset == 0) { fseek($this->getid3->fp, 0 - $id3v1tagsize - $apetagheadersize - $lyrics3tagsize, SEEK_END); $APEfooterID3v1 = fread($this->getid3->fp, $id3v1tagsize + $apetagheadersize + $lyrics3tagsize); //if (preg_match('/APETAGEX.{24}TAG.{125}$/i', $APEfooterID3v1)) { if (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $id3v1tagsize - $apetagheadersize, 8) == 'APETAGEX') { // APE tag found before ID3v1 $info['ape']['tag_offset_end'] = $info['filesize'] - $id3v1tagsize; //} elseif (preg_match('/APETAGEX.{24}$/i', $APEfooterID3v1)) { } elseif (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $apetagheadersize, 8) == 'APETAGEX') { // APE tag found, no ID3v1 $info['ape']['tag_offset_end'] = $info['filesize']; } } else { fseek($this->getid3->fp, $this->overrideendoffset - $apetagheadersize, SEEK_SET); if (fread($this->getid3->fp, 8) == 'APETAGEX') { $info['ape']['tag_offset_end'] = $this->overrideendoffset; } } if (!isset($info['ape']['tag_offset_end'])) { // APE tag not found unset($info['ape']); return false; } // shortcut $thisfile_ape =& $info['ape']; fseek($this->getid3->fp, $thisfile_ape['tag_offset_end'] - $apetagheadersize, SEEK_SET); $APEfooterData = fread($this->getid3->fp, 32); if (!($thisfile_ape['footer'] = $this->parseAPEheaderFooter($APEfooterData))) { $info['error'][] = 'Error parsing APE footer at offset ' . $thisfile_ape['tag_offset_end']; return false; } if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) { fseek($this->getid3->fp, $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize'] - $apetagheadersize, SEEK_SET); $thisfile_ape['tag_offset_start'] = ftell($this->getid3->fp); $APEtagData = fread($this->getid3->fp, $thisfile_ape['footer']['raw']['tagsize'] + $apetagheadersize); } else { $thisfile_ape['tag_offset_start'] = $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize']; fseek($this->getid3->fp, $thisfile_ape['tag_offset_start'], SEEK_SET); $APEtagData = fread($this->getid3->fp, $thisfile_ape['footer']['raw']['tagsize']); } $info['avdataend'] = $thisfile_ape['tag_offset_start']; if (isset($info['id3v1']['tag_offset_start']) && $info['id3v1']['tag_offset_start'] < $thisfile_ape['tag_offset_end']) { $info['warning'][] = 'ID3v1 tag information ignored since it appears to be a false synch in APEtag data'; unset($info['id3v1']); foreach ($info['warning'] as $key => $value) { if ($value == 'Some ID3v1 fields do not use NULL characters for padding') { unset($info['warning'][$key]); sort($info['warning']); break; } } } $offset = 0; if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) { if ($thisfile_ape['header'] = $this->parseAPEheaderFooter(substr($APEtagData, 0, $apetagheadersize))) { $offset += $apetagheadersize; } else { $info['error'][] = 'Error parsing APE header at offset ' . $thisfile_ape['tag_offset_start']; return false; } } // shortcut $info['replay_gain'] = array(); $thisfile_replaygain =& $info['replay_gain']; for ($i = 0; $i < $thisfile_ape['footer']['raw']['tag_items']; ++$i) { $value_size = Helper::LittleEndian2Int(substr($APEtagData, $offset, 4)); $offset += 4; $item_flags = Helper::LittleEndian2Int(substr($APEtagData, $offset, 4)); $offset += 4; if (strstr(substr($APEtagData, $offset), "") === false) { $info['error'][] = 'Cannot find null-byte (0x00) seperator between ItemKey #' . $i . ' and value. ItemKey starts ' . $offset . ' bytes into the APE tag, at file offset ' . ($thisfile_ape['tag_offset_start'] + $offset); return false; } $ItemKeyLength = strpos($APEtagData, "", $offset) - $offset; $item_key = strtolower(substr($APEtagData, $offset, $ItemKeyLength)); // shortcut $thisfile_ape['items'][$item_key] = array(); $thisfile_ape_items_current =& $thisfile_ape['items'][$item_key]; $thisfile_ape_items_current['offset'] = $thisfile_ape['tag_offset_start'] + $offset; $offset += $ItemKeyLength + 1; // skip 0x00 terminator $thisfile_ape_items_current['data'] = substr($APEtagData, $offset, $value_size); $offset += $value_size; $thisfile_ape_items_current['flags'] = $this->parseAPEtagFlags($item_flags); switch ($thisfile_ape_items_current['flags']['item_contents_raw']) { case 0: // UTF-8 // UTF-8 case 3: // Locator (URL, filename, etc), UTF-8 encoded $thisfile_ape_items_current['data'] = explode("", trim($thisfile_ape_items_current['data'])); break; default: // binary data break; } switch (strtolower($item_key)) { case 'replaygain_track_gain': $thisfile_replaygain['track']['adjustment'] = (double) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! $thisfile_replaygain['track']['originator'] = 'unspecified'; break; case 'replaygain_track_peak': $thisfile_replaygain['track']['peak'] = (double) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! $thisfile_replaygain['track']['originator'] = 'unspecified'; if ($thisfile_replaygain['track']['peak'] <= 0) { $info['warning'][] = 'ReplayGain Track peak from APEtag appears invalid: ' . $thisfile_replaygain['track']['peak'] . ' (original value = "' . $thisfile_ape_items_current['data'][0] . '")'; } break; case 'replaygain_album_gain': $thisfile_replaygain['album']['adjustment'] = (double) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! $thisfile_replaygain['album']['originator'] = 'unspecified'; break; case 'replaygain_album_peak': $thisfile_replaygain['album']['peak'] = (double) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! $thisfile_replaygain['album']['originator'] = 'unspecified'; if ($thisfile_replaygain['album']['peak'] <= 0) { $info['warning'][] = 'ReplayGain Album peak from APEtag appears invalid: ' . $thisfile_replaygain['album']['peak'] . ' (original value = "' . $thisfile_ape_items_current['data'][0] . '")'; } break; case 'mp3gain_undo': list($mp3gain_undo_left, $mp3gain_undo_right, $mp3gain_undo_wrap) = explode(',', $thisfile_ape_items_current['data'][0]); $thisfile_replaygain['mp3gain']['undo_left'] = intval($mp3gain_undo_left); $thisfile_replaygain['mp3gain']['undo_right'] = intval($mp3gain_undo_right); $thisfile_replaygain['mp3gain']['undo_wrap'] = $mp3gain_undo_wrap == 'Y' ? true : false; break; case 'mp3gain_minmax': list($mp3gain_globalgain_min, $mp3gain_globalgain_max) = explode(',', $thisfile_ape_items_current['data'][0]); $thisfile_replaygain['mp3gain']['globalgain_track_min'] = intval($mp3gain_globalgain_min); $thisfile_replaygain['mp3gain']['globalgain_track_max'] = intval($mp3gain_globalgain_max); break; case 'mp3gain_album_minmax': list($mp3gain_globalgain_album_min, $mp3gain_globalgain_album_max) = explode(',', $thisfile_ape_items_current['data'][0]); $thisfile_replaygain['mp3gain']['globalgain_album_min'] = intval($mp3gain_globalgain_album_min); $thisfile_replaygain['mp3gain']['globalgain_album_max'] = intval($mp3gain_globalgain_album_max); break; case 'tracknumber': if (is_array($thisfile_ape_items_current['data'])) { foreach ($thisfile_ape_items_current['data'] as $comment) { $thisfile_ape['comments']['track'][] = $comment; } } break; case 'cover art (artist)': case 'cover art (back)': case 'cover art (band logo)': case 'cover art (band)': case 'cover art (colored fish)': case 'cover art (composer)': case 'cover art (conductor)': case 'cover art (front)': case 'cover art (icon)': case 'cover art (illustration)': case 'cover art (lead)': case 'cover art (leaflet)': case 'cover art (lyricist)': case 'cover art (media)': case 'cover art (movie scene)': case 'cover art (other icon)': case 'cover art (other)': case 'cover art (performance)': case 'cover art (publisher logo)': case 'cover art (recording)': case 'cover art (studio)': // list of possible cover arts from http://taglib-sharp.sourcearchive.com/documentation/2.0.3.0-2/Ape_2Tag_8cs-source.html list($thisfile_ape_items_current['filename'], $thisfile_ape_items_current['data']) = explode("", $thisfile_ape_items_current['data'], 2); $thisfile_ape_items_current['data_offset'] = $thisfile_ape_items_current['offset'] + strlen($thisfile_ape_items_current['filename'] . ""); $thisfile_ape_items_current['data_length'] = strlen($thisfile_ape_items_current['data']); $thisfile_ape_items_current['image_mime'] = ''; $imageinfo = array(); $imagechunkcheck = Helper::GetDataImageSize($thisfile_ape_items_current['data'], $imageinfo); $thisfile_ape_items_current['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]); do { if ($this->inline_attachments === false) { // skip entirely unset($thisfile_ape_items_current['data']); break; } if ($this->inline_attachments === true) { // great } elseif (is_int($this->inline_attachments)) { if ($this->inline_attachments < $thisfile_ape_items_current['data_length']) { // too big, skip $info['warning'][] = 'attachment at ' . $thisfile_ape_items_current['offset'] . ' is too large to process inline (' . number_format($thisfile_ape_items_current['data_length']) . ' bytes)'; unset($thisfile_ape_items_current['data']); break; } } elseif (is_string($this->inline_attachments)) { $this->inline_attachments = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->inline_attachments), DIRECTORY_SEPARATOR); if (!is_dir($this->inline_attachments) || !is_writable($this->inline_attachments)) { // cannot write, skip $info['warning'][] = 'attachment at ' . $thisfile_ape_items_current['offset'] . ' cannot be saved to "' . $this->inline_attachments . '" (not writable)'; unset($thisfile_ape_items_current['data']); break; } } // if we get this far, must be OK if (is_string($this->inline_attachments)) { $destination_filename = $this->inline_attachments . DIRECTORY_SEPARATOR . md5($info['filenamepath']) . '_' . $thisfile_ape_items_current['data_offset']; if (!file_exists($destination_filename) || is_writable($destination_filename)) { file_put_contents($destination_filename, $thisfile_ape_items_current['data']); } else { $info['warning'][] = 'attachment at ' . $thisfile_ape_items_current['offset'] . ' cannot be saved to "' . $destination_filename . '" (not writable)'; } $thisfile_ape_items_current['data_filename'] = $destination_filename; unset($thisfile_ape_items_current['data']); } else { if (!isset($info['ape']['comments']['picture'])) { $info['ape']['comments']['picture'] = array(); } $info['ape']['comments']['picture'][] = array('data' => $thisfile_ape_items_current['data'], 'image_mime' => $thisfile_ape_items_current['image_mime']); } } while (false); break; default: if (is_array($thisfile_ape_items_current['data'])) { foreach ($thisfile_ape_items_current['data'] as $comment) { $thisfile_ape['comments'][strtolower($item_key)][] = $comment; } } break; } } if (empty($thisfile_replaygain)) { unset($info['replay_gain']); } return true; }
/** * @param type $endoffset * @param type $version * @param type $length * * @return bool * * @link http://www.volweb.cz/str/tags.htm */ public function getLyrics3Data($endoffset, $version, $length) { $info =& $this->getid3->info; if (!Helper::intValueSupported($endoffset)) { $info['warning'][] = 'Unable to check for Lyrics3 because file is larger than ' . round(PHP_INT_MAX / 1073741824) . 'GB'; return false; } fseek($this->getid3->fp, $endoffset, SEEK_SET); if ($length <= 0) { return false; } $rawdata = fread($this->getid3->fp, $length); $ParsedLyrics3['raw']['lyrics3version'] = $version; $ParsedLyrics3['raw']['lyrics3tagsize'] = $length; $ParsedLyrics3['tag_offset_start'] = $endoffset; $ParsedLyrics3['tag_offset_end'] = $endoffset + $length - 1; if (substr($rawdata, 0, 11) != 'LYRICSBEGIN') { if (strpos($rawdata, 'LYRICSBEGIN') !== false) { $info['warning'][] = '"LYRICSBEGIN" expected at ' . $endoffset . ' but actually found at ' . ($endoffset + strpos($rawdata, 'LYRICSBEGIN')) . ' - this is invalid for Lyrics3 v' . $version; $info['avdataend'] = $endoffset + strpos($rawdata, 'LYRICSBEGIN'); $rawdata = substr($rawdata, strpos($rawdata, 'LYRICSBEGIN')); $length = strlen($rawdata); $ParsedLyrics3['tag_offset_start'] = $info['avdataend']; $ParsedLyrics3['raw']['lyrics3tagsize'] = $length; } else { $info['error'][] = '"LYRICSBEGIN" expected at ' . $endoffset . ' but found "' . substr($rawdata, 0, 11) . '" instead'; return false; } } switch ($version) { case 1: if (substr($rawdata, strlen($rawdata) - 9, 9) == 'LYRICSEND') { $ParsedLyrics3['raw']['LYR'] = trim(substr($rawdata, 11, strlen($rawdata) - 11 - 9)); $this->Lyrics3LyricsTimestampParse($ParsedLyrics3); } else { $info['error'][] = '"LYRICSEND" expected at ' . (ftell($this->getid3->fp) - 11 + $length - 9) . ' but found "' . substr($rawdata, strlen($rawdata) - 9, 9) . '" instead'; return false; } break; case 2: if (substr($rawdata, strlen($rawdata) - 9, 9) == 'LYRICS200') { $ParsedLyrics3['raw']['unparsed'] = substr($rawdata, 11, strlen($rawdata) - 11 - 9 - 6); // LYRICSBEGIN + LYRICS200 + LSZ $rawdata = $ParsedLyrics3['raw']['unparsed']; while (strlen($rawdata) > 0) { $fieldname = substr($rawdata, 0, 3); $fieldsize = (int) substr($rawdata, 3, 5); $ParsedLyrics3['raw'][$fieldname] = substr($rawdata, 8, $fieldsize); $rawdata = substr($rawdata, 3 + 5 + $fieldsize); } if (isset($ParsedLyrics3['raw']['IND'])) { $i = 0; $flagnames = array('lyrics', 'timestamps', 'inhibitrandom'); foreach ($flagnames as $flagname) { if (strlen($ParsedLyrics3['raw']['IND']) > $i++) { $ParsedLyrics3['flags'][$flagname] = $this->IntString2Bool(substr($ParsedLyrics3['raw']['IND'], $i, 1 - 1)); } } } $fieldnametranslation = array('ETT' => 'title', 'EAR' => 'artist', 'EAL' => 'album', 'INF' => 'comment', 'AUT' => 'author'); foreach ($fieldnametranslation as $key => $value) { if (isset($ParsedLyrics3['raw'][$key])) { $ParsedLyrics3['comments'][$value][] = trim($ParsedLyrics3['raw'][$key]); } } if (isset($ParsedLyrics3['raw']['IMG'])) { $imagestrings = explode("\r\n", $ParsedLyrics3['raw']['IMG']); foreach ($imagestrings as $key => $imagestring) { if (strpos($imagestring, '||') !== false) { $imagearray = explode('||', $imagestring); $ParsedLyrics3['images'][$key]['filename'] = isset($imagearray[0]) ? $imagearray[0] : ''; $ParsedLyrics3['images'][$key]['description'] = isset($imagearray[1]) ? $imagearray[1] : ''; $ParsedLyrics3['images'][$key]['timestamp'] = $this->Lyrics3Timestamp2Seconds(isset($imagearray[2]) ? $imagearray[2] : ''); } } } if (isset($ParsedLyrics3['raw']['LYR'])) { $this->Lyrics3LyricsTimestampParse($ParsedLyrics3); } } else { $info['error'][] = '"LYRICS200" expected at ' . (ftell($this->getid3->fp) - 11 + $length - 9) . ' but found "' . substr($rawdata, strlen($rawdata) - 9, 9) . '" instead'; return false; } break; default: $info['error'][] = 'Cannot process Lyrics3 version ' . $version . ' (only v1 and v2)'; return false; break; } if (isset($info['id3v1']['tag_offset_start']) && $info['id3v1']['tag_offset_start'] <= $ParsedLyrics3['tag_offset_end']) { $info['warning'][] = 'ID3v1 tag information ignored since it appears to be a false synch in Lyrics3 tag data'; unset($info['id3v1']); foreach ($info['warning'] as $key => $value) { if ($value == 'Some ID3v1 fields do not use NULL characters for padding') { unset($info['warning'][$key]); sort($info['warning']); break; } } } $info['lyrics3'] = $ParsedLyrics3; return true; }
/** * @return bool */ public function analyze() { $info =& $this->getid3->info; $info['fileformat'] = 'zip'; $info['zip']['encoding'] = 'ISO-8859-1'; $info['zip']['files'] = array(); $info['zip']['compressed_size'] = 0; $info['zip']['uncompressed_size'] = 0; $info['zip']['entries_count'] = 0; if (!Helper::intValueSupported($info['filesize'])) { $info['error'][] = 'File is larger than ' . round(PHP_INT_MAX / 1073741824) . 'GB, not supported by PHP'; return false; } else { $EOCDsearchData = ''; $EOCDsearchCounter = 0; while ($EOCDsearchCounter++ < 512) { fseek($this->getid3->fp, -128 * $EOCDsearchCounter, SEEK_END); $EOCDsearchData = fread($this->getid3->fp, 128) . $EOCDsearchData; if (strstr($EOCDsearchData, 'PK' . "")) { $EOCDposition = strpos($EOCDsearchData, 'PK' . ""); fseek($this->getid3->fp, -128 * $EOCDsearchCounter + $EOCDposition, SEEK_END); $info['zip']['end_central_directory'] = $this->ZIPparseEndOfCentralDirectory(); fseek($this->getid3->fp, $info['zip']['end_central_directory']['directory_offset'], SEEK_SET); $info['zip']['entries_count'] = 0; while ($centraldirectoryentry = $this->ZIPparseCentralDirectory($this->getid3->fp)) { $info['zip']['central_directory'][] = $centraldirectoryentry; ++$info['zip']['entries_count']; $info['zip']['compressed_size'] += $centraldirectoryentry['compressed_size']; $info['zip']['uncompressed_size'] += $centraldirectoryentry['uncompressed_size']; if ($centraldirectoryentry['uncompressed_size'] > 0) { $info['zip']['files'] = Helper::array_merge_clobber($info['zip']['files'], Helper::CreateDeepArray($centraldirectoryentry['filename'], '/', $centraldirectoryentry['uncompressed_size'])); } } if ($info['zip']['entries_count'] == 0) { $info['error'][] = 'No Central Directory entries found (truncated file?)'; return false; } if (!empty($info['zip']['end_central_directory']['comment'])) { $info['zip']['comments']['comment'][] = $info['zip']['end_central_directory']['comment']; } if (isset($info['zip']['central_directory'][0]['compression_method'])) { $info['zip']['compression_method'] = $info['zip']['central_directory'][0]['compression_method']; } if (isset($info['zip']['central_directory'][0]['flags']['compression_speed'])) { $info['zip']['compression_speed'] = $info['zip']['central_directory'][0]['flags']['compression_speed']; } if (isset($info['zip']['compression_method']) && $info['zip']['compression_method'] == 'store' && !isset($info['zip']['compression_speed'])) { $info['zip']['compression_speed'] = 'store'; } return true; } } } if ($this->getZIPentriesFilepointer()) { // central directory couldn't be found and/or parsed // scan through actual file data entries, recover as much as possible from probable trucated file if ($info['zip']['compressed_size'] > $info['filesize'] - 46 - 22) { $info['error'][] = 'Warning: Truncated file! - Total compressed file sizes (' . $info['zip']['compressed_size'] . ' bytes) is greater than filesize minus Central Directory and End Of Central Directory structures (' . ($info['filesize'] - 46 - 22) . ' bytes)'; } $info['error'][] = 'Cannot find End Of Central Directory - returned list of files in [zip][entries] array may not be complete'; foreach ($info['zip']['entries'] as $key => $valuearray) { $info['zip']['files'][$valuearray['filename']] = $valuearray['uncompressed_size']; } return true; } else { unset($info['zip']); $info['fileformat'] = ''; $info['error'][] = 'Cannot find End Of Central Directory (truncated file?)'; return false; } }
/** * @staticvar array $decbin * * @param type $MaxFramesToScan * @param type $ReturnExtendedInfo * * @return bool */ public function getAACADTSheaderFilepointer($MaxFramesToScan = 1000000, $ReturnExtendedInfo = false) { $info =& $this->getid3->info; // based loosely on code from AACfile by Jurgen Faul <jfaulØgmx.de> // http://jfaul.de/atl or http://j-faul.virtualave.net/atl/atl.html // http://faac.sourceforge.net/wiki/index.php?page=ADTS // dead link // http://wiki.multimedia.cx/index.php?title=ADTS // * ADTS Fixed Header: these don't change from frame to frame // syncword 12 always: '111111111111' // ID 1 0: MPEG-4, 1: MPEG-2 // MPEG layer 2 If you send AAC in MPEG-TS, set to 0 // protection_absent 1 0: CRC present; 1: no CRC // profile 2 0: AAC Main; 1: AAC LC (Low Complexity); 2: AAC SSR (Scalable Sample Rate); 3: AAC LTP (Long Term Prediction) // sampling_frequency_index 4 15 not allowed // private_bit 1 usually 0 // channel_configuration 3 // original/copy 1 0: original; 1: copy // home 1 usually 0 // emphasis 2 only if ID == 0 (ie MPEG-4) // not present in some documentation? // * ADTS Variable Header: these can change from frame to frame // copyright_identification_bit 1 // copyright_identification_start 1 // aac_frame_length 13 length of the frame including header (in bytes) // adts_buffer_fullness 11 0x7FF indicates VBR // no_raw_data_blocks_in_frame 2 // * ADTS Error check // crc_check 16 only if protection_absent == 0 $byteoffset = $info['avdataoffset']; $framenumber = 0; // Init bit pattern array static $decbin = array(); // Populate $bindec for ($i = 0; $i < 256; ++$i) { $decbin[chr($i)] = str_pad(decbin($i), 8, '0', STR_PAD_LEFT); } // used to calculate bitrate below $BitrateCache = array(); while (true) { // breaks out when end-of-file encountered, or invalid data found, // or MaxFramesToScan frames have been scanned if (!Helper::intValueSupported($byteoffset)) { $info['warning'][] = 'Unable to parse AAC file beyond ' . ftell($this->getid3->fp) . ' (PHP does not support file operations beyond ' . round(PHP_INT_MAX / 1073741824) . 'GB)'; return false; } fseek($this->getid3->fp, $byteoffset, SEEK_SET); // First get substring $substring = fread($this->getid3->fp, 9); // header is 7 bytes (or 9 if CRC is present) $substringlength = strlen($substring); if ($substringlength != 9) { $info['error'][] = 'Failed to read 7 bytes at offset ' . (ftell($this->getid3->fp) - $substringlength) . ' (only read ' . $substringlength . ' bytes)'; return false; } // this would be easier with 64-bit math, but split it up to allow for 32-bit: $header1 = Helper::BigEndian2Int(substr($substring, 0, 2)); $header2 = Helper::BigEndian2Int(substr($substring, 2, 4)); $header3 = Helper::BigEndian2Int(substr($substring, 6, 1)); $info['aac']['header']['raw']['syncword'] = ($header1 & 0xfff0) >> 4; if ($info['aac']['header']['raw']['syncword'] != 0xfff) { $info['error'][] = 'Synch pattern (0x0FFF) not found at offset ' . (ftell($this->getid3->fp) - $substringlength) . ' (found 0x0' . strtoupper(dechex($info['aac']['header']['raw']['syncword'])) . ' instead)'; //if ($info['fileformat'] == 'aac') { // return true; //} unset($info['aac']); return false; } // Gather info for first frame only - this takes time to do 1000 times! if ($framenumber == 0) { $info['aac']['header_type'] = 'ADTS'; $info['fileformat'] = 'aac'; $info['audio']['dataformat'] = 'aac'; $info['aac']['header']['raw']['mpeg_version'] = ($header1 & 0x8) >> 3; $info['aac']['header']['raw']['mpeg_layer'] = ($header1 & 0x6) >> 1; $info['aac']['header']['raw']['protection_absent'] = ($header1 & 0x1) >> 0; $info['aac']['header']['raw']['profile_code'] = ($header2 & 0xc0000000) >> 30; $info['aac']['header']['raw']['sample_rate_code'] = ($header2 & 0x3c000000) >> 26; $info['aac']['header']['raw']['private_stream'] = ($header2 & 0x2000000) >> 25; $info['aac']['header']['raw']['channels_code'] = ($header2 & 0x1c00000) >> 22; $info['aac']['header']['raw']['original'] = ($header2 & 0x200000) >> 21; $info['aac']['header']['raw']['home'] = ($header2 & 0x100000) >> 20; $info['aac']['header']['raw']['copyright_stream'] = ($header2 & 0x80000) >> 19; $info['aac']['header']['raw']['copyright_start'] = ($header2 & 0x40000) >> 18; $info['aac']['header']['raw']['frame_length'] = ($header2 & 0x3ffe0) >> 5; $info['aac']['header']['mpeg_version'] = $info['aac']['header']['raw']['mpeg_version'] ? 2 : 4; $info['aac']['header']['crc_present'] = $info['aac']['header']['raw']['protection_absent'] ? false : true; $info['aac']['header']['profile'] = self::AACprofileLookup($info['aac']['header']['raw']['profile_code'], $info['aac']['header']['mpeg_version']); $info['aac']['header']['sample_frequency'] = self::AACsampleRateLookup($info['aac']['header']['raw']['sample_rate_code']); $info['aac']['header']['private'] = (bool) $info['aac']['header']['raw']['private_stream']; $info['aac']['header']['original'] = (bool) $info['aac']['header']['raw']['original']; $info['aac']['header']['home'] = (bool) $info['aac']['header']['raw']['home']; $info['aac']['header']['channels'] = $info['aac']['header']['raw']['channels_code'] == 7 ? 8 : $info['aac']['header']['raw']['channels_code']; if ($ReturnExtendedInfo) { $info['aac'][$framenumber]['copyright_id_bit'] = (bool) $info['aac']['header']['raw']['copyright_stream']; $info['aac'][$framenumber]['copyright_id_start'] = (bool) $info['aac']['header']['raw']['copyright_start']; } if ($info['aac']['header']['raw']['mpeg_layer'] != 0) { $info['warning'][] = 'Layer error - expected "0", found "' . $info['aac']['header']['raw']['mpeg_layer'] . '" instead'; } if ($info['aac']['header']['sample_frequency'] == 0) { $info['error'][] = 'Corrupt AAC file: sample_frequency == zero'; return false; } $info['audio']['sample_rate'] = $info['aac']['header']['sample_frequency']; $info['audio']['channels'] = $info['aac']['header']['channels']; } $FrameLength = ($header2 & 0x3ffe0) >> 5; if (!isset($BitrateCache[$FrameLength])) { $BitrateCache[$FrameLength] = $info['aac']['header']['sample_frequency'] / 1024 * $FrameLength * 8; } Helper::safe_inc($info['aac']['bitrate_distribution'][$BitrateCache[$FrameLength]], 1); $info['aac'][$framenumber]['aac_frame_length'] = $FrameLength; $info['aac'][$framenumber]['adts_buffer_fullness'] = ($header2 & 0x1f) << 6 & ($header3 & 0xfc) >> 2; if ($info['aac'][$framenumber]['adts_buffer_fullness'] == 0x7ff) { $info['audio']['bitrate_mode'] = 'vbr'; } else { $info['audio']['bitrate_mode'] = 'cbr'; } $info['aac'][$framenumber]['num_raw_data_blocks'] = ($header3 & 0x3) >> 0; if ($info['aac']['header']['crc_present']) { //$info['aac'][$framenumber]['crc'] = GetId3_lib::BigEndian2Int(substr($substring, 7, 2); } if (!$ReturnExtendedInfo) { unset($info['aac'][$framenumber]); } /* $rounded_precision = 5000; $info['aac']['bitrate_distribution_rounded'] = array(); foreach ($info['aac']['bitrate_distribution'] as $bitrate => $count) { $rounded_bitrate = round($bitrate / $rounded_precision) * $rounded_precision; getid3_lib::safe_inc($info['aac']['bitrate_distribution_rounded'][$rounded_bitrate], $count); } ksort($info['aac']['bitrate_distribution_rounded']); */ $byteoffset += $FrameLength; if (++$framenumber < $MaxFramesToScan && $byteoffset + 10 < $info['avdataend']) { // keep scanning } else { $info['aac']['frames'] = $framenumber; $info['playtime_seconds'] = $info['avdataend'] / $byteoffset * ($framenumber * 1024 / $info['aac']['header']['sample_frequency']); // (1 / % of file scanned) * (samples / (samples/sec)) = seconds if ($info['playtime_seconds'] == 0) { $info['error'][] = 'Corrupt AAC file: playtime_seconds == zero'; return false; } $info['audio']['bitrate'] = ($info['avdataend'] - $info['avdataoffset']) * 8 / $info['playtime_seconds']; ksort($info['aac']['bitrate_distribution']); $info['audio']['encoder_options'] = $info['aac']['header_type'] . ' ' . $info['aac']['header']['profile']; return true; } } // should never get here. }
public function RemoveID3v2() { // File MUST be writeable - CHMOD(646) at least. It's best if the // directory is also writeable, because that method is both faster and less susceptible to errors. if (is_writable(dirname($this->filename))) { // preferred method - only one copying operation, minimal chance of corrupting // original file if script is interrupted, but required directory to be writeable if (is_readable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'rb'))) { // Initialize GetId3 engine $getID3 = new GetId3Core(); $OldThisFileInfo = $getID3->analyze($this->filename); if (!Helper::intValueSupported($OldThisFileInfo['filesize'])) { $this->errors[] = 'Unable to remove ID3v2 because file is larger than ' . round(PHP_INT_MAX / 1073741824) . 'GB'; fclose($fp_source); return false; } rewind($fp_source); if ($OldThisFileInfo['avdataoffset'] !== false) { fseek($fp_source, $OldThisFileInfo['avdataoffset'], SEEK_SET); } if (is_writable($this->filename) && is_file($this->filename) && ($fp_temp = fopen($this->filename . 'getid3tmp', 'w+b'))) { while ($buffer = fread($fp_source, $this->fread_buffer_size)) { fwrite($fp_temp, $buffer, strlen($buffer)); } fclose($fp_temp); } else { $this->errors[] = 'Could not fopen("' . $this->filename . 'getid3tmp", "w+b")'; } fclose($fp_source); } else { $this->errors[] = 'Could not fopen("' . $this->filename . '", "rb")'; } if (file_exists($this->filename)) { unlink($this->filename); } rename($this->filename . 'getid3tmp', $this->filename); } elseif (is_writable($this->filename)) { // less desirable alternate method - double-copies the file, overwrites original file // and could corrupt source file if the script is interrupted or an error occurs. if (is_readable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'rb'))) { // Initialize GetId3 engine $getID3 = new GetId3Core(); $OldThisFileInfo = $getID3->analyze($this->filename); if (!Helper::intValueSupported($OldThisFileInfo['filesize'])) { $this->errors[] = 'Unable to remove ID3v2 because file is larger than ' . round(PHP_INT_MAX / 1073741824) . 'GB'; fclose($fp_source); return false; } rewind($fp_source); if ($OldThisFileInfo['avdataoffset'] !== false) { fseek($fp_source, $OldThisFileInfo['avdataoffset'], SEEK_SET); } if ($fp_temp = tmpfile()) { while ($buffer = fread($fp_source, $this->fread_buffer_size)) { fwrite($fp_temp, $buffer, strlen($buffer)); } fclose($fp_source); if (is_writable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'wb'))) { rewind($fp_temp); while ($buffer = fread($fp_temp, $this->fread_buffer_size)) { fwrite($fp_source, $buffer, strlen($buffer)); } fseek($fp_temp, -128, SEEK_END); fclose($fp_source); } else { $this->errors[] = 'Could not fopen("' . $this->filename . '", "wb")'; } fclose($fp_temp); } else { $this->errors[] = 'Could not create tmpfile()'; } } else { $this->errors[] = 'Could not fopen("' . $this->filename . '", "rb")'; } } else { $this->errors[] = 'Directory and file both not writeable'; } if (!empty($this->errors)) { return false; } return true; }
/** * @param type $min_data * * @return bool */ private function EnsureBufferHasEnoughData($min_data = 1024) { if ($this->current_offset - $this->EBMLbuffer_offset >= $this->EBMLbuffer_length - $min_data) { if (!Helper::intValueSupported($this->current_offset + $this->getid3->fread_buffer_size())) { $this->getid3->info['error'][] = 'EBML parser: cannot read past ' . $this->current_offset; return false; } $this->fseek($this->current_offset); $this->EBMLbuffer_offset = $this->current_offset; $this->EBMLbuffer = $this->fread(max($min_data, $this->getid3->fread_buffer_size())); $this->EBMLbuffer_length = strlen($this->EBMLbuffer); if ($this->EBMLbuffer_length == 0 && $this->feof()) { $this->getid3->info['error'][] = 'EBML parser: ran out of file at offset ' . $this->current_offset; return false; } } return true; }