Fix incorrect VBR-file duration (depending on quality) and add Cover extraction

This commit is contained in:
wapmorgan 2021-08-11 21:21:54 +03:00
parent 5f7d54fa69
commit ee86090f0a
2 changed files with 174 additions and 112 deletions

16
bin/mp3scan Normal file → Executable file
View File

@ -162,7 +162,21 @@ class Mp3InfoConsoleRunner {
} }
if ($verbose) { if ($verbose) {
print_r(get_object_vars($audio)); print_r(array_intersect_key(get_object_vars($audio), array_flip([
'codecVersion',
'layerVersion',
'duration',
'bitRate',
'sampleRate',
'isVbr',
'hasCover',
'channel',
'tags',
'tags1',
'tags2',
'id3v2MajorVersion',
'id3v2MinorVersion',
])));
} }
$this->totalDuration += $audio->duration; $this->totalDuration += $audio->duration;

View File

@ -78,51 +78,55 @@ class Mp3Info {
public static $headerSeekLimit = 2048; public static $headerSeekLimit = 2048;
/** /**
* MPEG codec version (1 or 2 or 2.5 or undefined) * @var int MPEG codec version (1 or 2 or 2.5 or undefined)
* @var int
*/ */
public $codecVersion; public $codecVersion;
/** /**
* Audio layer version (1 or 2 or 3) * @var int Audio layer version (1 or 2 or 3)
* @var int
*/ */
public $layerVersion; public $layerVersion;
/** /**
* Audio size in bytes. Note that this value is NOT equals file size. * @var int Audio size in bytes. Note that this value is NOT equals file size.
* @var int
*/ */
public $audioSize; public $audioSize;
/** /**
* Audio duration in seconds.microseconds (e.g. 3603.0171428571) * @var float Audio duration in seconds.microseconds (e.g. 3603.0171428571)
* @var float
*/ */
public $duration; public $duration;
/** /**
* Audio bit rate in bps (e.g. 128000) * @var int Audio bit rate in bps (e.g. 128000)
*/ */
public $bitRate; public $bitRate;
/** /**
* Audio sample rate in Hz (e.g. 44100) * @var int Audio sample rate in Hz (e.g. 44100)
* @var int
*/ */
public $sampleRate; public $sampleRate;
/** /**
* Contains true if audio has variable bit rate * @var boolean Contains true if audio has variable bit rate
* @var boolean
*/ */
public $isVbr = false; public $isVbr = false;
/** /**
* Contains VBR properties * @var boolean Contains true if audio has cover
* @var array */
public $hasCover = false;
/**
* @var array Contains VBR properties
*/ */
public $vbrProperties = []; public $vbrProperties = [];
/**
* @var array Contains picture properties
*/
public $coverProperties = [];
/** /**
* Channel mode (stereo or dual_mono or joint_stereo or mono) * Channel mode (stereo or dual_mono or joint_stereo or mono)
* @var string * @var string
@ -135,67 +139,57 @@ class Mp3Info {
public $tags = []; public $tags = [];
/** /**
* Audio tags ver. 1 (aka id3v1) * @var array Audio tags ver. 1 (aka id3v1)
* @var array
*/ */
public $tags1 = []; public $tags1 = [];
/** /**
* Audio tags ver. 2 (aka id3v2) * @var array Audio tags ver. 2 (aka id3v2)
* @var array
*/ */
public $tags2 = []; public $tags2 = [];
/** /**
* Major version of id3v2 tag (if id3v2 present) (2 or 3 or 4) * @var int Major version of id3v2 tag (if id3v2 present) (2 or 3 or 4)
* @var int
*/ */
public $id3v2MajorVersion; public $id3v2MajorVersion;
/** /**
* Minor version of id3v2 tag (if id3v2 present) * @var int Minor version of id3v2 tag (if id3v2 present)
* @var int
*/ */
public $id3v2MinorVersion; public $id3v2MinorVersion;
/** /**
* List of id3v2 header flags (if id3v2 present) * @var array List of id3v2 header flags (if id3v2 present)
* @var array
*/ */
public $id3v2Flags = []; public $id3v2Flags = [];
/** /**
* List of id3v2 tags flags (if id3v2 present) * @var array List of id3v2 tags flags (if id3v2 present)
* @var array
*/ */
public $id3v2TagsFlags = []; public $id3v2TagsFlags = [];
/** /**
* Contains audio file name * @var string Contains audio file name
* @var string
*/ */
public $_fileName; public $_fileName;
/** /**
* Contains file size * @var int Contains file size
* @var int
*/ */
public $_fileSize; public $_fileSize;
/** /**
* Number of audio frames in file * @var int Number of audio frames in file
* @var int
*/ */
public $_framesCount = 0; public $_framesCount = 0;
/** /**
* Contains time spent to read&extract audio information. * @var float Contains time spent to read&extract audio information.
* @var float
*/ */
public $_parsingTime; public $_parsingTime;
/** /**
* Calculated frame size for Constant Bit Rate * @var int Calculated frame size for Constant Bit Rate
* @var int
*/ */
private $_cbrFrameSize; private $_cbrFrameSize;
@ -225,6 +219,22 @@ class Mp3Info {
$this->audioSize = $this->parseAudio($this->_fileName = $filename, $this->_fileSize = filesize($filename), $mode); $this->audioSize = $this->parseAudio($this->_fileName = $filename, $this->_fileSize = filesize($filename), $mode);
} }
/**
* @return bool|null
*/
public function getCover()
{
if (empty($this->coverProperties)) {
return null;
}
$fp = fopen($this->_fileName, 'rb');
fseek($fp, $this->coverProperties['offset']);
$data = fread($fp, $this->coverProperties['size']);
fclose($fp);
return $data;
}
/** /**
* Reads audio file in binary mode. * Reads audio file in binary mode.
* mpeg audio file structure: * mpeg audio file structure:
@ -274,7 +284,7 @@ class Mp3Info {
if ($mode & self::META) { if ($mode & self::META) {
if ($this->_id3Size !== null) fseek($fp, $this->_id3Size); if ($this->_id3Size !== null) fseek($fp, $this->_id3Size);
/** /**
* First frame can lie. Need to fix in future. * First frame can lie. Need to fix in the future.
* @link https://github.com/wapmorgan/Mp3Info/issues/13#issuecomment-447470813 * @link https://github.com/wapmorgan/Mp3Info/issues/13#issuecomment-447470813
*/ */
$framesCount = $this->readMpegFrame($fp); $framesCount = $this->readMpegFrame($fp);
@ -292,9 +302,10 @@ class Mp3Info {
// The faster way to detect audio duration: // The faster way to detect audio duration:
$samples_in_second = $this->layerVersion == 1 ? self::LAYER_1_FRAME_SIZE : self::LAYERS_23_FRAME_SIZE; $samples_in_second = $this->layerVersion == 1 ? self::LAYER_1_FRAME_SIZE : self::LAYERS_23_FRAME_SIZE;
// for VBR: adjust samples in second according to VBR quality // for VBR: adjust samples in second according to VBR quality
if ($this->isVbr && isset($this->vbrProperties['quality'])) { // disabled for now
$samples_in_second = floor($samples_in_second * $this->vbrProperties['quality'] / 100); // if ($this->isVbr && isset($this->vbrProperties['quality'])) {
} // $samples_in_second = floor($samples_in_second * $this->vbrProperties['quality'] / 100);
// }
// Calculate total number of audio samples (framesCount * sampleInFrameCount) / samplesInSecondCount // Calculate total number of audio samples (framesCount * sampleInFrameCount) / samplesInSecondCount
$this->duration = ($this->_framesCount - 1) * $samples_in_second / $this->sampleRate; $this->duration = ($this->_framesCount - 1) * $samples_in_second / $this->sampleRate;
} }
@ -535,7 +546,7 @@ class Mp3Info {
* Parses id3v2.3.0 tag body. * Parses id3v2.3.0 tag body.
* @todo Complete. * @todo Complete.
*/ */
private function parseId3v23Body($fp, $lastByte) { protected function parseId3v23Body($fp, $lastByte) {
while (ftell($fp) < $lastByte) { while (ftell($fp) < $lastByte) {
$raw = fread($fp, 10); $raw = fread($fp, 10);
$frame_id = substr($raw, 0, 4); $frame_id = substr($raw, 0, 4);
@ -571,41 +582,40 @@ class Mp3Info {
case 'TRCK': # Track number/Position in set case 'TRCK': # Track number/Position in set
case 'TIT2': # Title/songname/content description case 'TIT2': # Title/songname/content description
case 'TPE1': # Lead performer(s)/Soloist(s) case 'TPE1': # Lead performer(s)/Soloist(s)
case 'TBPM': # BPM (beats per minute)
case 'TCOM': # Composer
case 'TCOP': # Copyright message
case 'TDAT': # Date
case 'TDLY': # Playlist delay
case 'TENC': # Encoded by
case 'TEXT': # Lyricist/Text writer
case 'TFLT': # File type
case 'TIME': # Time
case 'TIT1': # Content group description
case 'TIT3': # Subtitle/Description refinement
case 'TKEY': # Initial key
case 'TLAN': # Language(s)
case 'TLEN': # Length
case 'TMED': # Media type
case 'TOAL': # Original album/movie/show title
case 'TOFN': # Original filename
case 'TOLY': # Original lyricist(s)/text writer(s)
case 'TOPE': # Original artist(s)/performer(s)
case 'TORY': # Original release year
case 'TOWN': # File owner/licensee
case 'TPE2': # Band/orchestra/accompaniment
case 'TPE3': # Conductor/performer refinement
case 'TPE4': # Interpreted, remixed, or otherwise modified by
case 'TPOS': # Part of a set
case 'TPUB': # Publisher
case 'TRDA': # Recording dates
case 'TRSN': # Internet radio station name
case 'TRSO': # Internet radio station owner
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, fread($fp, $frame_size)); $this->tags2[$frame_id] = $this->handleTextFrame($frame_size, fread($fp, $frame_size));
break; break;
// case 'TBPM': # BPM (beats per minute)
// case 'TCOM': # Composer
// case 'TCOP': # Copyright message
// case 'TDAT': # Date
// case 'TDLY': # Playlist delay
// case 'TENC': # Encoded by
// case 'TEXT': # Lyricist/Text writer
// case 'TFLT': # File type
// case 'TIME': # Time
// case 'TIT1': # Content group description
// case 'TIT3': # Subtitle/Description refinement
// case 'TKEY': # Initial key
// case 'TLAN': # Language(s)
// case 'TLEN': # Length
// case 'TMED': # Media type
// case 'TOAL': # Original album/movie/show title
// case 'TOFN': # Original filename
// case 'TOLY': # Original lyricist(s)/text writer(s)
// case 'TOPE': # Original artist(s)/performer(s)
// case 'TORY': # Original release year
// case 'TOWN': # File owner/licensee
// case 'TPE2': # Band/orchestra/accompaniment
// case 'TPE3': # Conductor/performer refinement
// case 'TPE4': # Interpreted, remixed, or otherwise modified by
// case 'TPOS': # Part of a set
// case 'TPUB': # Publisher
// case 'TRDA': # Recording dates
// case 'TRSN': # Internet radio station name
// case 'TRSO': # Internet radio station owner
// case 'TSIZ': # Size
// case 'TSRC': # ISRC (international standard recording code)
// case 'TSSE': # Software/Hardware and settings used for encoding
################# Text information frames ################# Text information frames
################# URL link frames ################# URL link frames
@ -680,8 +690,18 @@ class Mp3Info {
// break; // break;
// case 'RVRB': # Reverb // case 'RVRB': # Reverb
// break; // break;
// case 'APIC': # Attached picture case 'APIC': # Attached picture
// break; $this->hasCover = true;
$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 // case 'GEOB': # General encapsulated object
// break; // break;
case 'PCNT': # Play counter case 'PCNT': # Play counter
@ -761,40 +781,40 @@ class Mp3Info {
case 'TRCK': # Track number/Position in set case 'TRCK': # Track number/Position in set
case 'TIT2': # Title/songname/content description case 'TIT2': # Title/songname/content description
case 'TPE1': # Lead performer(s)/Soloist(s) case 'TPE1': # Lead performer(s)/Soloist(s)
case 'TBPM': # BPM (beats per minute)
case 'TCOM': # Composer
case 'TCOP': # Copyright message
case 'TDAT': # Date
case 'TDLY': # Playlist delay
case 'TENC': # Encoded by
case 'TEXT': # Lyricist/Text writer
case 'TFLT': # File type
case 'TIME': # Time
case 'TIT1': # Content group description
case 'TIT3': # Subtitle/Description refinement
case 'TKEY': # Initial key
case 'TLAN': # Language(s)
case 'TLEN': # Length
case 'TMED': # Media type
case 'TOAL': # Original album/movie/show title
case 'TOFN': # Original filename
case 'TOLY': # Original lyricist(s)/text writer(s)
case 'TOPE': # Original artist(s)/performer(s)
case 'TORY': # Original release year
case 'TOWN': # File owner/licensee
case 'TPE2': # Band/orchestra/accompaniment
case 'TPE3': # Conductor/performer refinement
case 'TPE4': # Interpreted, remixed, or otherwise modified by
case 'TPOS': # Part of a set
case 'TPUB': # Publisher
case 'TRDA': # Recording dates
case 'TRSN': # Internet radio station name
case 'TRSO': # Internet radio station owner
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, fread($fp, $frame_size)); $this->tags2[$frame_id] = $this->handleTextFrame($frame_size, fread($fp, $frame_size));
break; break;
// case 'TBPM': # BPM (beats per minute)
// case 'TCOM': # Composer
// case 'TCOP': # Copyright message
// case 'TDAT': # Date
// case 'TDLY': # Playlist delay
// case 'TENC': # Encoded by
// case 'TEXT': # Lyricist/Text writer
// case 'TFLT': # File type
// case 'TIME': # Time
// case 'TIT1': # Content group description
// case 'TIT3': # Subtitle/Description refinement
// case 'TKEY': # Initial key
// case 'TLAN': # Language(s)
// case 'TLEN': # Length
// case 'TMED': # Media type
// case 'TOAL': # Original album/movie/show title
// case 'TOFN': # Original filename
// case 'TOLY': # Original lyricist(s)/text writer(s)
// case 'TOPE': # Original artist(s)/performer(s)
// case 'TORY': # Original release year
// case 'TOWN': # File owner/licensee
// case 'TPE2': # Band/orchestra/accompaniment
// case 'TPE3': # Conductor/performer refinement
// case 'TPE4': # Interpreted, remixed, or otherwise modified by
// case 'TPOS': # Part of a set
// case 'TPUB': # Publisher
// case 'TRDA': # Recording dates
// case 'TRSN': # Internet radio station name
// case 'TRSO': # Internet radio station owner
// case 'TSIZ': # Size
// case 'TSRC': # ISRC (international standard recording code)
// case 'TSSE': # Software/Hardware and settings used for encoding
################# Text information frames ################# Text information frames
@ -870,8 +890,18 @@ class Mp3Info {
// break; // break;
// case 'RVRB': # Reverb // case 'RVRB': # Reverb
// break; // break;
// case 'APIC': # Attached picture case 'APIC': # Attached picture
// break; $this->hasCover = true;
$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 // case 'GEOB': # General encapsulated object
// break; // break;
case 'PCNT': # Play counter case 'PCNT': # Play counter
@ -958,6 +988,24 @@ class Mp3Info {
} }
} }
/**
* @param resource $fp
* @param int $dataEnd
* @return string|null
*/
private function readTextUntilNull($fp, $dataEnd)
{
$text = null;
while (ftell($fp) < $dataEnd) {
$char = fgetc($fp);
if ($char === "\00") {
return $text;
}
$text .= $char;
}
return $text;
}
/** /**
* Fills `tags` property with values id3v2 and id3v1 tags. * Fills `tags` property with values id3v2 and id3v1 tags.
*/ */