public function Analyze() { $info =& $this->getid3->info; if (!Utils::intValueSupported($info['filesize'])) { $info['warning'][] = 'Unable to check for ID3v1 because file is larger than ' . round(PHP_INT_MAX / 1073741824) . 'GB'; return false; } $this->fseek(-256, SEEK_END); $preid3v1 = $this->fread(128); $id3v1tag = $this->fread(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; }
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 (!Utils::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) { $this->fseek(-128 * $EOCDsearchCounter, SEEK_END); $EOCDsearchData = $this->fread(128) . $EOCDsearchData; if (strstr($EOCDsearchData, 'PK' . "")) { $EOCDposition = strpos($EOCDsearchData, 'PK' . ""); $this->fseek(-128 * $EOCDsearchCounter + $EOCDposition, SEEK_END); $info['zip']['end_central_directory'] = $this->ZIPparseEndOfCentralDirectory(); $this->fseek($info['zip']['end_central_directory']['directory_offset']); $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) { zero-byte files are valid if (!empty($centraldirectoryentry['filename'])) { $info['zip']['files'] = Utils::array_merge_clobber($info['zip']['files'], Utils::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'; } // secondary check - we (should) already have all the info we NEED from the Central Directory above, but scanning each // Local File Header entry will foreach ($info['zip']['central_directory'] as $central_directory_entry) { $this->fseek($central_directory_entry['entry_offset']); if ($fileentry = $this->ZIPparseLocalFileHeader()) { $info['zip']['entries'][] = $fileentry; } else { $info['warning'][] = 'Error parsing Local File Header at offset ' . $central_directory_entry['entry_offset']; } } if (!empty($info['zip']['files']['[Content_Types].xml']) && !empty($info['zip']['files']['_rels']['.rels']) && !empty($info['zip']['files']['docProps']['app.xml']) && !empty($info['zip']['files']['docProps']['core.xml'])) { // http://technet.microsoft.com/en-us/library/cc179224.aspx $info['fileformat'] = 'zip.msoffice'; if (!empty($ThisFileInfo['zip']['files']['ppt'])) { $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; } elseif (!empty($ThisFileInfo['zip']['files']['xl'])) { $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; } elseif (!empty($ThisFileInfo['zip']['files']['word'])) { $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; } } return true; } } } if (!$this->getZIPentriesFilepointer()) { unset($info['zip']); $info['fileformat'] = ''; $info['error'][] = 'Cannot find End Of Central Directory (truncated file?)'; return false; } // 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; }
protected function fseek($bytes, $whence = SEEK_SET) { if ($this->data_string_flag) { switch ($whence) { case SEEK_SET: $this->data_string_position = $bytes; break; case SEEK_CUR: $this->data_string_position += $bytes; break; case SEEK_END: $this->data_string_position = $this->data_string_length + $bytes; break; } return 0; } else { $pos = $bytes; if ($whence == SEEK_CUR) { $pos = $this->ftell() + $bytes; } elseif ($whence == SEEK_END) { $pos = $this->getid3->info['filesize'] + $bytes; } if (!Utils::intValueSupported($pos)) { throw new Exception('cannot fseek(' . $pos . ') because beyond PHP filesystem limit', 10); } } return fseek($this->getid3->fp, $bytes, $whence); }
public function getLyrics3Data($endoffset, $version, $length) { // http://www.volweb.cz/str/tags.htm $info =& $this->getid3->info; if (!Utils::intValueSupported($endoffset)) { $info['warning'][] = 'Unable to check for Lyrics3 because file is larger than ' . round(PHP_INT_MAX / 1073741824) . 'GB'; return false; } $this->fseek($endoffset); if ($length <= 0) { return false; } $rawdata = $this->fread($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 ' . ($this->ftell() - 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 ' . ($this->ftell() - 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; }
public function Analyze() { $info =& $this->getid3->info; if (!Utils::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) { $this->fseek(0 - $id3v1tagsize - $apetagheadersize - $lyrics3tagsize, SEEK_END); $APEfooterID3v1 = $this->fread($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 { $this->fseek($this->overrideendoffset - $apetagheadersize); if ($this->fread(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']; $this->fseek($thisfile_ape['tag_offset_end'] - $apetagheadersize); $APEfooterData = $this->fread(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']) { $this->fseek($thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize'] - $apetagheadersize); $thisfile_ape['tag_offset_start'] = $this->ftell(); $APEtagData = $this->fread($thisfile_ape['footer']['raw']['tagsize'] + $apetagheadersize); } else { $thisfile_ape['tag_offset_start'] = $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize']; $this->fseek($thisfile_ape['tag_offset_start']); $APEtagData = $this->fread($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 = Utils::LittleEndian2Int(substr($APEtagData, $offset, 4)); $offset += 4; $item_flags = Utils::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 2: // Locator (URL, filename, etc), UTF-8 encoded $thisfile_ape_items_current['data'] = explode("", $thisfile_ape_items_current['data']); break; case 1: // binary data // binary data default: break; } switch (strtolower($item_key)) { // http://wiki.hydrogenaud.io/index.php?title=ReplayGain#MP3Gain case 'replaygain_track_gain': if (preg_match('#^[\\-\\+][0-9\\.,]{8}$#', $thisfile_ape_items_current['data'][0])) { $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'; } else { $info['warning'][] = 'MP3gainTrackGain value in APEtag appears invalid: "' . $thisfile_ape_items_current['data'][0] . '"'; } break; case 'replaygain_track_peak': if (preg_match('#^[0-9\\.,]{8}$#', $thisfile_ape_items_current['data'][0])) { $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] . '")'; } } else { $info['warning'][] = 'MP3gainTrackPeak value in APEtag appears invalid: "' . $thisfile_ape_items_current['data'][0] . '"'; } break; case 'replaygain_album_gain': if (preg_match('#^[\\-\\+][0-9\\.,]{8}$#', $thisfile_ape_items_current['data'][0])) { $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'; } else { $info['warning'][] = 'MP3gainAlbumGain value in APEtag appears invalid: "' . $thisfile_ape_items_current['data'][0] . '"'; } break; case 'replaygain_album_peak': if (preg_match('#^[0-9\\.,]{8}$#', $thisfile_ape_items_current['data'][0])) { $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] . '")'; } } else { $info['warning'][] = 'MP3gainAlbumPeak value in APEtag appears invalid: "' . $thisfile_ape_items_current['data'][0] . '"'; } break; case 'mp3gain_undo': if (preg_match('#^[\\-\\+][0-9]{3},[\\-\\+][0-9]{3},[NW]$#', $thisfile_ape_items_current['data'][0])) { 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; } else { $info['warning'][] = 'MP3gainUndo value in APEtag appears invalid: "' . $thisfile_ape_items_current['data'][0] . '"'; } break; case 'mp3gain_minmax': if (preg_match('#^[0-9]{3},[0-9]{3}$#', $thisfile_ape_items_current['data'][0])) { 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); } else { $info['warning'][] = 'MP3gainMinMax value in APEtag appears invalid: "' . $thisfile_ape_items_current['data'][0] . '"'; } break; case 'mp3gain_album_minmax': if (preg_match('#^[0-9]{3},[0-9]{3}$#', $thisfile_ape_items_current['data'][0])) { 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); } else { $info['warning'][] = 'MP3gainAlbumMinMax value in APEtag appears invalid: "' . $thisfile_ape_items_current['data'][0] . '"'; } 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 if (is_array($thisfile_ape_items_current['data'])) { $info['warning'][] = 'APEtag "' . $item_key . '" should be flagged as Binary data, but was incorrectly flagged as UTF-8'; $thisfile_ape_items_current['data'] = implode("", $thisfile_ape_items_current['data']); } 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']); do { $thisfile_ape_items_current['image_mime'] = ''; $imageinfo = array(); $imagechunkcheck = Utils::GetDataImageSize($thisfile_ape_items_current['data'], $imageinfo); if ($imagechunkcheck === false || !isset($imagechunkcheck[2])) { $info['warning'][] = 'APEtag "' . $item_key . '" contains invalid image data'; break; } $thisfile_ape_items_current['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]); 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(); } $comments_picture_data = array(); foreach (array('data', 'image_mime', 'image_width', 'image_height', 'imagetype', 'picturetype', 'description', 'datalength') as $picture_key) { if (isset($thisfile_ape_items_current[$picture_key])) { $comments_picture_data[$picture_key] = $thisfile_ape_items_current[$picture_key]; } } $info['ape']['comments']['picture'][] = $comments_picture_data; unset($comments_picture_data); } } 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; }
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 (!Utils::intValueSupported($byteoffset)) { $info['warning'][] = 'Unable to parse AAC file beyond ' . $this->ftell() . ' (PHP does not support file operations beyond ' . round(PHP_INT_MAX / 1073741824) . 'GB)'; return false; } $this->fseek($byteoffset); // First get substring $substring = $this->fread(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 ' . ($this->ftell() - $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 = Utils::BigEndian2Int(substr($substring, 0, 2)); $header2 = Utils::BigEndian2Int(substr($substring, 2, 4)); $header3 = Utils::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 ' . ($this->ftell() - $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; } Utils::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'] = Utils::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; Utils::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 Analyze() { $info =& $this->getid3->info; // shortcut $info['bonk'] = array(); $thisfile_bonk =& $info['bonk']; $thisfile_bonk['dataoffset'] = $info['avdataoffset']; $thisfile_bonk['dataend'] = $info['avdataend']; if (!Utils::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 $this->fseek($thisfile_bonk['dataend'] - 8); $PossibleBonkTag = $this->fread(8); while ($this->BonkIsValidTagName(substr($PossibleBonkTag, 4, 4), true)) { $BonkTagSize = Utils::LittleEndian2Int(substr($PossibleBonkTag, 0, 4)); $this->fseek(0 - $BonkTagSize, SEEK_CUR); $BonkTagOffset = $this->ftell(); $TagHeaderTest = $this->fread(5); if ($TagHeaderTest[0] != "" || substr($PossibleBonkTag, 4, 4) != strtolower(substr($PossibleBonkTag, 4, 4))) { $info['error'][] = 'Expecting "' . Utils::PrintHexBytes("" . strtoupper(substr($PossibleBonkTag, 4, 4))) . '" at offset ' . $BonkTagOffset . ', found "' . Utils::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; } $this->fseek($NextTagEndOffset); $PossibleBonkTag = $this->fread(8); } } // seek-from-beginning method for v0.4 and v0.5 if (empty($thisfile_bonk['BONK'])) { $this->fseek($thisfile_bonk['dataoffset']); do { $TagHeaderTest = $this->fread(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'])) { $this->fseek($thisfile_bonk['META']['tags']['info']); $TagHeaderTest = $this->fread(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; }
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 || !Utils::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; }