Compare commits
No commits in common. "develop" and "master" have entirely different histories.
@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace wapmorgan\Mp3Info;
|
||||
|
||||
class Mp3FileLocal
|
||||
{
|
||||
public string $fileName;
|
||||
|
||||
protected int $fileSize;
|
||||
|
||||
private $_filePtr;
|
||||
|
||||
/**
|
||||
* Creates a new local file object.
|
||||
*
|
||||
* @param string $fileName URL to open
|
||||
*/
|
||||
public function __construct(string $fileName)
|
||||
{
|
||||
$this->fileName = $fileName;
|
||||
$this->_filePtr = fopen($this->fileName, 'rb');
|
||||
$this->fileSize = filesize($this->fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file size
|
||||
*
|
||||
* @return int File size
|
||||
*/
|
||||
public function getFileSize(): int
|
||||
{
|
||||
return $this->fileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given amount of Bytes from the current file position.
|
||||
*
|
||||
* @param int $numBytes Bytes to read
|
||||
*
|
||||
* @return string Read Bytes
|
||||
*/
|
||||
public function getBytes(int $numBytes): string
|
||||
{
|
||||
return fread($this->_filePtr, $numBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current file position
|
||||
*
|
||||
* @return int File position
|
||||
*/
|
||||
public function getFilePos(): int
|
||||
{
|
||||
return ftell($this->_filePtr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file point to the given position.
|
||||
*
|
||||
* @param int $posBytes Position to jump to
|
||||
*
|
||||
* @return bool TRUE if successful
|
||||
*/
|
||||
public function seekTo(int $posBytes): bool
|
||||
{
|
||||
$result = fseek($this->_filePtr, $posBytes);
|
||||
return ($result == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the file pointer the given amount.
|
||||
*
|
||||
* @param int $posBytes Bytes to advance
|
||||
*
|
||||
* @return bool TRUE if successful
|
||||
*/
|
||||
public function seekForward(int $posBytes): bool
|
||||
{
|
||||
$newPos = $this->getFilePos() + $posBytes;
|
||||
return $this->seekTo($newPos);
|
||||
}
|
||||
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace wapmorgan\Mp3Info;
|
||||
|
||||
use \Exception;
|
||||
|
||||
class Mp3FileRemote
|
||||
{
|
||||
public string $fileName;
|
||||
public int $blockSize;
|
||||
|
||||
protected $buffer;
|
||||
protected int $filePos;
|
||||
protected $fileSize;
|
||||
|
||||
/**
|
||||
* Creates a new remote file object.
|
||||
*
|
||||
* @param string $fileName URL to open
|
||||
* @param int $blockSize Size of the blocks to query from the server (default: 4096)
|
||||
*/
|
||||
public function __construct(string $fileName, int $blockSize = 4096)
|
||||
{
|
||||
$this->fileName = $fileName;
|
||||
$this->blockSize = $blockSize;
|
||||
$this->buffer = [];
|
||||
$this->filePos = 0;
|
||||
$this->fileSize = $this->_readFileSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file size
|
||||
*
|
||||
* @return int File size
|
||||
*/
|
||||
public function getFileSize(): int
|
||||
{
|
||||
return $this->fileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a HEAD request to get the file size
|
||||
*
|
||||
* @return int Content-Length header
|
||||
*/
|
||||
private function _readFileSize(): int
|
||||
{
|
||||
// make HTTP HEAD request to get Content-Length
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'HEAD',
|
||||
],
|
||||
]);
|
||||
$result = get_headers($this->fileName, true, $context);
|
||||
return $result['Content-Length'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given amount of Bytes from the current file position.
|
||||
*
|
||||
* @param int $numBytes Bytes to read
|
||||
*
|
||||
* @return string Read Bytes
|
||||
*/
|
||||
public function getBytes(int $numBytes): string
|
||||
{
|
||||
$blockId = intdiv($this->filePos, $this->blockSize);
|
||||
$blockPos = $this->filePos % $this->blockSize;
|
||||
|
||||
$output = [];
|
||||
|
||||
do {
|
||||
// make sure we have this block
|
||||
if (!$this->downloadBlock($blockId)) {
|
||||
throw new Exception('Error downloading block ' . $blockId . ' (starting at pos. ' . ($blockId * $this->blockSize) . ')!');
|
||||
}
|
||||
if ($blockPos + $numBytes >= $this->blockSize) {
|
||||
// length of request is more than this block has, truncate to block len
|
||||
$subLen = $this->blockSize - $blockPos;
|
||||
} else {
|
||||
// requested length fits inside this block
|
||||
$subLen = $numBytes;
|
||||
}
|
||||
// $subLen = ($blockPos + $numBytes >= $this->blockSize) ? ($this->blockSize - $blockPos) : $numBytes;
|
||||
$output[] = substr($this->buffer[$blockId], $blockPos, $subLen);
|
||||
$this->filePos += $subLen;
|
||||
$numBytes -= $subLen;
|
||||
// advance to next block
|
||||
$blockPos = 0;
|
||||
$blockId++;
|
||||
} while ($numBytes > 0);
|
||||
|
||||
return implode('', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current file position
|
||||
*
|
||||
* @return int File position
|
||||
*/
|
||||
public function getFilePos(): int
|
||||
{
|
||||
return $this->filePos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file pointer to the given position.
|
||||
*
|
||||
* @param int $posBytes Position to jump to
|
||||
*
|
||||
* @return bool TRUE if successful
|
||||
*/
|
||||
public function seekTo(int $posBytes): bool
|
||||
{
|
||||
if ($posBytes < 0 || $posBytes > $this->fileSize) {
|
||||
return false;
|
||||
}
|
||||
$this->filePos = $posBytes;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the file pointer the given amount.
|
||||
*
|
||||
* @param int $posBytes Bytes to advance
|
||||
*
|
||||
* @return bool TRUE if successful
|
||||
*/
|
||||
public function seekForward(int $posBytes): bool
|
||||
{
|
||||
$newPos = $this->filePos + $posBytes;
|
||||
return $this->seekTo($newPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the given block if needed
|
||||
*
|
||||
* @param int $blockNo Block to download
|
||||
*
|
||||
* @return bool TRUE if successful
|
||||
*/
|
||||
protected function downloadBlock(int $blockNo): bool
|
||||
{
|
||||
if (array_key_exists($blockNo, $this->buffer)) {
|
||||
// already downloaded
|
||||
return true;
|
||||
}
|
||||
$bytesFrom = $blockNo * $this->blockSize;
|
||||
$bytesTo = $bytesFrom + $this->blockSize - 1;
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => [
|
||||
'Range: bytes=' . $bytesFrom . '-' . $bytesTo,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$filePtr = fopen($this->fileName, 'rb', false, $context);
|
||||
if ($filePtr === false) {
|
||||
return false;
|
||||
}
|
||||
$this->buffer[$blockNo] = fread($filePtr, $this->blockSize);
|
||||
$status = stream_get_meta_data($filePtr);
|
||||
$httpStatus = explode(' ', $status['wrapper_data'][0])[1];
|
||||
if ($httpStatus != '206') {
|
||||
if ($httpStatus != '200') {
|
||||
echo 'Download error!' . PHP_EOL;
|
||||
var_dump($status);
|
||||
return false;
|
||||
}
|
||||
echo 'Server doesn\'t support partial content!' . PHP_EOL;
|
||||
// Content received is whole file from start
|
||||
if ($blockNo != 0) {
|
||||
// move block to start if needed
|
||||
$this->buffer[0] =& $this->buffer[$blockNo];
|
||||
unset($this->buffer[$blockNo]);
|
||||
$blockNo = 0;
|
||||
}
|
||||
// receive remaining parts while we're at it
|
||||
while (!feof($filePtr)) {
|
||||
$blockNo++;
|
||||
$this->buffer[$blockNo] = fread($filePtr, $this->blockSize);
|
||||
}
|
||||
}
|
||||
fclose($filePtr);
|
||||
return true;
|
||||
}
|
||||
}
|
635
src/Mp3Info.php
635
src/Mp3Info.php
@ -1,14 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace wapmorgan\Mp3Info;
|
||||
|
||||
require __DIR__ . '/Mp3FileLocal.php';
|
||||
require __DIR__ . '/Mp3FileRemote.php';
|
||||
|
||||
use \wapmorgan\Mp3Info\Mp3FileLocal;
|
||||
use \wapmorgan\Mp3Info\Mp3FileRemote;
|
||||
use \Exception;
|
||||
use \RuntimeException;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* This class extracts information about an mpeg audio. (supported mpeg versions: MPEG-1, MPEG-2)
|
||||
@ -29,8 +23,7 @@ use \RuntimeException;
|
||||
* * {@link https://multimedia.cx/mp3extensions.txt Descripion of VBR header "Xing"}
|
||||
* * {@link http://gabriel.mp3-tech.org/mp3infotag.html Xing, Info and Lame tags specifications}
|
||||
*/
|
||||
class Mp3Info
|
||||
{
|
||||
class Mp3Info {
|
||||
const TAG1_SYNC = 'TAG';
|
||||
const TAG2_SYNC = 'ID3';
|
||||
const VBR_SYNC = 'Xing';
|
||||
@ -86,11 +79,6 @@ class Mp3Info
|
||||
|
||||
public static $framesCountRead = 2;
|
||||
|
||||
/**
|
||||
* @var Mp3File File object for I/O handling
|
||||
*/
|
||||
protected $fileObj;
|
||||
|
||||
/**
|
||||
* @var int MPEG codec version (1 or 2 or 2.5 or undefined)
|
||||
*/
|
||||
@ -121,31 +109,6 @@ class Mp3Info
|
||||
*/
|
||||
public $sampleRate;
|
||||
|
||||
/**
|
||||
* @var bool Header protection by 16 bit CRC
|
||||
*/
|
||||
public $isProtected;
|
||||
|
||||
/**
|
||||
* @var bool Frame data is padded with one slot
|
||||
*/
|
||||
public $isPadded;
|
||||
|
||||
/**
|
||||
* @var bool Private bit (only informative)
|
||||
*/
|
||||
public $isPrivate;
|
||||
|
||||
/**
|
||||
* @var bool Copyright bit (only informative)
|
||||
*/
|
||||
public $isCopyright;
|
||||
|
||||
/**
|
||||
* @var bool Original bit (only informative)
|
||||
*/
|
||||
public $isOriginal;
|
||||
|
||||
/**
|
||||
* @var boolean Contains true if audio has variable bit rate
|
||||
*/
|
||||
@ -191,13 +154,11 @@ class Mp3Info
|
||||
* @var int Major version of id3v2 tag (if id3v2 present) (2 or 3 or 4)
|
||||
*/
|
||||
public $id3v2MajorVersion;
|
||||
public $id3v2Version;
|
||||
|
||||
/**
|
||||
* @var int Minor version of id3v2 tag (if id3v2 present)
|
||||
*/
|
||||
public $id3v2MinorVersion;
|
||||
public $id3v2Revision;
|
||||
|
||||
/**
|
||||
* @var array List of id3v2 header flags (if id3v2 present)
|
||||
@ -247,23 +208,29 @@ class Mp3Info
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(string $filename, bool $parseTags = false)
|
||||
{
|
||||
public function __construct($filename, $parseTags = false) {
|
||||
if (self::$_bitRateTable === null)
|
||||
self::$_bitRateTable = require __DIR__.'/../data/bitRateTable.php';
|
||||
self::$_bitRateTable = require dirname(__FILE__).'/../data/bitRateTable.php';
|
||||
if (self::$_sampleRateTable === null)
|
||||
self::$_sampleRateTable = require __DIR__.'/../data/sampleRateTable.php';
|
||||
self::$_sampleRateTable = require dirname(__FILE__).'/../data/sampleRateTable.php';
|
||||
|
||||
$this->_fileName = $filename;
|
||||
if (str_contains($filename, '://')) {
|
||||
$this->fileObj = new Mp3FileRemote($filename);
|
||||
$isLocal = (strpos($filename, '://') === false);
|
||||
if (!$isLocal) {
|
||||
$this->_fileSize = static::getUrlContentLength($filename);
|
||||
} else {
|
||||
$this->fileObj = new Mp3FileLocal($filename);
|
||||
if (!file_exists($filename)) {
|
||||
throw new \Exception('File ' . $filename . ' is not present!');
|
||||
}
|
||||
$this->_fileSize = filesize($filename);
|
||||
}
|
||||
|
||||
if ($isLocal and !static::isValidAudio($filename)) {
|
||||
throw new \Exception('File ' . $filename . ' is not mpeg/audio!');
|
||||
}
|
||||
$this->_fileSize = $this->fileObj->getFileSize();
|
||||
|
||||
$mode = $parseTags ? self::META | self::TAGS : self::META;
|
||||
$this->audioSize = $this->parseAudio($mode);
|
||||
$this->audioSize = $this->parseAudio($this->_fileName, $this->_fileSize, $mode);
|
||||
}
|
||||
|
||||
|
||||
@ -276,74 +243,79 @@ class Mp3Info
|
||||
return null;
|
||||
}
|
||||
|
||||
$curPos = $this->fileObj->getFilePos();
|
||||
$this->fileObj->seekTo($this->coverProperties['offset']);
|
||||
$data = $this->fileObj->getBytes($this->coverProperties['size']);
|
||||
$this->fileObj->seekTo($curPos);
|
||||
$fp = fopen($this->_fileName, 'rb');
|
||||
if ($fp === false) {
|
||||
return false;
|
||||
}
|
||||
fseek($fp, $this->coverProperties['offset']);
|
||||
$data = fread($fp, $this->coverProperties['size']);
|
||||
fclose($fp);
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getSynchsafeSize(string $rawBytes): int
|
||||
{
|
||||
$sizeBytes = unpack('C4', $rawBytes);
|
||||
$size = $sizeBytes[1] << 21 | $sizeBytes[2] << 14 | $sizeBytes[3] << 7 | $sizeBytes[4];
|
||||
return $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads audio file in binary mode.
|
||||
* mpeg audio file structure:
|
||||
* ID3V2 TAG - provides a lot of meta data. [optional]
|
||||
* MPEG AUDIO FRAMES - contains audio data. A frame consists of a frame header and a frame data. The first frame may contain extra information about mp3 (marked with "Xing" or "Info" string). Rest of frames can contain only audio data.
|
||||
* ID3V1 TAG - provides a few of meta data. [optional]
|
||||
* @param string $filename
|
||||
* @param int $fileSize
|
||||
* @param int $mode
|
||||
* @return float|int
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function parseAudio($mode)
|
||||
{
|
||||
private function parseAudio($filename, $fileSize, $mode) {
|
||||
$time = microtime(true);
|
||||
|
||||
// create temp storage for media
|
||||
if (strpos($filename, '://') !== false) {
|
||||
$fp = fopen('php://memory', 'rwb');
|
||||
fwrite($fp, file_get_contents($filename));
|
||||
rewind($fp);
|
||||
} else {
|
||||
$fp = fopen($filename, 'rb');
|
||||
}
|
||||
|
||||
/** @var int Size of audio data (exclude tags size) */
|
||||
$audioSize = $this->fileObj->getFileSize();
|
||||
$audioSize = $fileSize;
|
||||
|
||||
// parse tags
|
||||
if ($this->fileObj->getBytes(3) == self::TAG2_SYNC) {
|
||||
if ($mode & self::TAGS) {
|
||||
$audioSize -= ($this->_id3Size = $this->readId3v2Body());
|
||||
} else {
|
||||
$this->fileObj->seekForward(2); // 2 bytes of tag version
|
||||
$this->fileObj->seekForward(1); // 1 byte of tag flags
|
||||
$size = $this->getSynchsafeSize($this->fileObj->getBytes(4));
|
||||
$size += 10; // add header size
|
||||
if (fread($fp, 3) == self::TAG2_SYNC) {
|
||||
if ($mode & self::TAGS) $audioSize -= ($this->_id3Size = $this->readId3v2Body($fp));
|
||||
else {
|
||||
fseek($fp, 2, SEEK_CUR); // 2 bytes of tag version
|
||||
fseek($fp, 1, SEEK_CUR); // 1 byte of tag flags
|
||||
$sizeBytes = $this->readBytes($fp, 4);
|
||||
array_walk($sizeBytes, function (&$value) {
|
||||
$value = substr(str_pad(base_convert($value, 10, 2), 8, 0, STR_PAD_LEFT), 1);
|
||||
});
|
||||
$size = bindec(implode($sizeBytes)) + 10;
|
||||
$audioSize -= ($this->_id3Size = $size);
|
||||
}
|
||||
}
|
||||
|
||||
$this->fileObj->seekTo($this->fileObj->getFileSize() - 128);
|
||||
if ($this->fileObj->getBytes(3) == self::TAG1_SYNC) {
|
||||
if ($mode & self::TAGS) {
|
||||
$audioSize -= $this->_readId3v1();
|
||||
} else {
|
||||
$audioSize -= 128;
|
||||
}
|
||||
fseek($fp, $fileSize - 128);
|
||||
if (fread($fp, 3) == self::TAG1_SYNC) {
|
||||
if ($mode & self::TAGS) $audioSize -= $this->readId3v1Body($fp);
|
||||
else $audioSize -= 128;
|
||||
}
|
||||
|
||||
if ($mode & self::TAGS) {
|
||||
$this->fillTags();
|
||||
}
|
||||
|
||||
$this->fileObj->seekTo(0);
|
||||
fseek($fp, 0);
|
||||
// audio meta
|
||||
if ($mode & self::META) {
|
||||
if ($this->_id3Size !== null) $this->fileObj->seekTo($this->_id3Size);
|
||||
if ($this->_id3Size !== null) fseek($fp, $this->_id3Size);
|
||||
/**
|
||||
* First frame can lie. Need to fix in the future.
|
||||
* @link https://github.com/wapmorgan/Mp3Info/issues/13#issuecomment-447470813
|
||||
* Read first N frames
|
||||
*/
|
||||
for ($i = 0; $i < self::$framesCountRead; $i++) {
|
||||
$framesCount = $this->_readMpegFrame();
|
||||
$framesCount = $this->readMpegFrame($fp);
|
||||
}
|
||||
|
||||
$this->_framesCount = $framesCount !== null
|
||||
@ -366,124 +338,128 @@ class Mp3Info
|
||||
// Calculate total number of audio samples (framesCount * sampleInFrameCount) / samplesInSecondCount
|
||||
$this->duration = ($this->_framesCount - 1) * $samples_in_second / $this->sampleRate;
|
||||
}
|
||||
fclose($fp);
|
||||
|
||||
$this->_parsingTime = microtime(true) - $time;
|
||||
return $audioSize;
|
||||
}
|
||||
|
||||
private function _findNextMpegFrame(int $headerSeekLimit): ?string
|
||||
{
|
||||
// find frame sync
|
||||
$headerSeekMax = $this->fileObj->getFilePos() + $headerSeekLimit;
|
||||
$headerBytes = $this->fileObj->getBytes(3); // preload with 3 Bytes
|
||||
do {
|
||||
$headerBytes .= $this->fileObj->getBytes(1); // load next Byte
|
||||
$headerBytes = substr($headerBytes, -4); // limit to 4 Bytes
|
||||
|
||||
if ((unpack('n', $headerBytes)[1] & self::FRAME_SYNC) === self::FRAME_SYNC) {
|
||||
return $headerBytes;
|
||||
}
|
||||
} while ($this->fileObj->getFilePos() <= $headerSeekMax);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read first frame information.
|
||||
*
|
||||
* @link https://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header
|
||||
* @param resource $fp
|
||||
* @return int Number of frames (if present if first frame of VBR-file)
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function _readMpegFrame()
|
||||
{
|
||||
$headerBytes = $this->_findNextMpegFrame(self::$headerSeekLimit);
|
||||
$pos = $this->fileObj->getFilePos() - 4;
|
||||
if (is_null($headerBytes)) {
|
||||
throw new Exception('No Mpeg frame header found up until pos ' . $pos . '(0x' . dechex($pos) . ')!');
|
||||
private function readMpegFrame($fp) {
|
||||
$header_seek_pos = ftell($fp) + self::$headerSeekLimit;
|
||||
do {
|
||||
$pos = ftell($fp);
|
||||
$first_header_byte = $this->readBytes($fp, 1);
|
||||
if ($first_header_byte[0] === 0xFF) {
|
||||
$second_header_byte = $this->readBytes($fp, 1);
|
||||
if ((($second_header_byte[0] >> 5) & 0b111) == 0b111) {
|
||||
fseek($fp, $pos);
|
||||
$header_bytes = $this->readBytes($fp, 4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
fseek($fp, 1, SEEK_CUR);
|
||||
} while (ftell($fp) <= $header_seek_pos);
|
||||
|
||||
if (!isset($header_bytes) || $header_bytes[0] !== 0xFF || (($header_bytes[1] >> 5) & 0b111) != 0b111) {
|
||||
throw new \Exception('At '.$pos
|
||||
.'(0x'.dechex($pos).') should be a frame header!');
|
||||
}
|
||||
|
||||
// 2nd Byte: (rest of frame sync), Version, Layer, Protection
|
||||
$this->codecVersion = [self::MPEG_25, null, self::MPEG_2, self::MPEG_1][ord($headerBytes[1]) >> 3 & 0b11];
|
||||
$this->layerVersion = [null, self::LAYER_3, self::LAYER_2, self::LAYER_1][ord($headerBytes[1]) >> 1 & 0b11];
|
||||
$this->isProtected = !((ord($headerBytes[1]) & 0b1) == 0b1); // inverted as 0=protected, 1=not protected
|
||||
|
||||
// 3rd Byte: Bitrate, Sampling rate, Padding, Private
|
||||
$this->bitRate = self::$_bitRateTable[$this->codecVersion][$this->layerVersion][ord($headerBytes[2]) >> 4];
|
||||
$this->sampleRate = self::$_sampleRateTable[$this->codecVersion][(ord($headerBytes[2]) >> 2) & 0b11];
|
||||
if ($this->sampleRate === false) {
|
||||
return null;
|
||||
switch ($header_bytes[1] >> 3 & 0b11) {
|
||||
case 0b00: $this->codecVersion = self::MPEG_25; break;
|
||||
case 0b01: $this->codecVersion = self::CODEC_UNDEFINED; break;
|
||||
case 0b10: $this->codecVersion = self::MPEG_2; break;
|
||||
case 0b11: $this->codecVersion = self::MPEG_1; break;
|
||||
}
|
||||
$this->isPadded = ((ord($headerBytes[2]) & 0b10) == 0b10);
|
||||
$this->isPrivate = ((ord($headerBytes[2]) & 0b1) == 0b1);
|
||||
|
||||
// 4th Byte: Channels, Mode extension, Copyright, Original, Emphasis
|
||||
$this->channel = [self::STEREO, self::JOINT_STEREO, self::DUAL_MONO, self::MONO][ord($headerBytes[3]) >> 6];
|
||||
// TODO: Mode extension (2 bits)
|
||||
$this->isCopyright = ((ord($headerBytes[3]) & 0b1000) == 0b1000);
|
||||
$this->isOriginal = ((ord($headerBytes[3]) & 0b100) == 0b100);
|
||||
// TODO: Emphasis (2 bits)
|
||||
switch ($header_bytes[1] >> 1 & 0b11) {
|
||||
case 0b01: $this->layerVersion = self::LAYER_3; break;
|
||||
case 0b10: $this->layerVersion = self::LAYER_2; break;
|
||||
case 0b11: $this->layerVersion = self::LAYER_1; break;
|
||||
}
|
||||
|
||||
$this->bitRate = self::$_bitRateTable[$this->codecVersion][$this->layerVersion][$header_bytes[2] >> 4];
|
||||
$this->sampleRate = self::$_sampleRateTable[$this->codecVersion][($header_bytes[2] >> 2) & 0b11];
|
||||
|
||||
switch ($header_bytes[3] >> 6) {
|
||||
case 0b00: $this->channel = self::STEREO; break;
|
||||
case 0b01: $this->channel = self::JOINT_STEREO; break;
|
||||
case 0b10: $this->channel = self::DUAL_MONO; break;
|
||||
case 0b11: $this->channel = self::MONO; break;
|
||||
}
|
||||
|
||||
$vbr_offset = self::$_vbrOffsets[$this->codecVersion][$this->channel == self::MONO ? 0 : 1];
|
||||
|
||||
// check for VBR
|
||||
$this->fileObj->seekTo($pos + $vbr_offset);
|
||||
if ($this->fileObj->getBytes(4) == self::VBR_SYNC) {
|
||||
fseek($fp, $pos + $vbr_offset);
|
||||
if (fread($fp, 4) == self::VBR_SYNC) {
|
||||
$this->isVbr = true;
|
||||
$flagsBytes = $this->fileObj->getBytes(4);
|
||||
$flagsBytes = $this->readBytes($fp, 4);
|
||||
|
||||
// VBR frames count presence
|
||||
if ((ord($flagsBytes[3]) & 2)) {
|
||||
$this->vbrProperties['frames'] = implode(unpack('N', $this->fileObj->getBytes(4)));
|
||||
if (($flagsBytes[3] & 2)) {
|
||||
$this->vbrProperties['frames'] = implode(unpack('N', fread($fp, 4)));
|
||||
}
|
||||
// VBR stream size presence
|
||||
if (ord($flagsBytes[3]) & 4) {
|
||||
$this->vbrProperties['bytes'] = implode(unpack('N', $this->fileObj->getBytes(4)));
|
||||
if ($flagsBytes[3] & 4) {
|
||||
$this->vbrProperties['bytes'] = implode(unpack('N', fread($fp, 4)));
|
||||
}
|
||||
// VBR TOC presence
|
||||
if (ord($flagsBytes[3]) & 1) {
|
||||
$this->fileObj->seekForward(100);
|
||||
if ($flagsBytes[3] & 1) {
|
||||
fseek($fp, 100, SEEK_CUR);
|
||||
}
|
||||
// VBR quality
|
||||
if (ord($flagsBytes[3]) & 8) {
|
||||
$this->vbrProperties['quality'] = implode(unpack('N', $this->fileObj->getBytes(4)));
|
||||
if ($flagsBytes[3] & 8) {
|
||||
$this->vbrProperties['quality'] = implode(unpack('N', fread($fp, 4)));
|
||||
}
|
||||
}
|
||||
|
||||
// go to the end of frame
|
||||
if ($this->layerVersion == self::LAYER_1) {
|
||||
$this->_cbrFrameSize = floor((12 * $this->bitRate / $this->sampleRate + (ord($headerBytes[2]) >> 1 & 0b1)) * 4);
|
||||
$this->_cbrFrameSize = floor((12 * $this->bitRate / $this->sampleRate + ($header_bytes[2] >> 1 & 0b1)) * 4);
|
||||
} else {
|
||||
$this->_cbrFrameSize = floor(144 * $this->bitRate / $this->sampleRate + (ord($headerBytes[2]) >> 1 & 0b1));
|
||||
$this->_cbrFrameSize = floor(144 * $this->bitRate / $this->sampleRate + ($header_bytes[2] >> 1 & 0b1));
|
||||
}
|
||||
|
||||
$this->fileObj->seekTo($pos + $this->_cbrFrameSize);
|
||||
fseek($fp, $pos + $this->_cbrFrameSize);
|
||||
|
||||
return $this->vbrProperties['frames'] ?? null;
|
||||
return isset($this->vbrProperties['frames']) ? $this->vbrProperties['frames'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fp
|
||||
* @param $n
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function readBytes($fp, $n) {
|
||||
$raw = fread($fp, $n);
|
||||
if (strlen($raw) !== $n) throw new \Exception('Unexpected end of file!');
|
||||
$bytes = array();
|
||||
for($i = 0; $i < $n; $i++) $bytes[$i] = ord($raw[$i]);
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads id3v1 tag.
|
||||
*
|
||||
* @link https://id3.org/ID3v1
|
||||
* @return int Returns length of id3v1 tag.
|
||||
*/
|
||||
private function _readId3v1(): int
|
||||
{
|
||||
$this->tags1['song'] = trim($this->fileObj->getBytes(30));
|
||||
$this->tags1['artist'] = trim($this->fileObj->getBytes(30));
|
||||
$this->tags1['album'] = trim($this->fileObj->getBytes(30));
|
||||
$this->tags1['year'] = trim($this->fileObj->getBytes(4));
|
||||
$comment = $this->fileObj->getBytes(30);
|
||||
if ($comment[28] == "\x00" && $comment[29] != "\x00") {
|
||||
// id3v1.1 - last Byte of comment is trackNo
|
||||
$this->tags1['track'] = ord($comment[29]);
|
||||
$this->tags1['comment'] = trim(substr($comment, 0, 28));
|
||||
} else {
|
||||
// id3v1.0
|
||||
$this->tags1['comment'] = trim($comment);
|
||||
}
|
||||
$this->tags1['genre'] = '(' . ord($this->fileObj->getBytes(1)) . ')';
|
||||
private function readId3v1Body($fp) {
|
||||
$this->tags1['song'] = trim(fread($fp, 30));
|
||||
$this->tags1['artist'] = trim(fread($fp, 30));
|
||||
$this->tags1['album'] = trim(fread($fp, 30));
|
||||
$this->tags1['year'] = trim(fread($fp, 4));
|
||||
$this->tags1['comment'] = trim(fread($fp, 28));
|
||||
fseek($fp, 1, SEEK_CUR);
|
||||
$this->tags1['track'] = ord(fread($fp, 1));
|
||||
$this->tags1['genre'] = ord(fread($fp, 1));
|
||||
return 128;
|
||||
}
|
||||
|
||||
@ -536,59 +512,60 @@ class Mp3Info
|
||||
* @return int Returns length of id3v2 tag.
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function readId3v2Body()
|
||||
private function readId3v2Body($fp)
|
||||
{
|
||||
// read the rest of the id3v2 header
|
||||
$raw = $this->fileObj->getBytes(3);
|
||||
$data = unpack('Cversion/Crevision/Cflags', $raw);
|
||||
$this->id3v2Version = $data['version'];
|
||||
$this->id3v2Revision = $data['revision'];
|
||||
// backwards compatibility:
|
||||
$this->id3v2MajorVersion = $data['version'];
|
||||
$this->id3v2MinorVersion = $data['revision'];
|
||||
|
||||
$this->id3v2Flags = array();
|
||||
|
||||
if ($this->id3v2Version >= 2) {
|
||||
// parse id3v2.2.0 header flags
|
||||
$this->id3v2Flags['unsynchronisation'] = ($data['flags'] & 128 == 128);
|
||||
$this->id3v2Flags['compression'] = ($data['flags'] & 64 == 64);
|
||||
$raw = fread($fp, 7);
|
||||
$data = unpack('cmajor_version/cminor_version/H*', $raw);
|
||||
$this->id3v2MajorVersion = $data['major_version'];
|
||||
$this->id3v2MinorVersion = $data['minor_version'];
|
||||
$data = str_pad(base_convert($data[1], 16, 2), 40, 0, STR_PAD_LEFT);
|
||||
$flags = substr($data, 0, 8);
|
||||
if ($this->id3v2MajorVersion == 2) { // parse id3v2.2.0 header flags
|
||||
$this->id3v2Flags = array(
|
||||
'unsynchronisation' => (bool)substr($flags, 0, 1),
|
||||
'compression' => (bool)substr($flags, 1, 1),
|
||||
);
|
||||
} else if ($this->id3v2MajorVersion == 3) { // parse id3v2.3.0 header flags
|
||||
$this->id3v2Flags = array(
|
||||
'unsynchronisation' => (bool)substr($flags, 0, 1),
|
||||
'extended_header' => (bool)substr($flags, 1, 1),
|
||||
'experimental_indicator' => (bool)substr($flags, 2, 1),
|
||||
);
|
||||
if ($this->id3v2Flags['extended_header'])
|
||||
throw new \Exception('NEED TO PARSE EXTENDED HEADER!');
|
||||
} else if ($this->id3v2MajorVersion == 4) { // parse id3v2.4.0 header flags
|
||||
$this->id3v2Flags = array(
|
||||
'unsynchronisation' => (bool)substr($flags, 0, 1),
|
||||
'extended_header' => (bool)substr($flags, 1, 1),
|
||||
'experimental_indicator' => (bool)substr($flags, 2, 1),
|
||||
'footer_present' => (bool)substr($flags, 3, 1),
|
||||
);
|
||||
if ($this->id3v2Flags['extended_header'])
|
||||
throw new \Exception('NEED TO PARSE EXTENDED HEADER!');
|
||||
if ($this->id3v2Flags['footer_present'])
|
||||
throw new \Exception('NEED TO PARSE id3v2.4 FOOTER!');
|
||||
}
|
||||
|
||||
if ($this->id3v2Version >= 3) {
|
||||
// id3v2.3 changes second bit from compression to extended_header
|
||||
$this->id3v2Flags['extended_header'] = &$this->id3v2Flags['compression'];
|
||||
unset($this->id3v2Flags['compression']);
|
||||
// parse additional id3v2.3.0 header flags
|
||||
$this->id3v2Flags['experimental_indicator'] = ($data['flags'] & 32 == 32);
|
||||
$size = substr($data, 8, 32);
|
||||
|
||||
if ($this->id3v2Flags['extended_header']) {
|
||||
throw new Exception('NEED TO PARSE EXTENDED HEADER!');
|
||||
}
|
||||
}
|
||||
// some fucking shit
|
||||
// getting only 7 of 8 bits of size bytes
|
||||
$sizes = str_split($size, 8);
|
||||
array_walk($sizes, function (&$value) {
|
||||
$value = substr($value, 1);
|
||||
});
|
||||
$size = implode($sizes);
|
||||
$size = bindec($size);
|
||||
|
||||
if ($this->id3v2Version >= 4) {
|
||||
// parse additional id3v2.4.0 header flags
|
||||
$this->id3v2Flags['footer_present'] = ($data['flags'] & 16 == 16);
|
||||
|
||||
if ($this->id3v2Flags['footer_present']) {
|
||||
// footer is a copy of header - so can be ignored
|
||||
// (10 Bytes not included in $size)
|
||||
//throw new Exception('NEED TO PARSE id3v2.4 FOOTER!');
|
||||
}
|
||||
}
|
||||
|
||||
$size = $this->getSynchsafeSize($this->fileObj->getBytes(4));
|
||||
|
||||
if ($this->id3v2Version == 2) {
|
||||
if ($this->id3v2MajorVersion == 2) {
|
||||
// parse id3v2.2.0 body
|
||||
/*throw new \Exception('NEED TO PARSE id3v2.2.0 flags!');*/
|
||||
} elseif ($this->id3v2Version == 3) {
|
||||
} else if ($this->id3v2MajorVersion == 3) {
|
||||
// parse id3v2.3.0 body
|
||||
$this->parseId3v23Body(10 + $size);
|
||||
} elseif ($this->id3v2Version == 4) {
|
||||
$this->parseId3v23Body($fp, 10 + $size);
|
||||
} else if ($this->id3v2MajorVersion == 4) {
|
||||
// parse id3v2.4.0 body
|
||||
$this->parseId3v24Body(10 + $size);
|
||||
$this->parseId3v24Body($fp, 10 + $size);
|
||||
}
|
||||
|
||||
return 10 + $size; // 10 bytes - header, rest - body
|
||||
@ -598,14 +575,13 @@ class Mp3Info
|
||||
* Parses id3v2.3.0 tag body.
|
||||
* @todo Complete.
|
||||
*/
|
||||
protected function parseId3v23Body($lastByte)
|
||||
{
|
||||
while ($this->fileObj->getFilePos() < $lastByte) {
|
||||
$raw = $this->fileObj->getBytes(10);
|
||||
protected function parseId3v23Body($fp, $lastByte) {
|
||||
while (ftell($fp) < $lastByte) {
|
||||
$raw = fread($fp, 10);
|
||||
$frame_id = substr($raw, 0, 4);
|
||||
|
||||
if ($frame_id == str_repeat(chr(0), 4)) {
|
||||
$this->fileObj->seekTo($lastByte);
|
||||
fseek($fp, $lastByte);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -613,7 +589,6 @@ class Mp3Info
|
||||
$frame_size = $data['frame_size'];
|
||||
$flags = base_convert($data['flags'], 16, 2);
|
||||
$this->id3v2TagsFlags[$frame_id] = array(
|
||||
'payload_size' => $frame_size,
|
||||
'flags' => array(
|
||||
'tag_alter_preservation' => (bool)substr($flags, 0, 1),
|
||||
'file_alter_preservation' => (bool)substr($flags, 1, 1),
|
||||
@ -668,7 +643,7 @@ class Mp3Info
|
||||
case 'TSIZ': # Size
|
||||
case 'TSRC': # ISRC (international standard recording code)
|
||||
case 'TSSE': # Software/Hardware and settings used for encoding
|
||||
$this->tags2[$frame_id] = $this->handleTextFrame($frame_size, $this->fileObj->getBytes($frame_size));
|
||||
$this->tags2[$frame_id] = $this->handleTextFrame($frame_size, fread($fp, $frame_size));
|
||||
break;
|
||||
################# Text information frames
|
||||
|
||||
@ -708,15 +683,15 @@ class Mp3Info
|
||||
// case 'SYLT': # Synchronized lyric/text
|
||||
// break;
|
||||
case 'COMM': # Comments
|
||||
$dataEnd = $this->fileObj->getFilePos() + $frame_size;
|
||||
$raw = $this->fileObj->getBytes(4);
|
||||
$dataEnd = ftell($fp) + $frame_size;
|
||||
$raw = fread($fp, 4);
|
||||
$data = unpack('C1encoding/A3language', $raw);
|
||||
// read until \null character
|
||||
$short_description = '';
|
||||
$last_null = false;
|
||||
$actual_text = false;
|
||||
while ($this->fileObj->getFilePos() < $dataEnd) {
|
||||
$char = $this->fileObj->getBytes(1);
|
||||
while (ftell($fp) < $dataEnd) {
|
||||
$char = fgetc($fp);
|
||||
if ($char == "\00" && $actual_text === false) {
|
||||
if ($data['encoding'] == 0x1) { # two null-bytes for utf-16
|
||||
if ($last_null)
|
||||
@ -746,20 +721,20 @@ class Mp3Info
|
||||
// break;
|
||||
case 'APIC': # Attached picture
|
||||
$this->hasCover = true;
|
||||
$dataEnd = $this->fileObj->getFilePos() + $frame_size;
|
||||
$this->coverProperties = ['text_encoding' => ord($this->fileObj->getBytes(1))];
|
||||
$last_byte = ftell($fp) + $frame_size;
|
||||
$this->coverProperties = ['text_encoding' => ord(fread($fp, 1))];
|
||||
// fseek($fp, $frame_size - 4, SEEK_CUR);
|
||||
$this->coverProperties['mime_type'] = $this->readTextUntilNull($dataEnd);
|
||||
$this->coverProperties['picture_type'] = ord($this->fileObj->getBytes(1));
|
||||
$this->coverProperties['description'] = $this->readTextUntilNull($dataEnd);
|
||||
$this->coverProperties['offset'] = $this->fileObj->getFilePos();
|
||||
$this->coverProperties['size'] = $dataEnd - $this->fileObj->getFilePos();
|
||||
$this->fileObj->seekTo($dataEnd);
|
||||
$this->coverProperties['mime_type'] = $this->readTextUntilNull($fp, $last_byte);
|
||||
$this->coverProperties['picture_type'] = ord(fread($fp, 1));
|
||||
$this->coverProperties['description'] = $this->readTextUntilNull($fp, $last_byte);
|
||||
$this->coverProperties['offset'] = ftell($fp);
|
||||
$this->coverProperties['size'] = $last_byte - ftell($fp);
|
||||
fseek($fp, $last_byte);
|
||||
break;
|
||||
// case 'GEOB': # General encapsulated object
|
||||
// break;
|
||||
case 'PCNT': # Play counter
|
||||
$data = unpack('L', $this->fileObj->getBytes($frame_size));
|
||||
$data = unpack('L', fread($fp, $frame_size));
|
||||
$this->tags2[$frame_id] = $data[1];
|
||||
break;
|
||||
// case 'POPM': # Popularimeter
|
||||
@ -785,7 +760,7 @@ class Mp3Info
|
||||
// case 'PRIV': # Private frame
|
||||
// break;
|
||||
default:
|
||||
$this->fileObj->seekForward($frame_size);
|
||||
fseek($fp, $frame_size, SEEK_CUR);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -793,25 +768,24 @@ class Mp3Info
|
||||
|
||||
/**
|
||||
* Parses id3v2.4.0 tag body.
|
||||
*
|
||||
* @param $fp
|
||||
* @param $lastByte
|
||||
*/
|
||||
protected function parseId3v24Body($lastByte)
|
||||
protected function parseId3v24Body($fp, $lastByte)
|
||||
{
|
||||
while ($this->fileObj->getFilePos() < $lastByte) {
|
||||
$frame_id = $this->fileObj->getBytes(4);
|
||||
while (ftell($fp) < $lastByte) {
|
||||
$raw = fread($fp, 10);
|
||||
$frame_id = substr($raw, 0, 4);
|
||||
|
||||
if ($frame_id == str_repeat(chr(0), 4)) {
|
||||
$this->fileObj->seekTo($lastByte);
|
||||
fseek($fp, $lastByte);
|
||||
break;
|
||||
}
|
||||
|
||||
$frame_size = $this->getSynchsafeSize($this->fileObj->getBytes(4));
|
||||
|
||||
$data = unpack('H2flags', $this->fileObj->getBytes(2));
|
||||
$data = unpack('Nframe_size/H2flags', substr($raw, 4));
|
||||
$frame_size = $data['frame_size'];
|
||||
$flags = base_convert($data['flags'], 16, 2);
|
||||
$this->id3v2TagsFlags[$frame_id] = array(
|
||||
'payload_size' => $frame_size,
|
||||
'flags' => array(
|
||||
'tag_alter_preservation' => (bool)substr($flags, 1, 1),
|
||||
'file_alter_preservation' => (bool)substr($flags, 2, 1),
|
||||
@ -832,6 +806,7 @@ class Mp3Info
|
||||
case 'TALB': # Album/Movie/Show title
|
||||
case 'TCON': # Content type
|
||||
case 'TYER': # Year
|
||||
case 'TXXX': # User defined text information frame
|
||||
case 'TRCK': # Track number/Position in set
|
||||
case 'TIT2': # Title/songname/content description
|
||||
case 'TPE1': # Lead performer(s)/Soloist(s)
|
||||
@ -839,7 +814,6 @@ class Mp3Info
|
||||
case 'TCOM': # Composer
|
||||
case 'TCOP': # Copyright message
|
||||
case 'TDAT': # Date
|
||||
case 'TDRC': # Recording time
|
||||
case 'TDLY': # Playlist delay
|
||||
case 'TENC': # Encoded by
|
||||
case 'TEXT': # Lyricist/Text writer
|
||||
@ -868,25 +842,7 @@ class Mp3Info
|
||||
case 'TSIZ': # Size
|
||||
case 'TSRC': # ISRC (international standard recording code)
|
||||
case 'TSSE': # Software/Hardware and settings used for encoding
|
||||
$this->tags2[$frame_id] = $this->handleTextFrame($frame_size, $this->fileObj->getBytes($frame_size));
|
||||
break;
|
||||
|
||||
case 'TXXX': # User defined text information frame
|
||||
$dataEnd = $this->fileObj->getFilePos() + $frame_size;
|
||||
$encoding = ord($this->fileObj->getBytes(1));
|
||||
$description_raw = $this->readTextUntilNull($dataEnd);
|
||||
$description = $this->_getUtf8Text($encoding, $description_raw);
|
||||
$value = $this->fileObj->getBytes($dataEnd - $this->fileObj->getFilePos());
|
||||
$tagName = $frame_id . ':' . $description;
|
||||
if (key_exists($tagName, $this->tags2)) {
|
||||
// this should never happen! TXXX-description must be unique.
|
||||
if (!is_array($this->tags2[$tagName])) {
|
||||
$this->tags2[$tagName] = array($this->tags2[$tagName]);
|
||||
}
|
||||
$this->tags2[$tagName][] = $value;
|
||||
} else {
|
||||
$this->tags2[$tagName] = $value;
|
||||
}
|
||||
$this->tags2[$frame_id] = $this->handleTextFrame($frame_size, fread($fp, $frame_size));
|
||||
break;
|
||||
|
||||
################# Text information frames
|
||||
@ -927,17 +883,34 @@ class Mp3Info
|
||||
// case 'SYLT': # Synchronized lyric/text
|
||||
// break;
|
||||
case 'COMM': # Comments
|
||||
$dataEnd = $this->fileObj->getFilePos() + $frame_size;
|
||||
$encoding = unpack('C', $this->fileObj->getBytes(1))[1];
|
||||
$language = $this->fileObj->getBytes(3);
|
||||
$allText_raw = $this->fileObj->getBytes($dataEnd - $this->fileObj->getFilePos());
|
||||
$allText = $this->_getUtf8Text($encoding, $allText_raw);
|
||||
$dataEnd = ftell($fp) + $frame_size;
|
||||
$raw = fread($fp, 4);
|
||||
$data = unpack('C1encoding/A3language', $raw);
|
||||
// read until \null character
|
||||
$short_description = null;
|
||||
$last_null = false;
|
||||
$actual_text = false;
|
||||
while (ftell($fp) < $dataEnd) {
|
||||
$char = fgetc($fp);
|
||||
if ($char == "\00" && $actual_text === false) {
|
||||
if ($data['encoding'] == 0x1) { # two null-bytes for utf-16
|
||||
if ($last_null)
|
||||
$actual_text = null;
|
||||
else
|
||||
$last_null = true;
|
||||
} else # no condition for iso-8859-1
|
||||
$actual_text = null;
|
||||
|
||||
list($short_description, $actual_text) = explode("\0", $allText, 2);
|
||||
|
||||
$this->tags2[$frame_id][$language] = array(
|
||||
'short' => $short_description,
|
||||
'actual' => $actual_text,
|
||||
}
|
||||
else if ($actual_text !== false) $actual_text .= $char;
|
||||
else $short_description .= $char;
|
||||
}
|
||||
if ($actual_text === false) $actual_text = $short_description;
|
||||
// list($short_description, $actual_text) = sscanf("s".chr(0)."s", $data['texts']);
|
||||
// list($short_description, $actual_text) = explode(chr(0), $data['texts']);
|
||||
$this->tags2[$frame_id][$data['language']] = array(
|
||||
'short' => (bool)($data['encoding'] == 0x00) ? mb_convert_encoding($short_description, 'utf-8', 'iso-8859-1') : mb_convert_encoding($short_description, 'utf-8', 'utf-16'),
|
||||
'actual' => (bool)($data['encoding'] == 0x00) ? mb_convert_encoding($actual_text, 'utf-8', 'iso-8859-1') : mb_convert_encoding($actual_text, 'utf-8', 'utf-16'),
|
||||
);
|
||||
break;
|
||||
// case 'RVAD': # Relative volume adjustment
|
||||
@ -948,20 +921,20 @@ class Mp3Info
|
||||
// break;
|
||||
case 'APIC': # Attached picture
|
||||
$this->hasCover = true;
|
||||
$dataEnd = $this->fileObj->getFilePos() + $frame_size;
|
||||
$this->coverProperties = ['text_encoding' => ord($this->fileObj->getBytes(1))];
|
||||
// $this->fileObj->seekForward($frame_size - 4);
|
||||
$this->coverProperties['mime_type'] = $this->readTextUntilNull($dataEnd);
|
||||
$this->coverProperties['picture_type'] = ord($this->fileObj->getBytes(1));
|
||||
$this->coverProperties['description'] = $this->readTextUntilNull($dataEnd);
|
||||
$this->coverProperties['offset'] = $this->fileObj->getFilePos();
|
||||
$this->coverProperties['size'] = $dataEnd - $this->fileObj->getFilePos();
|
||||
$this->fileObj->seekTo($dataEnd);
|
||||
$last_byte = ftell($fp) + $frame_size;
|
||||
$this->coverProperties = ['text_encoding' => ord(fread($fp, 1))];
|
||||
// fseek($fp, $frame_size - 4, SEEK_CUR);
|
||||
$this->coverProperties['mime_type'] = $this->readTextUntilNull($fp, $last_byte);
|
||||
$this->coverProperties['picture_type'] = ord(fread($fp, 1));
|
||||
$this->coverProperties['description'] = $this->readTextUntilNull($fp, $last_byte);
|
||||
$this->coverProperties['offset'] = ftell($fp);
|
||||
$this->coverProperties['size'] = $last_byte - ftell($fp);
|
||||
fseek($fp, $last_byte);
|
||||
break;
|
||||
// case 'GEOB': # General encapsulated object
|
||||
// break;
|
||||
case 'PCNT': # Play counter
|
||||
$data = unpack('L', $this->fileObj->getBytes($frame_size));
|
||||
$data = unpack('L', fread($fp, $frame_size));
|
||||
$this->tags2[$frame_id] = $data[1];
|
||||
break;
|
||||
// case 'POPM': # Popularimeter
|
||||
@ -987,44 +960,12 @@ class Mp3Info
|
||||
// case 'PRIV': # Private frame
|
||||
// break;
|
||||
default:
|
||||
$this->fileObj->seekForward($frame_size);
|
||||
fseek($fp, $frame_size, SEEK_CUR);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts text encoding according to ID3 indicator
|
||||
*
|
||||
* @param int $encoding Encoding ID from ID3 frame
|
||||
* @param string $rawText Raw text from ID3 frame
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function _getUtf8Text(int $encoding, ?string $rawText): string
|
||||
{
|
||||
if (is_null($rawText)) {
|
||||
$rawText = '';
|
||||
}
|
||||
|
||||
switch ($encoding) {
|
||||
case 0x00: // ISO-8859-1
|
||||
return mb_convert_encoding($rawText, 'utf-8', 'iso-8859-1');
|
||||
|
||||
case 0x01: // UTF-16 with BOM
|
||||
return mb_convert_encoding($rawText . "\00", 'utf-8', 'utf-16');
|
||||
|
||||
// Following is for id3v2.4.x only
|
||||
case 0x02: // UTF-16 without BOM
|
||||
return mb_convert_encoding($rawText . "\00", 'utf-8', 'utf-16');
|
||||
case 0x03: // UTF-8
|
||||
return $rawText;
|
||||
|
||||
default:
|
||||
throw new RuntimeException('Unknown text encoding type: ' . $encoding);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $frameSize
|
||||
* @param $raw
|
||||
@ -1034,7 +975,22 @@ class Mp3Info
|
||||
private function handleTextFrame($frameSize, $raw)
|
||||
{
|
||||
$data = unpack('C1encoding/A' . ($frameSize - 1) . 'information', $raw);
|
||||
return $this->_getUtf8Text($data['encoding'], $data['information']);
|
||||
|
||||
switch($data['encoding']) {
|
||||
case 0x00: # ISO-8859-1
|
||||
return mb_convert_encoding($data['information'], 'utf-8', 'iso-8859-1');
|
||||
case 0x01: # utf-16 with BOM
|
||||
return mb_convert_encoding($data['information'] . "\00", 'utf-8', 'utf-16');
|
||||
|
||||
# Following is for id3v2.4.x only
|
||||
case 0x02: # utf-16 without BOM
|
||||
return mb_convert_encoding($data['information'] . "\00", 'utf-8', 'utf-16');
|
||||
case 0x03: # utf-8
|
||||
return $data['information'];
|
||||
|
||||
default:
|
||||
throw new RuntimeException('Unknown text encoding type: '.$data['encoding']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1042,11 +998,11 @@ class Mp3Info
|
||||
* @param int $dataEnd
|
||||
* @return string|null
|
||||
*/
|
||||
private function readTextUntilNull($dataEnd)
|
||||
private function readTextUntilNull($fp, $dataEnd)
|
||||
{
|
||||
$text = null;
|
||||
while ($this->fileObj->getFilePos() < $dataEnd) {
|
||||
$char = $this->fileObj->getBytes(1);
|
||||
while (ftell($fp) < $dataEnd) {
|
||||
$char = fgetc($fp);
|
||||
if ($char === "\00") {
|
||||
return $text;
|
||||
}
|
||||
@ -1088,35 +1044,38 @@ class Mp3Info
|
||||
* @return boolean True if file looks that correct mpeg audio, False otherwise.
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function isValidAudio($filename)
|
||||
{
|
||||
if (str_contains($filename, '://')) {
|
||||
$fileObj = new Mp3FileRemote($filename);
|
||||
} else {
|
||||
if (!file_exists($filename)) {
|
||||
throw new Exception('File ' . $filename . ' is not present!');
|
||||
}
|
||||
$fileObj = new Mp3FileLocal($filename);
|
||||
public static function isValidAudio($filename) {
|
||||
if (!file_exists($filename) && strpos($filename, '://') == false) {
|
||||
throw new Exception('File ' . $filename . ' is not present!');
|
||||
}
|
||||
|
||||
$filesize = $fileObj->getFileSize();
|
||||
$filesize = file_exists($filename) ? filesize($filename) : static::getUrlContentLength($filename);
|
||||
|
||||
$raw = $fileObj->getBytes(3);
|
||||
if ($raw === self::TAG2_SYNC) {
|
||||
// id3v2 tag
|
||||
return true;
|
||||
}
|
||||
if ((unpack('n', $raw)[1] & self::FRAME_SYNC) === self::FRAME_SYNC) {
|
||||
// mpeg header tag
|
||||
return true;
|
||||
}
|
||||
if ($filesize > 128) {
|
||||
$fileObj->seekTo($filesize - 128);
|
||||
if ($fileObj->getBytes(3) === self::TAG1_SYNC) {
|
||||
// id3v1 tag
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
$raw = file_get_contents($filename, false, null, 0, 3);
|
||||
return $raw === self::TAG2_SYNC // id3v2 tag
|
||||
|| (self::FRAME_SYNC === (unpack('n*', $raw)[1] & self::FRAME_SYNC)) // mpeg header tag
|
||||
|| (
|
||||
$filesize > 128
|
||||
&& file_get_contents($filename, false, null, -128, 3) === self::TAG1_SYNC
|
||||
) // id3v1 tag
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return int|mixed|string
|
||||
*/
|
||||
public static function getUrlContentLength($url) {
|
||||
$context = stream_context_create(['http' => ['method' => 'HEAD']]);
|
||||
$head = array_change_key_case(get_headers($url, true, $context));
|
||||
// content-length of download (in bytes), read from Content-Length: field
|
||||
$clen = isset($head['content-length']) ? $head['content-length'] : 0;
|
||||
|
||||
// cannot retrieve file size, return "-1"
|
||||
if (!$clen) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return $clen; // return size in bytes
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user