Add functions for generating metadata for video and audio, using the
ID3 library. Also allows themes/plugins to add thumbnail support to these media types. Think stuff like album art, movie covers, and video freeze-frames. props wonderboymusic. fixes #23673 git-svn-id: http://core.svn.wordpress.org/trunk@23766 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
parent
d6fee50fd3
commit
12d3fc7999
|
@ -141,8 +141,15 @@ if ( post_type_supports( $post_type, 'post-formats' ) ) {
|
|||
if ( post_type_supports($post_type, 'page-attributes') )
|
||||
add_meta_box('pageparentdiv', 'page' == $post_type ? __('Page Attributes') : __('Attributes'), 'page_attributes_meta_box', null, 'side', 'core');
|
||||
|
||||
if ( current_theme_supports( 'post-thumbnails', $post_type ) && post_type_supports( $post_type, 'thumbnail' ) )
|
||||
add_meta_box('postimagediv', __('Featured Image'), 'post_thumbnail_meta_box', null, 'side', 'low');
|
||||
$audio_post_support = $video_post_support = false;
|
||||
$theme_support = current_theme_supports( 'post-thumbnails', $post_type ) && post_type_supports( $post_type, 'thumbnail' );
|
||||
if ( 'attachment' === $post_type && ! empty( $post->post_mime_type ) ) {
|
||||
$audio_post_support = 0 === strpos( $post->post_mime_type, 'audio/' ) && current_theme_supports( 'post-thumbnails', 'attachment:audio' ) && post_type_supports( 'attachment:audio', 'thumbnail' );
|
||||
$video_post_support = 0 === strpos( $post->post_mime_type, 'video/' ) && current_theme_supports( 'post-thumbnails', 'attachment:video' ) && post_type_supports( 'attachment:video', 'thumbnail' );
|
||||
}
|
||||
|
||||
if ( $theme_support || $audio_post_support || $video_post_support )
|
||||
add_meta_box('postimagediv', __('Featured Image'), 'post_thumbnail_meta_box', null, 'side', 'low');
|
||||
|
||||
if ( post_type_supports($post_type, 'excerpt') )
|
||||
add_meta_box('postexcerpt', __('Excerpt'), 'post_excerpt_meta_box', null, 'normal', 'core');
|
||||
|
|
|
@ -73,6 +73,7 @@ function wp_generate_attachment_metadata( $attachment_id, $file ) {
|
|||
$attachment = get_post( $attachment_id );
|
||||
|
||||
$metadata = array();
|
||||
$support = false;
|
||||
if ( preg_match('!^image/!', get_post_mime_type( $attachment )) && file_is_displayable_image($file) ) {
|
||||
$imagesize = getimagesize( $file );
|
||||
$metadata['width'] = $imagesize[0];
|
||||
|
@ -117,8 +118,41 @@ function wp_generate_attachment_metadata( $attachment_id, $file ) {
|
|||
if ( $image_meta )
|
||||
$metadata['image_meta'] = $image_meta;
|
||||
|
||||
} elseif ( preg_match( '#^video/#', get_post_mime_type( $attachment ) ) ) {
|
||||
$metadata = wp_read_video_metadata( $file );
|
||||
$support = current_theme_supports( 'post-thumbnails', 'attachment:video' ) && post_type_supports( 'attachment:video', 'thumbnail' );
|
||||
} elseif ( preg_match( '#^audio/#', get_post_mime_type( $attachment ) ) ) {
|
||||
$metadata = wp_read_audio_metadata( $file );
|
||||
$support = current_theme_supports( 'post-thumbnails', 'attachment:audio' ) && post_type_supports( 'attachment:audio', 'thumbnail' );
|
||||
}
|
||||
|
||||
if ( $support && ! empty( $metadata['image']['data'] ) ) {
|
||||
$ext = '.jpg';
|
||||
switch ( $metadata['image']['mime'] ) {
|
||||
case 'image/gif':
|
||||
$ext = '.gif';
|
||||
break;
|
||||
case 'image/png':
|
||||
$ext = '.png';
|
||||
break;
|
||||
}
|
||||
$basename = str_replace( '.', '-', basename( $file ) ) . '-image' . $ext;
|
||||
$uploaded = wp_upload_bits( $basename, '', $metadata['image']['data'] );
|
||||
if ( false === $uploaded['error'] ) {
|
||||
$attachment = array(
|
||||
'post_mime_type' => $metadata['image']['mime'],
|
||||
'post_type' => 'attachment',
|
||||
'post_content' => '',
|
||||
);
|
||||
$sub_attachment_id = wp_insert_attachment( $attachment, $uploaded['file'] );
|
||||
$attach_data = wp_generate_attachment_metadata( $sub_attachment_id, $uploaded['file'] );
|
||||
wp_update_attachment_metadata( $sub_attachment_id, $attach_data );
|
||||
update_post_meta( $attachment_id, '_thumbnail_id', $sub_attachment_id );
|
||||
}
|
||||
}
|
||||
// remove the blob of binary data from the array
|
||||
unset( $metadata['image']['data'] );
|
||||
|
||||
return apply_filters( 'wp_generate_attachment_metadata', $metadata, $attachment_id );
|
||||
}
|
||||
|
||||
|
|
|
@ -2397,3 +2397,140 @@ add_filter( 'media_upload_gallery', 'media_upload_gallery' );
|
|||
add_filter( 'media_upload_library', 'media_upload_library' );
|
||||
|
||||
add_action( 'attachment_submitbox_misc_actions', 'attachment_submitbox_metadata' );
|
||||
|
||||
/**
|
||||
* Parse ID3v2, ID3v1, and getID3 comments to extract usable data
|
||||
*
|
||||
* @since 3.6.0
|
||||
*
|
||||
* @param array $metadata An existing array with data
|
||||
* @param array $data Data supplied by ID3 tags
|
||||
*/
|
||||
function wp_add_id3_tag_data( &$metadata, $data ) {
|
||||
foreach ( array( 'id3v2', 'id3v1' ) as $version ) {
|
||||
if ( ! empty( $data[$version]['comments'] ) ) {
|
||||
foreach ( $data[$version]['comments'] as $key => $list ) {
|
||||
if ( ! empty( $list ) ) {
|
||||
$metadata[$key] = reset( $list );
|
||||
// fix bug in byte stream analysis
|
||||
if ( 'terms_of_use' === $key && 0 === strpos( $metadata[$key], 'yright notice.' ) )
|
||||
$metadata[$key] = 'Cop' . $metadata[$key];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $data['id3v2']['APIC'] ) ) {
|
||||
$image = reset( $data['id3v2']['APIC']);
|
||||
if ( ! empty( $image['data'] ) ) {
|
||||
$metadata['image'] = array(
|
||||
'data' => $image['data'],
|
||||
'mime' => $image['image_mime'],
|
||||
'width' => $image['image_width'],
|
||||
'height' => $image['image_height']
|
||||
);
|
||||
}
|
||||
} elseif ( ! empty( $data['comments']['picture'] ) ) {
|
||||
$image = reset( $data['comments']['picture'] );
|
||||
if ( ! empty( $image['data'] ) ) {
|
||||
$metadata['image'] = array(
|
||||
'data' => $image['data'],
|
||||
'mime' => $image['image_mime']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve metadata from a video file's ID3 tags
|
||||
*
|
||||
* @since 3.6.0
|
||||
*
|
||||
* @param string $file Path to file.
|
||||
* @return array|boolean Returns array of metadata, if found.
|
||||
*/
|
||||
function wp_read_video_metadata( $file ) {
|
||||
if ( ! file_exists( $file ) )
|
||||
return false;
|
||||
|
||||
$metadata = array();
|
||||
|
||||
if ( ! class_exists( 'getID3' ) )
|
||||
require( ABSPATH . WPINC . '/ID3/class-getid3.php' );
|
||||
$id3 = new getID3();
|
||||
$data = $id3->analyze( $file );
|
||||
|
||||
if ( isset( $data['video']['lossless'] ) )
|
||||
$metadata['lossless'] = $data['video']['lossless'];
|
||||
if ( ! empty( $data['video']['bitrate'] ) )
|
||||
$metadata['bitrate'] = (int) $data['video']['bitrate'];
|
||||
if ( ! empty( $data['video']['bitrate_mode'] ) )
|
||||
$metadata['bitrate_mode'] = $data['video']['bitrate_mode'];
|
||||
if ( ! empty( $data['filesize'] ) )
|
||||
$metadata['filesize'] = (int) $data['filesize'];
|
||||
if ( ! empty( $data['mime_type'] ) )
|
||||
$metadata['mime_type'] = $data['mime_type'];
|
||||
if ( ! empty( $data['playtime_seconds'] ) )
|
||||
$metadata['length'] = (int) ceil( $data['playtime_seconds'] );
|
||||
if ( ! empty( $data['playtime_string'] ) )
|
||||
$metadata['length_formatted'] = $data['playtime_string'];
|
||||
if ( ! empty( $data['video']['resolution_x'] ) )
|
||||
$metadata['width'] = (int) $data['video']['resolution_x'];
|
||||
if ( ! empty( $data['video']['resolution_y'] ) )
|
||||
$metadata['height'] = (int) $data['video']['resolution_y'];
|
||||
if ( ! empty( $data['fileformat'] ) )
|
||||
$metadata['fileformat'] = $data['fileformat'];
|
||||
if ( ! empty( $data['video']['dataformat'] ) )
|
||||
$metadata['dataformat'] = $data['video']['dataformat'];
|
||||
if ( ! empty( $data['video']['encoder'] ) )
|
||||
$metadata['encoder'] = $data['video']['encoder'];
|
||||
if ( ! empty( $data['video']['codec'] ) )
|
||||
$metadata['codec'] = $data['video']['codec'];
|
||||
|
||||
unset( $data['audio']['streams'] );
|
||||
$metadata['audio'] = $data['audio'];
|
||||
|
||||
wp_add_id3_tag_data( $metadata, $data );
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve metadata from a audio file's ID3 tags
|
||||
*
|
||||
* @since 3.6.0
|
||||
*
|
||||
* @param string $file Path to file.
|
||||
* @return array|boolean Returns array of metadata, if found.
|
||||
*/
|
||||
function wp_read_audio_metadata( $file ) {
|
||||
if ( ! file_exists( $file ) )
|
||||
return false;
|
||||
$metadata = array();
|
||||
|
||||
if ( ! class_exists( 'getID3' ) )
|
||||
require( ABSPATH . WPINC . '/ID3/class-getid3.php' );
|
||||
$id3 = new getID3();
|
||||
$data = $id3->analyze( $file );
|
||||
|
||||
if ( ! empty( $data['audio'] ) ) {
|
||||
unset( $data['audio']['streams'] );
|
||||
$metadata = $data['audio'];
|
||||
}
|
||||
|
||||
if ( ! empty( $data['fileformat'] ) )
|
||||
$metadata['fileformat'] = $data['fileformat'];
|
||||
if ( ! empty( $data['filesize'] ) )
|
||||
$metadata['filesize'] = (int) $data['filesize'];
|
||||
if ( ! empty( $data['mime_type'] ) )
|
||||
$metadata['mime_type'] = $data['mime_type'];
|
||||
if ( ! empty( $data['playtime_seconds'] ) )
|
||||
$metadata['length'] = (int) ceil( $data['playtime_seconds'] );
|
||||
if ( ! empty( $data['playtime_string'] ) )
|
||||
$metadata['length_formatted'] = $data['playtime_string'];
|
||||
|
||||
wp_add_id3_tag_data( $metadata, $data );
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,729 @@
|
|||
<?php
|
||||
/////////////////////////////////////////////////////////////////
|
||||
/// getID3() by James Heinrich <info@getid3.org> //
|
||||
// available at http://getid3.sourceforge.net //
|
||||
// or http://www.getid3.org //
|
||||
// //
|
||||
// FLV module by Seth Kaufman <sethØwhirl-i-gig*com> //
|
||||
// //
|
||||
// * version 0.1 (26 June 2005) //
|
||||
// //
|
||||
// //
|
||||
// * version 0.1.1 (15 July 2005) //
|
||||
// minor modifications by James Heinrich <info@getid3.org> //
|
||||
// //
|
||||
// * version 0.2 (22 February 2006) //
|
||||
// Support for On2 VP6 codec and meta information //
|
||||
// by Steve Webster <steve.websterØfeaturecreep*com> //
|
||||
// //
|
||||
// * version 0.3 (15 June 2006) //
|
||||
// Modified to not read entire file into memory //
|
||||
// by James Heinrich <info@getid3.org> //
|
||||
// //
|
||||
// * version 0.4 (07 December 2007) //
|
||||
// Bugfixes for incorrectly parsed FLV dimensions //
|
||||
// and incorrect parsing of onMetaTag //
|
||||
// by Evgeny Moysevich <moysevichØgmail*com> //
|
||||
// //
|
||||
// * version 0.5 (21 May 2009) //
|
||||
// Fixed parsing of audio tags and added additional codec //
|
||||
// details. The duration is now read from onMetaTag (if //
|
||||
// exists), rather than parsing whole file //
|
||||
// by Nigel Barnes <ngbarnesØhotmail*com> //
|
||||
// //
|
||||
// * version 0.6 (24 May 2009) //
|
||||
// Better parsing of files with h264 video //
|
||||
// by Evgeny Moysevich <moysevichØgmail*com> //
|
||||
// //
|
||||
// * version 0.6.1 (30 May 2011) //
|
||||
// prevent infinite loops in expGolombUe() //
|
||||
// //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// //
|
||||
// module.audio-video.flv.php //
|
||||
// module for analyzing Shockwave Flash Video files //
|
||||
// dependencies: NONE //
|
||||
// ///
|
||||
/////////////////////////////////////////////////////////////////
|
||||
|
||||
define('GETID3_FLV_TAG_AUDIO', 8);
|
||||
define('GETID3_FLV_TAG_VIDEO', 9);
|
||||
define('GETID3_FLV_TAG_META', 18);
|
||||
|
||||
define('GETID3_FLV_VIDEO_H263', 2);
|
||||
define('GETID3_FLV_VIDEO_SCREEN', 3);
|
||||
define('GETID3_FLV_VIDEO_VP6FLV', 4);
|
||||
define('GETID3_FLV_VIDEO_VP6FLV_ALPHA', 5);
|
||||
define('GETID3_FLV_VIDEO_SCREENV2', 6);
|
||||
define('GETID3_FLV_VIDEO_H264', 7);
|
||||
|
||||
define('H264_AVC_SEQUENCE_HEADER', 0);
|
||||
define('H264_PROFILE_BASELINE', 66);
|
||||
define('H264_PROFILE_MAIN', 77);
|
||||
define('H264_PROFILE_EXTENDED', 88);
|
||||
define('H264_PROFILE_HIGH', 100);
|
||||
define('H264_PROFILE_HIGH10', 110);
|
||||
define('H264_PROFILE_HIGH422', 122);
|
||||
define('H264_PROFILE_HIGH444', 144);
|
||||
define('H264_PROFILE_HIGH444_PREDICTIVE', 244);
|
||||
|
||||
class getid3_flv extends getid3_handler
|
||||
{
|
||||
public $max_frames = 100000; // break out of the loop if too many frames have been scanned; only scan this many if meta frame does not contain useful duration
|
||||
|
||||
public function Analyze() {
|
||||
$info = &$this->getid3->info;
|
||||
|
||||
fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET);
|
||||
|
||||
$FLVdataLength = $info['avdataend'] - $info['avdataoffset'];
|
||||
$FLVheader = fread($this->getid3->fp, 5);
|
||||
|
||||
$info['fileformat'] = 'flv';
|
||||
$info['flv']['header']['signature'] = substr($FLVheader, 0, 3);
|
||||
$info['flv']['header']['version'] = getid3_lib::BigEndian2Int(substr($FLVheader, 3, 1));
|
||||
$TypeFlags = getid3_lib::BigEndian2Int(substr($FLVheader, 4, 1));
|
||||
|
||||
$magic = 'FLV';
|
||||
if ($info['flv']['header']['signature'] != $magic) {
|
||||
$info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($info['flv']['header']['signature']).'"';
|
||||
unset($info['flv']);
|
||||
unset($info['fileformat']);
|
||||
return false;
|
||||
}
|
||||
|
||||
$info['flv']['header']['hasAudio'] = (bool) ($TypeFlags & 0x04);
|
||||
$info['flv']['header']['hasVideo'] = (bool) ($TypeFlags & 0x01);
|
||||
|
||||
$FrameSizeDataLength = getid3_lib::BigEndian2Int(fread($this->getid3->fp, 4));
|
||||
$FLVheaderFrameLength = 9;
|
||||
if ($FrameSizeDataLength > $FLVheaderFrameLength) {
|
||||
fseek($this->getid3->fp, $FrameSizeDataLength - $FLVheaderFrameLength, SEEK_CUR);
|
||||
}
|
||||
$Duration = 0;
|
||||
$found_video = false;
|
||||
$found_audio = false;
|
||||
$found_meta = false;
|
||||
$found_valid_meta_playtime = false;
|
||||
$tagParseCount = 0;
|
||||
$info['flv']['framecount'] = array('total'=>0, 'audio'=>0, 'video'=>0);
|
||||
$flv_framecount = &$info['flv']['framecount'];
|
||||
while (((ftell($this->getid3->fp) + 16) < $info['avdataend']) && (($tagParseCount++ <= $this->max_frames) || !$found_valid_meta_playtime)) {
|
||||
$ThisTagHeader = fread($this->getid3->fp, 16);
|
||||
|
||||
$PreviousTagLength = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 0, 4));
|
||||
$TagType = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 4, 1));
|
||||
$DataLength = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 5, 3));
|
||||
$Timestamp = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 8, 3));
|
||||
$LastHeaderByte = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 15, 1));
|
||||
$NextOffset = ftell($this->getid3->fp) - 1 + $DataLength;
|
||||
if ($Timestamp > $Duration) {
|
||||
$Duration = $Timestamp;
|
||||
}
|
||||
|
||||
$flv_framecount['total']++;
|
||||
switch ($TagType) {
|
||||
case GETID3_FLV_TAG_AUDIO:
|
||||
$flv_framecount['audio']++;
|
||||
if (!$found_audio) {
|
||||
$found_audio = true;
|
||||
$info['flv']['audio']['audioFormat'] = ($LastHeaderByte >> 4) & 0x0F;
|
||||
$info['flv']['audio']['audioRate'] = ($LastHeaderByte >> 2) & 0x03;
|
||||
$info['flv']['audio']['audioSampleSize'] = ($LastHeaderByte >> 1) & 0x01;
|
||||
$info['flv']['audio']['audioType'] = $LastHeaderByte & 0x01;
|
||||
}
|
||||
break;
|
||||
|
||||
case GETID3_FLV_TAG_VIDEO:
|
||||
$flv_framecount['video']++;
|
||||
if (!$found_video) {
|
||||
$found_video = true;
|
||||
$info['flv']['video']['videoCodec'] = $LastHeaderByte & 0x07;
|
||||
|
||||
$FLVvideoHeader = fread($this->getid3->fp, 11);
|
||||
|
||||
if ($info['flv']['video']['videoCodec'] == GETID3_FLV_VIDEO_H264) {
|
||||
// this code block contributed by: moysevichØgmail*com
|
||||
|
||||
$AVCPacketType = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 0, 1));
|
||||
if ($AVCPacketType == H264_AVC_SEQUENCE_HEADER) {
|
||||
// read AVCDecoderConfigurationRecord
|
||||
$configurationVersion = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 4, 1));
|
||||
$AVCProfileIndication = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 5, 1));
|
||||
$profile_compatibility = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 6, 1));
|
||||
$lengthSizeMinusOne = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 7, 1));
|
||||
$numOfSequenceParameterSets = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 8, 1));
|
||||
|
||||
if (($numOfSequenceParameterSets & 0x1F) != 0) {
|
||||
// there is at least one SequenceParameterSet
|
||||
// read size of the first SequenceParameterSet
|
||||
//$spsSize = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 9, 2));
|
||||
$spsSize = getid3_lib::LittleEndian2Int(substr($FLVvideoHeader, 9, 2));
|
||||
// read the first SequenceParameterSet
|
||||
$sps = fread($this->getid3->fp, $spsSize);
|
||||
if (strlen($sps) == $spsSize) { // make sure that whole SequenceParameterSet was red
|
||||
$spsReader = new AVCSequenceParameterSetReader($sps);
|
||||
$spsReader->readData();
|
||||
$info['video']['resolution_x'] = $spsReader->getWidth();
|
||||
$info['video']['resolution_y'] = $spsReader->getHeight();
|
||||
}
|
||||
}
|
||||
}
|
||||
// end: moysevichØgmail*com
|
||||
|
||||
} elseif ($info['flv']['video']['videoCodec'] == GETID3_FLV_VIDEO_H263) {
|
||||
|
||||
$PictureSizeType = (getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 3, 2))) >> 7;
|
||||
$PictureSizeType = $PictureSizeType & 0x0007;
|
||||
$info['flv']['header']['videoSizeType'] = $PictureSizeType;
|
||||
switch ($PictureSizeType) {
|
||||
case 0:
|
||||
//$PictureSizeEnc = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 5, 2));
|
||||
//$PictureSizeEnc <<= 1;
|
||||
//$info['video']['resolution_x'] = ($PictureSizeEnc & 0xFF00) >> 8;
|
||||
//$PictureSizeEnc = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 6, 2));
|
||||
//$PictureSizeEnc <<= 1;
|
||||
//$info['video']['resolution_y'] = ($PictureSizeEnc & 0xFF00) >> 8;
|
||||
|
||||
$PictureSizeEnc['x'] = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 4, 2));
|
||||
$PictureSizeEnc['y'] = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 5, 2));
|
||||
$PictureSizeEnc['x'] >>= 7;
|
||||
$PictureSizeEnc['y'] >>= 7;
|
||||
$info['video']['resolution_x'] = $PictureSizeEnc['x'] & 0xFF;
|
||||
$info['video']['resolution_y'] = $PictureSizeEnc['y'] & 0xFF;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
$PictureSizeEnc['x'] = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 4, 3));
|
||||
$PictureSizeEnc['y'] = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 6, 3));
|
||||
$PictureSizeEnc['x'] >>= 7;
|
||||
$PictureSizeEnc['y'] >>= 7;
|
||||
$info['video']['resolution_x'] = $PictureSizeEnc['x'] & 0xFFFF;
|
||||
$info['video']['resolution_y'] = $PictureSizeEnc['y'] & 0xFFFF;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
$info['video']['resolution_x'] = 352;
|
||||
$info['video']['resolution_y'] = 288;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
$info['video']['resolution_x'] = 176;
|
||||
$info['video']['resolution_y'] = 144;
|
||||
break;
|
||||
|
||||
case 4:
|
||||
$info['video']['resolution_x'] = 128;
|
||||
$info['video']['resolution_y'] = 96;
|
||||
break;
|
||||
|
||||
case 5:
|
||||
$info['video']['resolution_x'] = 320;
|
||||
$info['video']['resolution_y'] = 240;
|
||||
break;
|
||||
|
||||
case 6:
|
||||
$info['video']['resolution_x'] = 160;
|
||||
$info['video']['resolution_y'] = 120;
|
||||
break;
|
||||
|
||||
default:
|
||||
$info['video']['resolution_x'] = 0;
|
||||
$info['video']['resolution_y'] = 0;
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
$info['video']['pixel_aspect_ratio'] = $info['video']['resolution_x'] / $info['video']['resolution_y'];
|
||||
}
|
||||
break;
|
||||
|
||||
// Meta tag
|
||||
case GETID3_FLV_TAG_META:
|
||||
if (!$found_meta) {
|
||||
$found_meta = true;
|
||||
fseek($this->getid3->fp, -1, SEEK_CUR);
|
||||
$datachunk = fread($this->getid3->fp, $DataLength);
|
||||
$AMFstream = new AMFStream($datachunk);
|
||||
$reader = new AMFReader($AMFstream);
|
||||
$eventName = $reader->readData();
|
||||
$info['flv']['meta'][$eventName] = $reader->readData();
|
||||
unset($reader);
|
||||
|
||||
$copykeys = array('framerate'=>'frame_rate', 'width'=>'resolution_x', 'height'=>'resolution_y', 'audiodatarate'=>'bitrate', 'videodatarate'=>'bitrate');
|
||||
foreach ($copykeys as $sourcekey => $destkey) {
|
||||
if (isset($info['flv']['meta']['onMetaData'][$sourcekey])) {
|
||||
switch ($sourcekey) {
|
||||
case 'width':
|
||||
case 'height':
|
||||
$info['video'][$destkey] = intval(round($info['flv']['meta']['onMetaData'][$sourcekey]));
|
||||
break;
|
||||
case 'audiodatarate':
|
||||
$info['audio'][$destkey] = getid3_lib::CastAsInt(round($info['flv']['meta']['onMetaData'][$sourcekey] * 1000));
|
||||
break;
|
||||
case 'videodatarate':
|
||||
case 'frame_rate':
|
||||
default:
|
||||
$info['video'][$destkey] = $info['flv']['meta']['onMetaData'][$sourcekey];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($info['flv']['meta']['onMetaData']['duration'])) {
|
||||
$found_valid_meta_playtime = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// noop
|
||||
break;
|
||||
}
|
||||
fseek($this->getid3->fp, $NextOffset, SEEK_SET);
|
||||
}
|
||||
|
||||
$info['playtime_seconds'] = $Duration / 1000;
|
||||
if ($info['playtime_seconds'] > 0) {
|
||||
$info['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds'];
|
||||
}
|
||||
|
||||
if ($info['flv']['header']['hasAudio']) {
|
||||
$info['audio']['codec'] = $this->FLVaudioFormat($info['flv']['audio']['audioFormat']);
|
||||
$info['audio']['sample_rate'] = $this->FLVaudioRate($info['flv']['audio']['audioRate']);
|
||||
$info['audio']['bits_per_sample'] = $this->FLVaudioBitDepth($info['flv']['audio']['audioSampleSize']);
|
||||
|
||||
$info['audio']['channels'] = $info['flv']['audio']['audioType'] + 1; // 0=mono,1=stereo
|
||||
$info['audio']['lossless'] = ($info['flv']['audio']['audioFormat'] ? false : true); // 0=uncompressed
|
||||
$info['audio']['dataformat'] = 'flv';
|
||||
}
|
||||
if (!empty($info['flv']['header']['hasVideo'])) {
|
||||
$info['video']['codec'] = $this->FLVvideoCodec($info['flv']['video']['videoCodec']);
|
||||
$info['video']['dataformat'] = 'flv';
|
||||
$info['video']['lossless'] = false;
|
||||
}
|
||||
|
||||
// Set information from meta
|
||||
if (!empty($info['flv']['meta']['onMetaData']['duration'])) {
|
||||
$info['playtime_seconds'] = $info['flv']['meta']['onMetaData']['duration'];
|
||||
$info['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds'];
|
||||
}
|
||||
if (isset($info['flv']['meta']['onMetaData']['audiocodecid'])) {
|
||||
$info['audio']['codec'] = $this->FLVaudioFormat($info['flv']['meta']['onMetaData']['audiocodecid']);
|
||||
}
|
||||
if (isset($info['flv']['meta']['onMetaData']['videocodecid'])) {
|
||||
$info['video']['codec'] = $this->FLVvideoCodec($info['flv']['meta']['onMetaData']['videocodecid']);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function FLVaudioFormat($id) {
|
||||
$FLVaudioFormat = array(
|
||||
0 => 'Linear PCM, platform endian',
|
||||
1 => 'ADPCM',
|
||||
2 => 'mp3',
|
||||
3 => 'Linear PCM, little endian',
|
||||
4 => 'Nellymoser 16kHz mono',
|
||||
5 => 'Nellymoser 8kHz mono',
|
||||
6 => 'Nellymoser',
|
||||
7 => 'G.711A-law logarithmic PCM',
|
||||
8 => 'G.711 mu-law logarithmic PCM',
|
||||
9 => 'reserved',
|
||||
10 => 'AAC',
|
||||
11 => false, // unknown?
|
||||
12 => false, // unknown?
|
||||
13 => false, // unknown?
|
||||
14 => 'mp3 8kHz',
|
||||
15 => 'Device-specific sound',
|
||||
);
|
||||
return (isset($FLVaudioFormat[$id]) ? $FLVaudioFormat[$id] : false);
|
||||
}
|
||||
|
||||
public function FLVaudioRate($id) {
|
||||
$FLVaudioRate = array(
|
||||
0 => 5500,
|
||||
1 => 11025,
|
||||
2 => 22050,
|
||||
3 => 44100,
|
||||
);
|
||||
return (isset($FLVaudioRate[$id]) ? $FLVaudioRate[$id] : false);
|
||||
}
|
||||
|
||||
public function FLVaudioBitDepth($id) {
|
||||
$FLVaudioBitDepth = array(
|
||||
0 => 8,
|
||||
1 => 16,
|
||||
);
|
||||
return (isset($FLVaudioBitDepth[$id]) ? $FLVaudioBitDepth[$id] : false);
|
||||
}
|
||||
|
||||
public function FLVvideoCodec($id) {
|
||||
$FLVvideoCodec = array(
|
||||
GETID3_FLV_VIDEO_H263 => 'Sorenson H.263',
|
||||
GETID3_FLV_VIDEO_SCREEN => 'Screen video',
|
||||
GETID3_FLV_VIDEO_VP6FLV => 'On2 VP6',
|
||||
GETID3_FLV_VIDEO_VP6FLV_ALPHA => 'On2 VP6 with alpha channel',
|
||||
GETID3_FLV_VIDEO_SCREENV2 => 'Screen video v2',
|
||||
GETID3_FLV_VIDEO_H264 => 'Sorenson H.264',
|
||||
);
|
||||
return (isset($FLVvideoCodec[$id]) ? $FLVvideoCodec[$id] : false);
|
||||
}
|
||||
}
|
||||
|
||||
class AMFStream {
|
||||
public $bytes;
|
||||
public $pos;
|
||||
|
||||
public function AMFStream(&$bytes) {
|
||||
$this->bytes =& $bytes;
|
||||
$this->pos = 0;
|
||||
}
|
||||
|
||||
public function readByte() {
|
||||
return getid3_lib::BigEndian2Int(substr($this->bytes, $this->pos++, 1));
|
||||
}
|
||||
|
||||
public function readInt() {
|
||||
return ($this->readByte() << 8) + $this->readByte();
|
||||
}
|
||||
|
||||
public function readLong() {
|
||||
return ($this->readByte() << 24) + ($this->readByte() << 16) + ($this->readByte() << 8) + $this->readByte();
|
||||
}
|
||||
|
||||
public function readDouble() {
|
||||
return getid3_lib::BigEndian2Float($this->read(8));
|
||||
}
|
||||
|
||||
public function readUTF() {
|
||||
$length = $this->readInt();
|
||||
return $this->read($length);
|
||||
}
|
||||
|
||||
public function readLongUTF() {
|
||||
$length = $this->readLong();
|
||||
return $this->read($length);
|
||||
}
|
||||
|
||||
public function read($length) {
|
||||
$val = substr($this->bytes, $this->pos, $length);
|
||||
$this->pos += $length;
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function peekByte() {
|
||||
$pos = $this->pos;
|
||||
$val = $this->readByte();
|
||||
$this->pos = $pos;
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function peekInt() {
|
||||
$pos = $this->pos;
|
||||
$val = $this->readInt();
|
||||
$this->pos = $pos;
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function peekLong() {
|
||||
$pos = $this->pos;
|
||||
$val = $this->readLong();
|
||||
$this->pos = $pos;
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function peekDouble() {
|
||||
$pos = $this->pos;
|
||||
$val = $this->readDouble();
|
||||
$this->pos = $pos;
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function peekUTF() {
|
||||
$pos = $this->pos;
|
||||
$val = $this->readUTF();
|
||||
$this->pos = $pos;
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function peekLongUTF() {
|
||||
$pos = $this->pos;
|
||||
$val = $this->readLongUTF();
|
||||
$this->pos = $pos;
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
|
||||
class AMFReader {
|
||||
public $stream;
|
||||
|
||||
public function AMFReader(&$stream) {
|
||||
$this->stream =& $stream;
|
||||
}
|
||||
|
||||
public function readData() {
|
||||
$value = null;
|
||||
|
||||
$type = $this->stream->readByte();
|
||||
switch ($type) {
|
||||
|
||||
// Double
|
||||
case 0:
|
||||
$value = $this->readDouble();
|
||||
break;
|
||||
|
||||
// Boolean
|
||||
case 1:
|
||||
$value = $this->readBoolean();
|
||||
break;
|
||||
|
||||
// String
|
||||
case 2:
|
||||
$value = $this->readString();
|
||||
break;
|
||||
|
||||
// Object
|
||||
case 3:
|
||||
$value = $this->readObject();
|
||||
break;
|
||||
|
||||
// null
|
||||
case 6:
|
||||
return null;
|
||||
break;
|
||||
|
||||
// Mixed array
|
||||
case 8:
|
||||
$value = $this->readMixedArray();
|
||||
break;
|
||||
|
||||
// Array
|
||||
case 10:
|
||||
$value = $this->readArray();
|
||||
break;
|
||||
|
||||
// Date
|
||||
case 11:
|
||||
$value = $this->readDate();
|
||||
break;
|
||||
|
||||
// Long string
|
||||
case 13:
|
||||
$value = $this->readLongString();
|
||||
break;
|
||||
|
||||
// XML (handled as string)
|
||||
case 15:
|
||||
$value = $this->readXML();
|
||||
break;
|
||||
|
||||
// Typed object (handled as object)
|
||||
case 16:
|
||||
$value = $this->readTypedObject();
|
||||
break;
|
||||
|
||||
// Long string
|
||||
default:
|
||||
$value = '(unknown or unsupported data type)';
|
||||
break;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function readDouble() {
|
||||
return $this->stream->readDouble();
|
||||
}
|
||||
|
||||
public function readBoolean() {
|
||||
return $this->stream->readByte() == 1;
|
||||
}
|
||||
|
||||
public function readString() {
|
||||
return $this->stream->readUTF();
|
||||
}
|
||||
|
||||
public function readObject() {
|
||||
// Get highest numerical index - ignored
|
||||
// $highestIndex = $this->stream->readLong();
|
||||
|
||||
$data = array();
|
||||
|
||||
while ($key = $this->stream->readUTF()) {
|
||||
$data[$key] = $this->readData();
|
||||
}
|
||||
// Mixed array record ends with empty string (0x00 0x00) and 0x09
|
||||
if (($key == '') && ($this->stream->peekByte() == 0x09)) {
|
||||
// Consume byte
|
||||
$this->stream->readByte();
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function readMixedArray() {
|
||||
// Get highest numerical index - ignored
|
||||
$highestIndex = $this->stream->readLong();
|
||||
|
||||
$data = array();
|
||||
|
||||
while ($key = $this->stream->readUTF()) {
|
||||
if (is_numeric($key)) {
|
||||
$key = (float) $key;
|
||||
}
|
||||
$data[$key] = $this->readData();
|
||||
}
|
||||
// Mixed array record ends with empty string (0x00 0x00) and 0x09
|
||||
if (($key == '') && ($this->stream->peekByte() == 0x09)) {
|
||||
// Consume byte
|
||||
$this->stream->readByte();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function readArray() {
|
||||
$length = $this->stream->readLong();
|
||||
$data = array();
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$data[] = $this->readData();
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function readDate() {
|
||||
$timestamp = $this->stream->readDouble();
|
||||
$timezone = $this->stream->readInt();
|
||||
return $timestamp;
|
||||
}
|
||||
|
||||
public function readLongString() {
|
||||
return $this->stream->readLongUTF();
|
||||
}
|
||||
|
||||
public function readXML() {
|
||||
return $this->stream->readLongUTF();
|
||||
}
|
||||
|
||||
public function readTypedObject() {
|
||||
$className = $this->stream->readUTF();
|
||||
return $this->readObject();
|
||||
}
|
||||
}
|
||||
|
||||
class AVCSequenceParameterSetReader {
|
||||
public $sps;
|
||||
public $start = 0;
|
||||
public $currentBytes = 0;
|
||||
public $currentBits = 0;
|
||||
public $width;
|
||||
public $height;
|
||||
|
||||
public function AVCSequenceParameterSetReader($sps) {
|
||||
$this->sps = $sps;
|
||||
}
|
||||
|
||||
public function readData() {
|
||||
$this->skipBits(8);
|
||||
$this->skipBits(8);
|
||||
$profile = $this->getBits(8); // read profile
|
||||
$this->skipBits(16);
|
||||
$this->expGolombUe(); // read sps id
|
||||
if (in_array($profile, array(H264_PROFILE_HIGH, H264_PROFILE_HIGH10, H264_PROFILE_HIGH422, H264_PROFILE_HIGH444, H264_PROFILE_HIGH444_PREDICTIVE))) {
|
||||
if ($this->expGolombUe() == 3) {
|
||||
$this->skipBits(1);
|
||||
}
|
||||
$this->expGolombUe();
|
||||
$this->expGolombUe();
|
||||
$this->skipBits(1);
|
||||
if ($this->getBit()) {
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
if ($this->getBit()) {
|
||||
$size = $i < 6 ? 16 : 64;
|
||||
$lastScale = 8;
|
||||
$nextScale = 8;
|
||||
for ($j = 0; $j < $size; $j++) {
|
||||
if ($nextScale != 0) {
|
||||
$deltaScale = $this->expGolombUe();
|
||||
$nextScale = ($lastScale + $deltaScale + 256) % 256;
|
||||
}
|
||||
if ($nextScale != 0) {
|
||||
$lastScale = $nextScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->expGolombUe();
|
||||
$pocType = $this->expGolombUe();
|
||||
if ($pocType == 0) {
|
||||
$this->expGolombUe();
|
||||
} elseif ($pocType == 1) {
|
||||
$this->skipBits(1);
|
||||
$this->expGolombSe();
|
||||
$this->expGolombSe();
|
||||
$pocCycleLength = $this->expGolombUe();
|
||||
for ($i = 0; $i < $pocCycleLength; $i++) {
|
||||
$this->expGolombSe();
|
||||
}
|
||||
}
|
||||
$this->expGolombUe();
|
||||
$this->skipBits(1);
|
||||
$this->width = ($this->expGolombUe() + 1) * 16;
|
||||
$heightMap = $this->expGolombUe() + 1;
|
||||
$this->height = (2 - $this->getBit()) * $heightMap * 16;
|
||||
}
|
||||
|
||||
public function skipBits($bits) {
|
||||
$newBits = $this->currentBits + $bits;
|
||||
$this->currentBytes += (int)floor($newBits / 8);
|
||||
$this->currentBits = $newBits % 8;
|
||||
}
|
||||
|
||||
public function getBit() {
|
||||
$result = (getid3_lib::BigEndian2Int(substr($this->sps, $this->currentBytes, 1)) >> (7 - $this->currentBits)) & 0x01;
|
||||
$this->skipBits(1);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getBits($bits) {
|
||||
$result = 0;
|
||||
for ($i = 0; $i < $bits; $i++) {
|
||||
$result = ($result << 1) + $this->getBit();
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function expGolombUe() {
|
||||
$significantBits = 0;
|
||||
$bit = $this->getBit();
|
||||
while ($bit == 0) {
|
||||
$significantBits++;
|
||||
$bit = $this->getBit();
|
||||
|
||||
if ($significantBits > 31) {
|
||||
// something is broken, this is an emergency escape to prevent infinite loops
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return (1 << $significantBits) + $this->getBits($significantBits) - 1;
|
||||
}
|
||||
|
||||
public function expGolombSe() {
|
||||
$result = $this->expGolombUe();
|
||||
if (($result & 0x01) == 0) {
|
||||
return -($result >> 1);
|
||||
} else {
|
||||
return ($result + 1) >> 1;
|
||||
}
|
||||
}
|
||||
|
||||
public function getWidth() {
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
public function getHeight() {
|
||||
return $this->height;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,473 @@
|
|||
<?php
|
||||
/////////////////////////////////////////////////////////////////
|
||||
/// getID3() by James Heinrich <info@getid3.org> //
|
||||
// available at http://getid3.sourceforge.net //
|
||||
// or http://www.getid3.org //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// See readme.txt for more details //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// //
|
||||
// module.audio.ac3.php //
|
||||
// module for analyzing AC-3 (aka Dolby Digital) audio files //
|
||||
// dependencies: NONE //
|
||||
// ///
|
||||
/////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
class getid3_ac3 extends getid3_handler
|
||||
{
|
||||
private $AC3header = array();
|
||||
private $BSIoffset = 0;
|
||||
|
||||
const syncword = "\x0B\x77";
|
||||
|
||||
public function Analyze() {
|
||||
$info = &$this->getid3->info;
|
||||
|
||||
///AH
|
||||
$info['ac3']['raw']['bsi'] = array();
|
||||
$thisfile_ac3 = &$info['ac3'];
|
||||
$thisfile_ac3_raw = &$thisfile_ac3['raw'];
|
||||
$thisfile_ac3_raw_bsi = &$thisfile_ac3_raw['bsi'];
|
||||
|
||||
|
||||
// http://www.atsc.org/standards/a_52a.pdf
|
||||
|
||||
$info['fileformat'] = 'ac3';
|
||||
|
||||
// An AC-3 serial coded audio bit stream is made up of a sequence of synchronization frames
|
||||
// Each synchronization frame contains 6 coded audio blocks (AB), each of which represent 256
|
||||
// new audio samples per channel. A synchronization information (SI) header at the beginning
|
||||
// of each frame contains information needed to acquire and maintain synchronization. A
|
||||
// bit stream information (BSI) header follows SI, and contains parameters describing the coded
|
||||
// audio service. The coded audio blocks may be followed by an auxiliary data (Aux) field. At the
|
||||
// end of each frame is an error check field that includes a CRC word for error detection. An
|
||||
// additional CRC word is located in the SI header, the use of which, by a decoder, is optional.
|
||||
//
|
||||
// syncinfo() | bsi() | AB0 | AB1 | AB2 | AB3 | AB4 | AB5 | Aux | CRC
|
||||
|
||||
// syncinfo() {
|
||||
// syncword 16
|
||||
// crc1 16
|
||||
// fscod 2
|
||||
// frmsizecod 6
|
||||
// } /* end of syncinfo */
|
||||
|
||||
$this->fseek($info['avdataoffset']);
|
||||
$this->AC3header['syncinfo'] = $this->fread(5);
|
||||
|
||||
if (strpos($this->AC3header['syncinfo'], self::syncword) === 0) {
|
||||
$thisfile_ac3_raw['synchinfo']['synchword'] = self::syncword;
|
||||
$offset = 2;
|
||||
} else {
|
||||
if (!$this->isDependencyFor('matroska')) {
|
||||
unset($info['fileformat'], $info['ac3']);
|
||||
return $this->error('Expecting "'.getid3_lib::PrintHexBytes(self::syncword).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($this->AC3header['syncinfo'], 0, 2)).'"');
|
||||
}
|
||||
$offset = 0;
|
||||
$this->fseek(-2, SEEK_CUR);
|
||||
}
|
||||
|
||||
$info['audio']['dataformat'] = 'ac3';
|
||||
$info['audio']['bitrate_mode'] = 'cbr';
|
||||
$info['audio']['lossless'] = false;
|
||||
|
||||
$thisfile_ac3_raw['synchinfo']['crc1'] = getid3_lib::LittleEndian2Int(substr($this->AC3header['syncinfo'], $offset, 2));
|
||||
$ac3_synchinfo_fscod_frmsizecod = getid3_lib::LittleEndian2Int(substr($this->AC3header['syncinfo'], ($offset + 2), 1));
|
||||
$thisfile_ac3_raw['synchinfo']['fscod'] = ($ac3_synchinfo_fscod_frmsizecod & 0xC0) >> 6;
|
||||
$thisfile_ac3_raw['synchinfo']['frmsizecod'] = ($ac3_synchinfo_fscod_frmsizecod & 0x3F);
|
||||
|
||||
$thisfile_ac3['sample_rate'] = self::sampleRateCodeLookup($thisfile_ac3_raw['synchinfo']['fscod']);
|
||||
if ($thisfile_ac3_raw['synchinfo']['fscod'] <= 3) {
|
||||
$info['audio']['sample_rate'] = $thisfile_ac3['sample_rate'];
|
||||
}
|
||||
|
||||
$thisfile_ac3['frame_length'] = self::frameSizeLookup($thisfile_ac3_raw['synchinfo']['frmsizecod'], $thisfile_ac3_raw['synchinfo']['fscod']);
|
||||
$thisfile_ac3['bitrate'] = self::bitrateLookup($thisfile_ac3_raw['synchinfo']['frmsizecod']);
|
||||
$info['audio']['bitrate'] = $thisfile_ac3['bitrate'];
|
||||
|
||||
$this->AC3header['bsi'] = getid3_lib::BigEndian2Bin($this->fread(15));
|
||||
$ac3_bsi_offset = 0;
|
||||
|
||||
$thisfile_ac3_raw_bsi['bsid'] = $this->readHeaderBSI(5);
|
||||
if ($thisfile_ac3_raw_bsi['bsid'] > 8) {
|
||||
// Decoders which can decode version 8 will thus be able to decode version numbers less than 8.
|
||||
// If this standard is extended by the addition of additional elements or features, a value of bsid greater than 8 will be used.
|
||||
// Decoders built to this version of the standard will not be able to decode versions with bsid greater than 8.
|
||||
$this->error('Bit stream identification is version '.$thisfile_ac3_raw_bsi['bsid'].', but getID3() only understands up to version 8');
|
||||
unset($info['ac3']);
|
||||
return false;
|
||||
}
|
||||
|
||||
$thisfile_ac3_raw_bsi['bsmod'] = $this->readHeaderBSI(3);
|
||||
$thisfile_ac3_raw_bsi['acmod'] = $this->readHeaderBSI(3);
|
||||
|
||||
$thisfile_ac3['service_type'] = self::serviceTypeLookup($thisfile_ac3_raw_bsi['bsmod'], $thisfile_ac3_raw_bsi['acmod']);
|
||||
$ac3_coding_mode = self::audioCodingModeLookup($thisfile_ac3_raw_bsi['acmod']);
|
||||
foreach($ac3_coding_mode as $key => $value) {
|
||||
$thisfile_ac3[$key] = $value;
|
||||
}
|
||||
switch ($thisfile_ac3_raw_bsi['acmod']) {
|
||||
case 0:
|
||||
case 1:
|
||||
$info['audio']['channelmode'] = 'mono';
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
$info['audio']['channelmode'] = 'stereo';
|
||||
break;
|
||||
default:
|
||||
$info['audio']['channelmode'] = 'surround';
|
||||
break;
|
||||
}
|
||||
$info['audio']['channels'] = $thisfile_ac3['num_channels'];
|
||||
|
||||
if ($thisfile_ac3_raw_bsi['acmod'] & 0x01) {
|
||||
// If the lsb of acmod is a 1, center channel is in use and cmixlev follows in the bit stream.
|
||||
$thisfile_ac3_raw_bsi['cmixlev'] = $this->readHeaderBSI(2);
|
||||
$thisfile_ac3['center_mix_level'] = self::centerMixLevelLookup($thisfile_ac3_raw_bsi['cmixlev']);
|
||||
}
|
||||
|
||||
if ($thisfile_ac3_raw_bsi['acmod'] & 0x04) {
|
||||
// If the msb of acmod is a 1, surround channels are in use and surmixlev follows in the bit stream.
|
||||
$thisfile_ac3_raw_bsi['surmixlev'] = $this->readHeaderBSI(2);
|
||||
$thisfile_ac3['surround_mix_level'] = self::surroundMixLevelLookup($thisfile_ac3_raw_bsi['surmixlev']);
|
||||
}
|
||||
|
||||
if ($thisfile_ac3_raw_bsi['acmod'] == 0x02) {
|
||||
// When operating in the two channel mode, this 2-bit code indicates whether or not the program has been encoded in Dolby Surround.
|
||||
$thisfile_ac3_raw_bsi['dsurmod'] = $this->readHeaderBSI(2);
|
||||
$thisfile_ac3['dolby_surround_mode'] = self::dolbySurroundModeLookup($thisfile_ac3_raw_bsi['dsurmod']);
|
||||
}
|
||||
|
||||
$thisfile_ac3_raw_bsi['lfeon'] = (bool) $this->readHeaderBSI(1);
|
||||
$thisfile_ac3['lfe_enabled'] = $thisfile_ac3_raw_bsi['lfeon'];
|
||||
if ($thisfile_ac3_raw_bsi['lfeon']) {
|
||||
//$info['audio']['channels']++;
|
||||
$info['audio']['channels'] .= '.1';
|
||||
}
|
||||
|
||||
$thisfile_ac3['channels_enabled'] = self::channelsEnabledLookup($thisfile_ac3_raw_bsi['acmod'], $thisfile_ac3_raw_bsi['lfeon']);
|
||||
|
||||
// This indicates how far the average dialogue level is below digital 100 percent. Valid values are 1-31.
|
||||
// The value of 0 is reserved. The values of 1 to 31 are interpreted as -1 dB to -31 dB with respect to digital 100 percent.
|
||||
$thisfile_ac3_raw_bsi['dialnorm'] = $this->readHeaderBSI(5);
|
||||
$thisfile_ac3['dialogue_normalization'] = '-'.$thisfile_ac3_raw_bsi['dialnorm'].'dB';
|
||||
|
||||
$thisfile_ac3_raw_bsi['compre_flag'] = (bool) $this->readHeaderBSI(1);
|
||||
if ($thisfile_ac3_raw_bsi['compre_flag']) {
|
||||
$thisfile_ac3_raw_bsi['compr'] = $this->readHeaderBSI(8);
|
||||
$thisfile_ac3['heavy_compression'] = self::heavyCompression($thisfile_ac3_raw_bsi['compr']);
|
||||
}
|
||||
|
||||
$thisfile_ac3_raw_bsi['langcode_flag'] = (bool) $this->readHeaderBSI(1);
|
||||
if ($thisfile_ac3_raw_bsi['langcode_flag']) {
|
||||
$thisfile_ac3_raw_bsi['langcod'] = $this->readHeaderBSI(8);
|
||||
}
|
||||
|
||||
$thisfile_ac3_raw_bsi['audprodie'] = (bool) $this->readHeaderBSI(1);
|
||||
if ($thisfile_ac3_raw_bsi['audprodie']) {
|
||||
$thisfile_ac3_raw_bsi['mixlevel'] = $this->readHeaderBSI(5);
|
||||
$thisfile_ac3_raw_bsi['roomtyp'] = $this->readHeaderBSI(2);
|
||||
|
||||
$thisfile_ac3['mixing_level'] = (80 + $thisfile_ac3_raw_bsi['mixlevel']).'dB';
|
||||
$thisfile_ac3['room_type'] = self::roomTypeLookup($thisfile_ac3_raw_bsi['roomtyp']);
|
||||
}
|
||||
|
||||
if ($thisfile_ac3_raw_bsi['acmod'] == 0x00) {
|
||||
// If acmod is 0, then two completely independent program channels (dual mono)
|
||||
// are encoded into the bit stream, and are referenced as Ch1, Ch2. In this case,
|
||||
// a number of additional items are present in BSI or audblk to fully describe Ch2.
|
||||
|
||||
// This indicates how far the average dialogue level is below digital 100 percent. Valid values are 1-31.
|
||||
// The value of 0 is reserved. The values of 1 to 31 are interpreted as -1 dB to -31 dB with respect to digital 100 percent.
|
||||
$thisfile_ac3_raw_bsi['dialnorm2'] = $this->readHeaderBSI(5);
|
||||
$thisfile_ac3['dialogue_normalization2'] = '-'.$thisfile_ac3_raw_bsi['dialnorm2'].'dB';
|
||||
|
||||
$thisfile_ac3_raw_bsi['compre_flag2'] = (bool) $this->readHeaderBSI(1);
|
||||
if ($thisfile_ac3_raw_bsi['compre_flag2']) {
|
||||
$thisfile_ac3_raw_bsi['compr2'] = $this->readHeaderBSI(8);
|
||||
$thisfile_ac3['heavy_compression2'] = self::heavyCompression($thisfile_ac3_raw_bsi['compr2']);
|
||||
}
|
||||
|
||||
$thisfile_ac3_raw_bsi['langcode_flag2'] = (bool) $this->readHeaderBSI(1);
|
||||
if ($thisfile_ac3_raw_bsi['langcode_flag2']) {
|
||||
$thisfile_ac3_raw_bsi['langcod2'] = $this->readHeaderBSI(8);
|
||||
}
|
||||
|
||||
$thisfile_ac3_raw_bsi['audprodie2'] = (bool) $this->readHeaderBSI(1);
|
||||
if ($thisfile_ac3_raw_bsi['audprodie2']) {
|
||||
$thisfile_ac3_raw_bsi['mixlevel2'] = $this->readHeaderBSI(5);
|
||||
$thisfile_ac3_raw_bsi['roomtyp2'] = $this->readHeaderBSI(2);
|
||||
|
||||
$thisfile_ac3['mixing_level2'] = (80 + $thisfile_ac3_raw_bsi['mixlevel2']).'dB';
|
||||
$thisfile_ac3['room_type2'] = self::roomTypeLookup($thisfile_ac3_raw_bsi['roomtyp2']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$thisfile_ac3_raw_bsi['copyright'] = (bool) $this->readHeaderBSI(1);
|
||||
|
||||
$thisfile_ac3_raw_bsi['original'] = (bool) $this->readHeaderBSI(1);
|
||||
|
||||
$thisfile_ac3_raw_bsi['timecode1_flag'] = (bool) $this->readHeaderBSI(1);
|
||||
if ($thisfile_ac3_raw_bsi['timecode1_flag']) {
|
||||
$thisfile_ac3_raw_bsi['timecode1'] = $this->readHeaderBSI(14);
|
||||
}
|
||||
|
||||
$thisfile_ac3_raw_bsi['timecode2_flag'] = (bool) $this->readHeaderBSI(1);
|
||||
if ($thisfile_ac3_raw_bsi['timecode2_flag']) {
|
||||
$thisfile_ac3_raw_bsi['timecode2'] = $this->readHeaderBSI(14);
|
||||
}
|
||||
|
||||
$thisfile_ac3_raw_bsi['addbsi_flag'] = (bool) $this->readHeaderBSI(1);
|
||||
if ($thisfile_ac3_raw_bsi['addbsi_flag']) {
|
||||
$thisfile_ac3_raw_bsi['addbsi_length'] = $this->readHeaderBSI(6);
|
||||
|
||||
$this->AC3header['bsi'] .= getid3_lib::BigEndian2Bin($this->fread($thisfile_ac3_raw_bsi['addbsi_length']));
|
||||
|
||||
$thisfile_ac3_raw_bsi['addbsi_data'] = substr($this->AC3header['bsi'], $this->BSIoffset, $thisfile_ac3_raw_bsi['addbsi_length'] * 8);
|
||||
$this->BSIoffset += $thisfile_ac3_raw_bsi['addbsi_length'] * 8;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function readHeaderBSI($length) {
|
||||
$data = substr($this->AC3header['bsi'], $this->BSIoffset, $length);
|
||||
$this->BSIoffset += $length;
|
||||
|
||||
return bindec($data);
|
||||
}
|
||||
|
||||
public static function sampleRateCodeLookup($fscod) {
|
||||
static $sampleRateCodeLookup = array(
|
||||
0 => 48000,
|
||||
1 => 44100,
|
||||
2 => 32000,
|
||||
3 => 'reserved' // If the reserved code is indicated, the decoder should not attempt to decode audio and should mute.
|
||||
);
|
||||
return (isset($sampleRateCodeLookup[$fscod]) ? $sampleRateCodeLookup[$fscod] : false);
|
||||
}
|
||||
|
||||
public static function serviceTypeLookup($bsmod, $acmod) {
|
||||
static $serviceTypeLookup = array();
|
||||
if (empty($serviceTypeLookup)) {
|
||||
for ($i = 0; $i <= 7; $i++) {
|
||||
$serviceTypeLookup[0][$i] = 'main audio service: complete main (CM)';
|
||||
$serviceTypeLookup[1][$i] = 'main audio service: music and effects (ME)';
|
||||
$serviceTypeLookup[2][$i] = 'associated service: visually impaired (VI)';
|
||||
$serviceTypeLookup[3][$i] = 'associated service: hearing impaired (HI)';
|
||||
$serviceTypeLookup[4][$i] = 'associated service: dialogue (D)';
|
||||
$serviceTypeLookup[5][$i] = 'associated service: commentary (C)';
|
||||
$serviceTypeLookup[6][$i] = 'associated service: emergency (E)';
|
||||
}
|
||||
|
||||
$serviceTypeLookup[7][1] = 'associated service: voice over (VO)';
|
||||
for ($i = 2; $i <= 7; $i++) {
|
||||
$serviceTypeLookup[7][$i] = 'main audio service: karaoke';
|
||||
}
|
||||
}
|
||||
return (isset($serviceTypeLookup[$bsmod][$acmod]) ? $serviceTypeLookup[$bsmod][$acmod] : false);
|
||||
}
|
||||
|
||||
public static function audioCodingModeLookup($acmod) {
|
||||
// array(channel configuration, # channels (not incl LFE), channel order)
|
||||
static $audioCodingModeLookup = array (
|
||||
0 => array('channel_config'=>'1+1', 'num_channels'=>2, 'channel_order'=>'Ch1,Ch2'),
|
||||
1 => array('channel_config'=>'1/0', 'num_channels'=>1, 'channel_order'=>'C'),
|
||||
2 => array('channel_config'=>'2/0', 'num_channels'=>2, 'channel_order'=>'L,R'),
|
||||
3 => array('channel_config'=>'3/0', 'num_channels'=>3, 'channel_order'=>'L,C,R'),
|
||||
4 => array('channel_config'=>'2/1', 'num_channels'=>3, 'channel_order'=>'L,R,S'),
|
||||
5 => array('channel_config'=>'3/1', 'num_channels'=>4, 'channel_order'=>'L,C,R,S'),
|
||||
6 => array('channel_config'=>'2/2', 'num_channels'=>4, 'channel_order'=>'L,R,SL,SR'),
|
||||
7 => array('channel_config'=>'3/2', 'num_channels'=>5, 'channel_order'=>'L,C,R,SL,SR'),
|
||||
);
|
||||
return (isset($audioCodingModeLookup[$acmod]) ? $audioCodingModeLookup[$acmod] : false);
|
||||
}
|
||||
|
||||
public static function centerMixLevelLookup($cmixlev) {
|
||||
static $centerMixLevelLookup;
|
||||
if (empty($centerMixLevelLookup)) {
|
||||
$centerMixLevelLookup = array(
|
||||
0 => pow(2, -3.0 / 6), // 0.707 (-3.0 dB)
|
||||
1 => pow(2, -4.5 / 6), // 0.595 (-4.5 dB)
|
||||
2 => pow(2, -6.0 / 6), // 0.500 (-6.0 dB)
|
||||
3 => 'reserved'
|
||||
);
|
||||
}
|
||||
return (isset($centerMixLevelLookup[$cmixlev]) ? $centerMixLevelLookup[$cmixlev] : false);
|
||||
}
|
||||
|
||||
public static function surroundMixLevelLookup($surmixlev) {
|
||||
static $surroundMixLevelLookup;
|
||||
if (empty($surroundMixLevelLookup)) {
|
||||
$surroundMixLevelLookup = array(
|
||||
0 => pow(2, -3.0 / 6),
|
||||
1 => pow(2, -6.0 / 6),
|
||||
2 => 0,
|
||||
3 => 'reserved'
|
||||
);
|
||||
}
|
||||
return (isset($surroundMixLevelLookup[$surmixlev]) ? $surroundMixLevelLookup[$surmixlev] : false);
|
||||
}
|
||||
|
||||
public static function dolbySurroundModeLookup($dsurmod) {
|
||||
static $dolbySurroundModeLookup = array(
|
||||
0 => 'not indicated',
|
||||
1 => 'Not Dolby Surround encoded',
|
||||
2 => 'Dolby Surround encoded',
|
||||
3 => 'reserved'
|
||||
);
|
||||
return (isset($dolbySurroundModeLookup[$dsurmod]) ? $dolbySurroundModeLookup[$dsurmod] : false);
|
||||
}
|
||||
|
||||
public static function channelsEnabledLookup($acmod, $lfeon) {
|
||||
$lookup = array(
|
||||
'ch1'=>(bool) ($acmod == 0),
|
||||
'ch2'=>(bool) ($acmod == 0),
|
||||
'left'=>(bool) ($acmod > 1),
|
||||
'right'=>(bool) ($acmod > 1),
|
||||
'center'=>(bool) ($acmod & 0x01),
|
||||
'surround_mono'=>false,
|
||||
'surround_left'=>false,
|
||||
'surround_right'=>false,
|
||||
'lfe'=>$lfeon);
|
||||
switch ($acmod) {
|
||||
case 4:
|
||||
case 5:
|
||||
$lookup['surround_mono'] = true;
|
||||
break;
|
||||
case 6:
|
||||
case 7:
|
||||
$lookup['surround_left'] = true;
|
||||
$lookup['surround_right'] = true;
|
||||
break;
|
||||
}
|
||||
return $lookup;
|
||||
}
|
||||
|
||||
public static function heavyCompression($compre) {
|
||||
// The first four bits indicate gain changes in 6.02dB increments which can be
|
||||
// implemented with an arithmetic shift operation. The following four bits
|
||||
// indicate linear gain changes, and require a 5-bit multiply.
|
||||
// We will represent the two 4-bit fields of compr as follows:
|
||||
// X0 X1 X2 X3 . Y4 Y5 Y6 Y7
|
||||
// The meaning of the X values is most simply described by considering X to represent a 4-bit
|
||||
// signed integer with values from -8 to +7. The gain indicated by X is then (X + 1) * 6.02 dB. The
|
||||
// following table shows this in detail.
|
||||
|
||||
// Meaning of 4 msb of compr
|
||||
// 7 +48.16 dB
|
||||
// 6 +42.14 dB
|
||||
// 5 +36.12 dB
|
||||
// 4 +30.10 dB
|
||||
// 3 +24.08 dB
|
||||
// 2 +18.06 dB
|
||||
// 1 +12.04 dB
|
||||
// 0 +6.02 dB
|
||||
// -1 0 dB
|
||||
// -2 -6.02 dB
|
||||
// -3 -12.04 dB
|
||||
// -4 -18.06 dB
|
||||
// -5 -24.08 dB
|
||||
// -6 -30.10 dB
|
||||
// -7 -36.12 dB
|
||||
// -8 -42.14 dB
|
||||
|
||||
$fourbit = str_pad(decbin(($compre & 0xF0) >> 4), 4, '0', STR_PAD_LEFT);
|
||||
if ($fourbit{0} == '1') {
|
||||
$log_gain = -8 + bindec(substr($fourbit, 1));
|
||||
} else {
|
||||
$log_gain = bindec(substr($fourbit, 1));
|
||||
}
|
||||
$log_gain = ($log_gain + 1) * getid3_lib::RGADamplitude2dB(2);
|
||||
|
||||
// The value of Y is a linear representation of a gain change of up to -6 dB. Y is considered to
|
||||
// be an unsigned fractional integer, with a leading value of 1, or: 0.1 Y4 Y5 Y6 Y7 (base 2). Y can
|
||||
// represent values between 0.111112 (or 31/32) and 0.100002 (or 1/2). Thus, Y can represent gain
|
||||
// changes from -0.28 dB to -6.02 dB.
|
||||
|
||||
$lin_gain = (16 + ($compre & 0x0F)) / 32;
|
||||
|
||||
// The combination of X and Y values allows compr to indicate gain changes from
|
||||
// 48.16 - 0.28 = +47.89 dB, to
|
||||
// -42.14 - 6.02 = -48.16 dB.
|
||||
|
||||
return $log_gain - $lin_gain;
|
||||
}
|
||||
|
||||
public static function roomTypeLookup($roomtyp) {
|
||||
static $roomTypeLookup = array(
|
||||
0 => 'not indicated',
|
||||
1 => 'large room, X curve monitor',
|
||||
2 => 'small room, flat monitor',
|
||||
3 => 'reserved'
|
||||
);
|
||||
return (isset($roomTypeLookup[$roomtyp]) ? $roomTypeLookup[$roomtyp] : false);
|
||||
}
|
||||
|
||||
public static function frameSizeLookup($frmsizecod, $fscod) {
|
||||
$padding = (bool) ($frmsizecod % 2);
|
||||
$framesizeid = floor($frmsizecod / 2);
|
||||
|
||||
static $frameSizeLookup = array();
|
||||
if (empty($frameSizeLookup)) {
|
||||
$frameSizeLookup = array (
|
||||
0 => array(128, 138, 192),
|
||||
1 => array(40, 160, 174, 240),
|
||||
2 => array(48, 192, 208, 288),
|
||||
3 => array(56, 224, 242, 336),
|
||||
4 => array(64, 256, 278, 384),
|
||||
5 => array(80, 320, 348, 480),
|
||||
6 => array(96, 384, 416, 576),
|
||||
7 => array(112, 448, 486, 672),
|
||||
8 => array(128, 512, 556, 768),
|
||||
9 => array(160, 640, 696, 960),
|
||||
10 => array(192, 768, 834, 1152),
|
||||
11 => array(224, 896, 974, 1344),
|
||||
12 => array(256, 1024, 1114, 1536),
|
||||
13 => array(320, 1280, 1392, 1920),
|
||||
14 => array(384, 1536, 1670, 2304),
|
||||
15 => array(448, 1792, 1950, 2688),
|
||||
16 => array(512, 2048, 2228, 3072),
|
||||
17 => array(576, 2304, 2506, 3456),
|
||||
18 => array(640, 2560, 2786, 3840)
|
||||
);
|
||||
}
|
||||
if (($fscod == 1) && $padding) {
|
||||
// frame lengths are padded by 1 word (16 bits) at 44100
|
||||
$frameSizeLookup[$frmsizecod] += 2;
|
||||
}
|
||||
return (isset($frameSizeLookup[$framesizeid][$fscod]) ? $frameSizeLookup[$framesizeid][$fscod] : false);
|
||||
}
|
||||
|
||||
public static function bitrateLookup($frmsizecod) {
|
||||
$framesizeid = floor($frmsizecod / 2);
|
||||
|
||||
static $bitrateLookup = array(
|
||||
0 => 32000,
|
||||
1 => 40000,
|
||||
2 => 48000,
|
||||
3 => 56000,
|
||||
4 => 64000,
|
||||
5 => 80000,
|
||||
6 => 96000,
|
||||
7 => 112000,
|
||||
8 => 128000,
|
||||
9 => 160000,
|
||||
10 => 192000,
|
||||
11 => 224000,
|
||||
12 => 256000,
|
||||
13 => 320000,
|
||||
14 => 384000,
|
||||
15 => 448000,
|
||||
16 => 512000,
|
||||
17 => 576000,
|
||||
18 => 640000
|
||||
);
|
||||
return (isset($bitrateLookup[$framesizeid]) ? $bitrateLookup[$framesizeid] : false);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,290 @@
|
|||
<?php
|
||||
/////////////////////////////////////////////////////////////////
|
||||
/// getID3() by James Heinrich <info@getid3.org> //
|
||||
// available at http://getid3.sourceforge.net //
|
||||
// or http://www.getid3.org //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// See readme.txt for more details //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// //
|
||||
// module.audio.dts.php //
|
||||
// module for analyzing DTS Audio files //
|
||||
// dependencies: NONE //
|
||||
// //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
/**
|
||||
* @tutorial http://wiki.multimedia.cx/index.php?title=DTS
|
||||
*/
|
||||
class getid3_dts extends getid3_handler
|
||||
{
|
||||
/**
|
||||
* Default DTS syncword used in native .cpt or .dts formats
|
||||
*/
|
||||
const syncword = "\x7F\xFE\x80\x01";
|
||||
|
||||
private $readBinDataOffset = 0;
|
||||
|
||||
/**
|
||||
* Possible syncwords indicating bitstream encoding
|
||||
*/
|
||||
public static $syncwords = array(
|
||||
0 => "\x7F\xFE\x80\x01", // raw big-endian
|
||||
1 => "\xFE\x7F\x01\x80", // raw little-endian
|
||||
2 => "\x1F\xFF\xE8\x00", // 14-bit big-endian
|
||||
3 => "\xFF\x1F\x00\xE8"); // 14-bit little-endian
|
||||
|
||||
public function Analyze() {
|
||||
$info = &$this->getid3->info;
|
||||
$info['fileformat'] = 'dts';
|
||||
|
||||
$this->fseek($info['avdataoffset']);
|
||||
$DTSheader = $this->fread(20); // we only need 2 words magic + 6 words frame header, but these words may be normal 16-bit words OR 14-bit words with 2 highest bits set to zero, so 8 words can be either 8*16/8 = 16 bytes OR 8*16*(16/14)/8 = 18.3 bytes
|
||||
|
||||
// check syncword
|
||||
$sync = substr($DTSheader, 0, 4);
|
||||
if (($encoding = array_search($sync, self::$syncwords)) !== false) {
|
||||
|
||||
$info['dts']['raw']['magic'] = $sync;
|
||||
$this->readBinDataOffset = 32;
|
||||
|
||||
} elseif ($this->isDependencyFor('matroska')) {
|
||||
|
||||
// Matroska contains DTS without syncword encoded as raw big-endian format
|
||||
$encoding = 0;
|
||||
$this->readBinDataOffset = 0;
|
||||
|
||||
} else {
|
||||
|
||||
unset($info['fileformat']);
|
||||
return $this->error('Expecting "'.implode('| ', array_map('getid3_lib::PrintHexBytes', self::$syncwords)).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($sync).'"');
|
||||
|
||||
}
|
||||
|
||||
// decode header
|
||||
$fhBS = '';
|
||||
for ($word_offset = 0; $word_offset <= strlen($DTSheader); $word_offset += 2) {
|
||||
switch ($encoding) {
|
||||
case 0: // raw big-endian
|
||||
$fhBS .= getid3_lib::BigEndian2Bin( substr($DTSheader, $word_offset, 2) );
|
||||
break;
|
||||
case 1: // raw little-endian
|
||||
$fhBS .= getid3_lib::BigEndian2Bin(strrev(substr($DTSheader, $word_offset, 2)));
|
||||
break;
|
||||
case 2: // 14-bit big-endian
|
||||
$fhBS .= substr(getid3_lib::BigEndian2Bin( substr($DTSheader, $word_offset, 2) ), 2, 14);
|
||||
break;
|
||||
case 3: // 14-bit little-endian
|
||||
$fhBS .= substr(getid3_lib::BigEndian2Bin(strrev(substr($DTSheader, $word_offset, 2))), 2, 14);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$info['dts']['raw']['frame_type'] = $this->readBinData($fhBS, 1);
|
||||
$info['dts']['raw']['deficit_samples'] = $this->readBinData($fhBS, 5);
|
||||
$info['dts']['flags']['crc_present'] = (bool) $this->readBinData($fhBS, 1);
|
||||
$info['dts']['raw']['pcm_sample_blocks'] = $this->readBinData($fhBS, 7);
|
||||
$info['dts']['raw']['frame_byte_size'] = $this->readBinData($fhBS, 14);
|
||||
$info['dts']['raw']['channel_arrangement'] = $this->readBinData($fhBS, 6);
|
||||
$info['dts']['raw']['sample_frequency'] = $this->readBinData($fhBS, 4);
|
||||
$info['dts']['raw']['bitrate'] = $this->readBinData($fhBS, 5);
|
||||
$info['dts']['flags']['embedded_downmix'] = (bool) $this->readBinData($fhBS, 1);
|
||||
$info['dts']['flags']['dynamicrange'] = (bool) $this->readBinData($fhBS, 1);
|
||||
$info['dts']['flags']['timestamp'] = (bool) $this->readBinData($fhBS, 1);
|
||||
$info['dts']['flags']['auxdata'] = (bool) $this->readBinData($fhBS, 1);
|
||||
$info['dts']['flags']['hdcd'] = (bool) $this->readBinData($fhBS, 1);
|
||||
$info['dts']['raw']['extension_audio'] = $this->readBinData($fhBS, 3);
|
||||
$info['dts']['flags']['extended_coding'] = (bool) $this->readBinData($fhBS, 1);
|
||||
$info['dts']['flags']['audio_sync_insertion'] = (bool) $this->readBinData($fhBS, 1);
|
||||
$info['dts']['raw']['lfe_effects'] = $this->readBinData($fhBS, 2);
|
||||
$info['dts']['flags']['predictor_history'] = (bool) $this->readBinData($fhBS, 1);
|
||||
if ($info['dts']['flags']['crc_present']) {
|
||||
$info['dts']['raw']['crc16'] = $this->readBinData($fhBS, 16);
|
||||
}
|
||||
$info['dts']['flags']['mri_perfect_reconst'] = (bool) $this->readBinData($fhBS, 1);
|
||||
$info['dts']['raw']['encoder_soft_version'] = $this->readBinData($fhBS, 4);
|
||||
$info['dts']['raw']['copy_history'] = $this->readBinData($fhBS, 2);
|
||||
$info['dts']['raw']['bits_per_sample'] = $this->readBinData($fhBS, 2);
|
||||
$info['dts']['flags']['surround_es'] = (bool) $this->readBinData($fhBS, 1);
|
||||
$info['dts']['flags']['front_sum_diff'] = (bool) $this->readBinData($fhBS, 1);
|
||||
$info['dts']['flags']['surround_sum_diff'] = (bool) $this->readBinData($fhBS, 1);
|
||||
$info['dts']['raw']['dialog_normalization'] = $this->readBinData($fhBS, 4);
|
||||
|
||||
|
||||
$info['dts']['bitrate'] = self::bitrateLookup($info['dts']['raw']['bitrate']);
|
||||
$info['dts']['bits_per_sample'] = self::bitPerSampleLookup($info['dts']['raw']['bits_per_sample']);
|
||||
$info['dts']['sample_rate'] = self::sampleRateLookup($info['dts']['raw']['sample_frequency']);
|
||||
$info['dts']['dialog_normalization'] = self::dialogNormalization($info['dts']['raw']['dialog_normalization'], $info['dts']['raw']['encoder_soft_version']);
|
||||
$info['dts']['flags']['lossless'] = (($info['dts']['raw']['bitrate'] == 31) ? true : false);
|
||||
$info['dts']['bitrate_mode'] = (($info['dts']['raw']['bitrate'] == 30) ? 'vbr' : 'cbr');
|
||||
$info['dts']['channels'] = self::numChannelsLookup($info['dts']['raw']['channel_arrangement']);
|
||||
$info['dts']['channel_arrangement'] = self::channelArrangementLookup($info['dts']['raw']['channel_arrangement']);
|
||||
|
||||
$info['audio']['dataformat'] = 'dts';
|
||||
$info['audio']['lossless'] = $info['dts']['flags']['lossless'];
|
||||
$info['audio']['bitrate_mode'] = $info['dts']['bitrate_mode'];
|
||||
$info['audio']['bits_per_sample'] = $info['dts']['bits_per_sample'];
|
||||
$info['audio']['sample_rate'] = $info['dts']['sample_rate'];
|
||||
$info['audio']['channels'] = $info['dts']['channels'];
|
||||
$info['audio']['bitrate'] = $info['dts']['bitrate'];
|
||||
if (isset($info['avdataend']) && !empty($info['dts']['bitrate']) && is_numeric($info['dts']['bitrate'])) {
|
||||
$info['playtime_seconds'] = ($info['avdataend'] - $info['avdataoffset']) / ($info['dts']['bitrate'] / 8);
|
||||
if (($encoding == 2) || ($encoding == 3)) {
|
||||
// 14-bit data packed into 16-bit words, so the playtime is wrong because only (14/16) of the bytes in the data portion of the file are used at the specified bitrate
|
||||
$info['playtime_seconds'] *= (14 / 16);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function readBinData($bin, $length) {
|
||||
$data = substr($bin, $this->readBinDataOffset, $length);
|
||||
$this->readBinDataOffset += $length;
|
||||
|
||||
return bindec($data);
|
||||
}
|
||||
|
||||
public static function bitrateLookup($index) {
|
||||
static $lookup = array(
|
||||
0 => 32000,
|
||||
1 => 56000,
|
||||
2 => 64000,
|
||||
3 => 96000,
|
||||
4 => 112000,
|
||||
5 => 128000,
|
||||
6 => 192000,
|
||||
7 => 224000,
|
||||
8 => 256000,
|
||||
9 => 320000,
|
||||
10 => 384000,
|
||||
11 => 448000,
|
||||
12 => 512000,
|
||||
13 => 576000,
|
||||
14 => 640000,
|
||||
15 => 768000,
|
||||
16 => 960000,
|
||||
17 => 1024000,
|
||||
18 => 1152000,
|
||||
19 => 1280000,
|
||||
20 => 1344000,
|
||||
21 => 1408000,
|
||||
22 => 1411200,
|
||||
23 => 1472000,
|
||||
24 => 1536000,
|
||||
25 => 1920000,
|
||||
26 => 2048000,
|
||||
27 => 3072000,
|
||||
28 => 3840000,
|
||||
29 => 'open',
|
||||
30 => 'variable',
|
||||
31 => 'lossless',
|
||||
);
|
||||
return (isset($lookup[$index]) ? $lookup[$index] : false);
|
||||
}
|
||||
|
||||
public static function sampleRateLookup($index) {
|
||||
static $lookup = array(
|
||||
0 => 'invalid',
|
||||
1 => 8000,
|
||||
2 => 16000,
|
||||
3 => 32000,
|
||||
4 => 'invalid',
|
||||
5 => 'invalid',
|
||||
6 => 11025,
|
||||
7 => 22050,
|
||||
8 => 44100,
|
||||
9 => 'invalid',
|
||||
10 => 'invalid',
|
||||
11 => 12000,
|
||||
12 => 24000,
|
||||
13 => 48000,
|
||||
14 => 'invalid',
|
||||
15 => 'invalid',
|
||||
);
|
||||
return (isset($lookup[$index]) ? $lookup[$index] : false);
|
||||
}
|
||||
|
||||
public static function bitPerSampleLookup($index) {
|
||||
static $lookup = array(
|
||||
0 => 16,
|
||||
1 => 20,
|
||||
2 => 24,
|
||||
3 => 24,
|
||||
);
|
||||
return (isset($lookup[$index]) ? $lookup[$index] : false);
|
||||
}
|
||||
|
||||
public static function numChannelsLookup($index) {
|
||||
switch ($index) {
|
||||
case 0:
|
||||
return 1;
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
return 2;
|
||||
break;
|
||||
case 5:
|
||||
case 6:
|
||||
return 3;
|
||||
break;
|
||||
case 7:
|
||||
case 8:
|
||||
return 4;
|
||||
break;
|
||||
case 9:
|
||||
return 5;
|
||||
break;
|
||||
case 10:
|
||||
case 11:
|
||||
case 12:
|
||||
return 6;
|
||||
break;
|
||||
case 13:
|
||||
return 7;
|
||||
break;
|
||||
case 14:
|
||||
case 15:
|
||||
return 8;
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function channelArrangementLookup($index) {
|
||||
static $lookup = array(
|
||||
0 => 'A',
|
||||
1 => 'A + B (dual mono)',
|
||||
2 => 'L + R (stereo)',
|
||||
3 => '(L+R) + (L-R) (sum-difference)',
|
||||
4 => 'LT + RT (left and right total)',
|
||||
5 => 'C + L + R',
|
||||
6 => 'L + R + S',
|
||||
7 => 'C + L + R + S',
|
||||
8 => 'L + R + SL + SR',
|
||||
9 => 'C + L + R + SL + SR',
|
||||
10 => 'CL + CR + L + R + SL + SR',
|
||||
11 => 'C + L + R+ LR + RR + OV',
|
||||
12 => 'CF + CR + LF + RF + LR + RR',
|
||||
13 => 'CL + C + CR + L + R + SL + SR',
|
||||
14 => 'CL + CR + L + R + SL1 + SL2 + SR1 + SR2',
|
||||
15 => 'CL + C+ CR + L + R + SL + S + SR',
|
||||
);
|
||||
return (isset($lookup[$index]) ? $lookup[$index] : 'user-defined');
|
||||
}
|
||||
|
||||
public static function dialogNormalization($index, $version) {
|
||||
switch ($version) {
|
||||
case 7:
|
||||
return 0 - $index;
|
||||
break;
|
||||
case 6:
|
||||
return 0 - 16 - $index;
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,442 @@
|
|||
<?php
|
||||
/////////////////////////////////////////////////////////////////
|
||||
/// getID3() by James Heinrich <info@getid3.org> //
|
||||
// available at http://getid3.sourceforge.net //
|
||||
// or http://www.getid3.org //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// See readme.txt for more details //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// //
|
||||
// module.audio.flac.php //
|
||||
// module for analyzing FLAC and OggFLAC audio files //
|
||||
// dependencies: module.audio.ogg.php //
|
||||
// ///
|
||||
/////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.ogg.php', __FILE__, true);
|
||||
|
||||
/**
|
||||
* @tutorial http://flac.sourceforge.net/format.html
|
||||
*/
|
||||
class getid3_flac extends getid3_handler
|
||||
{
|
||||
const syncword = 'fLaC';
|
||||
|
||||
public function Analyze() {
|
||||
$info = &$this->getid3->info;
|
||||
|
||||
$this->fseek($info['avdataoffset']);
|
||||
$StreamMarker = $this->fread(4);
|
||||
if ($StreamMarker != self::syncword) {
|
||||
return $this->error('Expecting "'.getid3_lib::PrintHexBytes(self::syncword).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($StreamMarker).'"');
|
||||
}
|
||||
$info['fileformat'] = 'flac';
|
||||
$info['audio']['dataformat'] = 'flac';
|
||||
$info['audio']['bitrate_mode'] = 'vbr';
|
||||
$info['audio']['lossless'] = true;
|
||||
|
||||
// parse flac container
|
||||
return $this->parseMETAdata();
|
||||
}
|
||||
|
||||
public function parseMETAdata() {
|
||||
$info = &$this->getid3->info;
|
||||
do {
|
||||
$BlockOffset = $this->ftell();
|
||||
$BlockHeader = $this->fread(4);
|
||||
$LBFBT = getid3_lib::BigEndian2Int(substr($BlockHeader, 0, 1));
|
||||
$LastBlockFlag = (bool) ($LBFBT & 0x80);
|
||||
$BlockType = ($LBFBT & 0x7F);
|
||||
$BlockLength = getid3_lib::BigEndian2Int(substr($BlockHeader, 1, 3));
|
||||
$BlockTypeText = self::metaBlockTypeLookup($BlockType);
|
||||
|
||||
if (($BlockOffset + 4 + $BlockLength) > $info['avdataend']) {
|
||||
$this->error('METADATA_BLOCK_HEADER.BLOCK_TYPE ('.$BlockTypeText.') at offset '.$BlockOffset.' extends beyond end of file');
|
||||
break;
|
||||
}
|
||||
if ($BlockLength < 1) {
|
||||
$this->error('METADATA_BLOCK_HEADER.BLOCK_LENGTH ('.$BlockLength.') at offset '.$BlockOffset.' is invalid');
|
||||
break;
|
||||
}
|
||||
|
||||
$info['flac'][$BlockTypeText]['raw'] = array();
|
||||
$BlockTypeText_raw = &$info['flac'][$BlockTypeText]['raw'];
|
||||
|
||||
$BlockTypeText_raw['offset'] = $BlockOffset;
|
||||
$BlockTypeText_raw['last_meta_block'] = $LastBlockFlag;
|
||||
$BlockTypeText_raw['block_type'] = $BlockType;
|
||||
$BlockTypeText_raw['block_type_text'] = $BlockTypeText;
|
||||
$BlockTypeText_raw['block_length'] = $BlockLength;
|
||||
if ($BlockTypeText_raw['block_type'] != 0x06) { // do not read attachment data automatically
|
||||
$BlockTypeText_raw['block_data'] = $this->fread($BlockLength);
|
||||
}
|
||||
|
||||
switch ($BlockTypeText) {
|
||||
case 'STREAMINFO': // 0x00
|
||||
if (!$this->parseSTREAMINFO($BlockTypeText_raw['block_data'])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PADDING': // 0x01
|
||||
unset($info['flac']['PADDING']); // ignore
|
||||
break;
|
||||
|
||||
case 'APPLICATION': // 0x02
|
||||
if (!$this->parseAPPLICATION($BlockTypeText_raw['block_data'])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'SEEKTABLE': // 0x03
|
||||
if (!$this->parseSEEKTABLE($BlockTypeText_raw['block_data'])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'VORBIS_COMMENT': // 0x04
|
||||
if (!$this->parseVORBIS_COMMENT($BlockTypeText_raw['block_data'])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'CUESHEET': // 0x05
|
||||
if (!$this->parseCUESHEET($BlockTypeText_raw['block_data'])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PICTURE': // 0x06
|
||||
if (!$this->parsePICTURE()) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->warning('Unhandled METADATA_BLOCK_HEADER.BLOCK_TYPE ('.$BlockType.') at offset '.$BlockOffset);
|
||||
}
|
||||
|
||||
unset($info['flac'][$BlockTypeText]['raw']);
|
||||
$info['avdataoffset'] = $this->ftell();
|
||||
}
|
||||
while ($LastBlockFlag === false);
|
||||
|
||||
// handle tags
|
||||
if (!empty($info['flac']['VORBIS_COMMENT']['comments'])) {
|
||||
$info['flac']['comments'] = $info['flac']['VORBIS_COMMENT']['comments'];
|
||||
}
|
||||
if (!empty($info['flac']['VORBIS_COMMENT']['vendor'])) {
|
||||
$info['audio']['encoder'] = str_replace('reference ', '', $info['flac']['VORBIS_COMMENT']['vendor']);
|
||||
}
|
||||
|
||||
// copy attachments to 'comments' array if nesesary
|
||||
if (isset($info['flac']['PICTURE']) && ($this->getid3->option_save_attachments !== getID3::ATTACHMENTS_NONE)) {
|
||||
foreach ($info['flac']['PICTURE'] as $entry) {
|
||||
if (!empty($entry['data'])) {
|
||||
$info['flac']['comments']['picture'][] = array('image_mime'=>$entry['image_mime'], 'data'=>$entry['data']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($info['flac']['STREAMINFO'])) {
|
||||
if (!$this->isDependencyFor('matroska')) {
|
||||
$info['flac']['compressed_audio_bytes'] = $info['avdataend'] - $info['avdataoffset'];
|
||||
}
|
||||
$info['flac']['uncompressed_audio_bytes'] = $info['flac']['STREAMINFO']['samples_stream'] * $info['flac']['STREAMINFO']['channels'] * ($info['flac']['STREAMINFO']['bits_per_sample'] / 8);
|
||||
if ($info['flac']['uncompressed_audio_bytes'] == 0) {
|
||||
return $this->error('Corrupt FLAC file: uncompressed_audio_bytes == zero');
|
||||
}
|
||||
if (!empty($info['flac']['compressed_audio_bytes'])) {
|
||||
$info['flac']['compression_ratio'] = $info['flac']['compressed_audio_bytes'] / $info['flac']['uncompressed_audio_bytes'];
|
||||
}
|
||||
}
|
||||
|
||||
// set md5_data_source - built into flac 0.5+
|
||||
if (isset($info['flac']['STREAMINFO']['audio_signature'])) {
|
||||
|
||||
if ($info['flac']['STREAMINFO']['audio_signature'] === str_repeat("\x00", 16)) {
|
||||
$this->warning('FLAC STREAMINFO.audio_signature is null (known issue with libOggFLAC)');
|
||||
}
|
||||
else {
|
||||
$info['md5_data_source'] = '';
|
||||
$md5 = $info['flac']['STREAMINFO']['audio_signature'];
|
||||
for ($i = 0; $i < strlen($md5); $i++) {
|
||||
$info['md5_data_source'] .= str_pad(dechex(ord($md5[$i])), 2, '00', STR_PAD_LEFT);
|
||||
}
|
||||
if (!preg_match('/^[0-9a-f]{32}$/', $info['md5_data_source'])) {
|
||||
unset($info['md5_data_source']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($info['flac']['STREAMINFO']['bits_per_sample'])) {
|
||||
$info['audio']['bits_per_sample'] = $info['flac']['STREAMINFO']['bits_per_sample'];
|
||||
if ($info['audio']['bits_per_sample'] == 8) {
|
||||
// special case
|
||||
// must invert sign bit on all data bytes before MD5'ing to match FLAC's calculated value
|
||||
// MD5sum calculates on unsigned bytes, but FLAC calculated MD5 on 8-bit audio data as signed
|
||||
$this->warning('FLAC calculates MD5 data strangely on 8-bit audio, so the stored md5_data_source value will not match the decoded WAV file');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function parseSTREAMINFO($BlockData) {
|
||||
$info = &$this->getid3->info;
|
||||
|
||||
$info['flac']['STREAMINFO'] = array();
|
||||
$streaminfo = &$info['flac']['STREAMINFO'];
|
||||
|
||||
$streaminfo['min_block_size'] = getid3_lib::BigEndian2Int(substr($BlockData, 0, 2));
|
||||
$streaminfo['max_block_size'] = getid3_lib::BigEndian2Int(substr($BlockData, 2, 2));
|
||||
$streaminfo['min_frame_size'] = getid3_lib::BigEndian2Int(substr($BlockData, 4, 3));
|
||||
$streaminfo['max_frame_size'] = getid3_lib::BigEndian2Int(substr($BlockData, 7, 3));
|
||||
|
||||
$SRCSBSS = getid3_lib::BigEndian2Bin(substr($BlockData, 10, 8));
|
||||
$streaminfo['sample_rate'] = getid3_lib::Bin2Dec(substr($SRCSBSS, 0, 20));
|
||||
$streaminfo['channels'] = getid3_lib::Bin2Dec(substr($SRCSBSS, 20, 3)) + 1;
|
||||
$streaminfo['bits_per_sample'] = getid3_lib::Bin2Dec(substr($SRCSBSS, 23, 5)) + 1;
|
||||
$streaminfo['samples_stream'] = getid3_lib::Bin2Dec(substr($SRCSBSS, 28, 36));
|
||||
|
||||
$streaminfo['audio_signature'] = substr($BlockData, 18, 16);
|
||||
|
||||
if (!empty($streaminfo['sample_rate'])) {
|
||||
|
||||
$info['audio']['bitrate_mode'] = 'vbr';
|
||||
$info['audio']['sample_rate'] = $streaminfo['sample_rate'];
|
||||
$info['audio']['channels'] = $streaminfo['channels'];
|
||||
$info['audio']['bits_per_sample'] = $streaminfo['bits_per_sample'];
|
||||
$info['playtime_seconds'] = $streaminfo['samples_stream'] / $streaminfo['sample_rate'];
|
||||
if ($info['playtime_seconds'] > 0) {
|
||||
if (!$this->isDependencyFor('matroska')) {
|
||||
$info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds'];
|
||||
}
|
||||
else {
|
||||
$this->warning('Cannot determine audio bitrate because total stream size is unknown');
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
return $this->error('Corrupt METAdata block: STREAMINFO');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function parseAPPLICATION($BlockData) {
|
||||
$info = &$this->getid3->info;
|
||||
|
||||
$ApplicationID = getid3_lib::BigEndian2Int(substr($BlockData, 0, 4));
|
||||
$info['flac']['APPLICATION'][$ApplicationID]['name'] = self::applicationIDLookup($ApplicationID);
|
||||
$info['flac']['APPLICATION'][$ApplicationID]['data'] = substr($BlockData, 4);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function parseSEEKTABLE($BlockData) {
|
||||
$info = &$this->getid3->info;
|
||||
|
||||
$offset = 0;
|
||||
$BlockLength = strlen($BlockData);
|
||||
$placeholderpattern = str_repeat("\xFF", 8);
|
||||
while ($offset < $BlockLength) {
|
||||
$SampleNumberString = substr($BlockData, $offset, 8);
|
||||
$offset += 8;
|
||||
if ($SampleNumberString == $placeholderpattern) {
|
||||
|
||||
// placeholder point
|
||||
getid3_lib::safe_inc($info['flac']['SEEKTABLE']['placeholders'], 1);
|
||||
$offset += 10;
|
||||
|
||||
} else {
|
||||
|
||||
$SampleNumber = getid3_lib::BigEndian2Int($SampleNumberString);
|
||||
$info['flac']['SEEKTABLE'][$SampleNumber]['offset'] = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8));
|
||||
$offset += 8;
|
||||
$info['flac']['SEEKTABLE'][$SampleNumber]['samples'] = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 2));
|
||||
$offset += 2;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function parseVORBIS_COMMENT($BlockData) {
|
||||
$info = &$this->getid3->info;
|
||||
|
||||
$getid3_ogg = new getid3_ogg($this->getid3);
|
||||
if ($this->isDependencyFor('matroska')) {
|
||||
$getid3_ogg->setStringMode($this->data_string);
|
||||
}
|
||||
$getid3_ogg->ParseVorbisComments();
|
||||
if (isset($info['ogg'])) {
|
||||
unset($info['ogg']['comments_raw']);
|
||||
$info['flac']['VORBIS_COMMENT'] = $info['ogg'];
|
||||
unset($info['ogg']);
|
||||
}
|
||||
|
||||
unset($getid3_ogg);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function parseCUESHEET($BlockData) {
|
||||
$info = &$this->getid3->info;
|
||||
$offset = 0;
|
||||
$info['flac']['CUESHEET']['media_catalog_number'] = trim(substr($BlockData, $offset, 128), "\0");
|
||||
$offset += 128;
|
||||
$info['flac']['CUESHEET']['lead_in_samples'] = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8));
|
||||
$offset += 8;
|
||||
$info['flac']['CUESHEET']['flags']['is_cd'] = (bool) (getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1)) & 0x80);
|
||||
$offset += 1;
|
||||
|
||||
$offset += 258; // reserved
|
||||
|
||||
$info['flac']['CUESHEET']['number_tracks'] = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
|
||||
$offset += 1;
|
||||
|
||||
for ($track = 0; $track < $info['flac']['CUESHEET']['number_tracks']; $track++) {
|
||||
$TrackSampleOffset = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8));
|
||||
$offset += 8;
|
||||
$TrackNumber = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
|
||||
$offset += 1;
|
||||
|
||||
$info['flac']['CUESHEET']['tracks'][$TrackNumber]['sample_offset'] = $TrackSampleOffset;
|
||||
|
||||
$info['flac']['CUESHEET']['tracks'][$TrackNumber]['isrc'] = substr($BlockData, $offset, 12);
|
||||
$offset += 12;
|
||||
|
||||
$TrackFlagsRaw = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
|
||||
$offset += 1;
|
||||
$info['flac']['CUESHEET']['tracks'][$TrackNumber]['flags']['is_audio'] = (bool) ($TrackFlagsRaw & 0x80);
|
||||
$info['flac']['CUESHEET']['tracks'][$TrackNumber]['flags']['pre_emphasis'] = (bool) ($TrackFlagsRaw & 0x40);
|
||||
|
||||
$offset += 13; // reserved
|
||||
|
||||
$info['flac']['CUESHEET']['tracks'][$TrackNumber]['index_points'] = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
|
||||
$offset += 1;
|
||||
|
||||
for ($index = 0; $index < $info['flac']['CUESHEET']['tracks'][$TrackNumber]['index_points']; $index++) {
|
||||
$IndexSampleOffset = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8));
|
||||
$offset += 8;
|
||||
$IndexNumber = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
|
||||
$offset += 1;
|
||||
|
||||
$offset += 3; // reserved
|
||||
|
||||
$info['flac']['CUESHEET']['tracks'][$TrackNumber]['indexes'][$IndexNumber] = $IndexSampleOffset;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse METADATA_BLOCK_PICTURE flac structure and extract attachment
|
||||
* External usage: audio.ogg
|
||||
*/
|
||||
public function parsePICTURE() {
|
||||
$info = &$this->getid3->info;
|
||||
|
||||
$picture['typeid'] = getid3_lib::BigEndian2Int($this->fread(4));
|
||||
$picture['type'] = self::pictureTypeLookup($picture['typeid']);
|
||||
$picture['image_mime'] = $this->fread(getid3_lib::BigEndian2Int($this->fread(4)));
|
||||
$descr_length = getid3_lib::BigEndian2Int($this->fread(4));
|
||||
if ($descr_length) {
|
||||
$picture['description'] = $this->fread($descr_length);
|
||||
}
|
||||
$picture['width'] = getid3_lib::BigEndian2Int($this->fread(4));
|
||||
$picture['height'] = getid3_lib::BigEndian2Int($this->fread(4));
|
||||
$picture['color_depth'] = getid3_lib::BigEndian2Int($this->fread(4));
|
||||
$picture['colors_indexed'] = getid3_lib::BigEndian2Int($this->fread(4));
|
||||
$data_length = getid3_lib::BigEndian2Int($this->fread(4));
|
||||
|
||||
if ($picture['image_mime'] == '-->') {
|
||||
$picture['data'] = $this->fread($data_length);
|
||||
} else {
|
||||
$picture['data'] = $this->saveAttachment(
|
||||
str_replace('/', '_', $picture['type']).'_'.$this->ftell(),
|
||||
$this->ftell(),
|
||||
$data_length,
|
||||
$picture['image_mime']);
|
||||
}
|
||||
|
||||
$info['flac']['PICTURE'][] = $picture;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function metaBlockTypeLookup($blocktype) {
|
||||
static $lookup = array(
|
||||
0 => 'STREAMINFO',
|
||||
1 => 'PADDING',
|
||||
2 => 'APPLICATION',
|
||||
3 => 'SEEKTABLE',
|
||||
4 => 'VORBIS_COMMENT',
|
||||
5 => 'CUESHEET',
|
||||
6 => 'PICTURE',
|
||||
);
|
||||
return (isset($lookup[$blocktype]) ? $lookup[$blocktype] : 'reserved');
|
||||
}
|
||||
|
||||
public static function applicationIDLookup($applicationid) {
|
||||
// http://flac.sourceforge.net/id.html
|
||||
static $lookup = array(
|
||||
0x41544348 => 'FlacFile', // "ATCH"
|
||||
0x42534F4C => 'beSolo', // "BSOL"
|
||||
0x42554753 => 'Bugs Player', // "BUGS"
|
||||
0x43756573 => 'GoldWave cue points (specification)', // "Cues"
|
||||
0x46696361 => 'CUE Splitter', // "Fica"
|
||||
0x46746F6C => 'flac-tools', // "Ftol"
|
||||
0x4D4F5442 => 'MOTB MetaCzar', // "MOTB"
|
||||
0x4D505345 => 'MP3 Stream Editor', // "MPSE"
|
||||
0x4D754D4C => 'MusicML: Music Metadata Language', // "MuML"
|
||||
0x52494646 => 'Sound Devices RIFF chunk storage', // "RIFF"
|
||||
0x5346464C => 'Sound Font FLAC', // "SFFL"
|
||||
0x534F4E59 => 'Sony Creative Software', // "SONY"
|
||||
0x5351455A => 'flacsqueeze', // "SQEZ"
|
||||
0x54745776 => 'TwistedWave', // "TtWv"
|
||||
0x55495453 => 'UITS Embedding tools', // "UITS"
|
||||
0x61696666 => 'FLAC AIFF chunk storage', // "aiff"
|
||||
0x696D6167 => 'flac-image application for storing arbitrary files in APPLICATION metadata blocks', // "imag"
|
||||
0x7065656D => 'Parseable Embedded Extensible Metadata (specification)', // "peem"
|
||||
0x71667374 => 'QFLAC Studio', // "qfst"
|
||||
0x72696666 => 'FLAC RIFF chunk storage', // "riff"
|
||||
0x74756E65 => 'TagTuner', // "tune"
|
||||
0x78626174 => 'XBAT', // "xbat"
|
||||
0x786D6364 => 'xmcd', // "xmcd"
|
||||
);
|
||||
return (isset($lookup[$applicationid]) ? $lookup[$applicationid] : 'reserved');
|
||||
}
|
||||
|
||||
public static function pictureTypeLookup($type_id) {
|
||||
static $lookup = array (
|
||||
0 => 'Other',
|
||||
1 => '32x32 pixels \'file icon\' (PNG only)',
|
||||
2 => 'Other file icon',
|
||||
3 => 'Cover (front)',
|
||||
4 => 'Cover (back)',
|
||||
5 => 'Leaflet page',
|
||||
6 => 'Media (e.g. label side of CD)',
|
||||
7 => 'Lead artist/lead performer/soloist',
|
||||
8 => 'Artist/performer',
|
||||
9 => 'Conductor',
|
||||
10 => 'Band/Orchestra',
|
||||
11 => 'Composer',
|
||||
12 => 'Lyricist/text writer',
|
||||
13 => 'Recording Location',
|
||||
14 => 'During recording',
|
||||
15 => 'During performance',
|
||||
16 => 'Movie/video screen capture',
|
||||
17 => 'A bright coloured fish',
|
||||
18 => 'Illustration',
|
||||
19 => 'Band/artist logotype',
|
||||
20 => 'Publisher/Studio logotype',
|
||||
);
|
||||
return (isset($lookup[$type_id]) ? $lookup[$type_id] : 'reserved');
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,671 @@
|
|||
<?php
|
||||
/////////////////////////////////////////////////////////////////
|
||||
/// getID3() by James Heinrich <info@getid3.org> //
|
||||
// available at http://getid3.sourceforge.net //
|
||||
// or http://www.getid3.org //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// See readme.txt for more details //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// //
|
||||
// module.audio.ogg.php //
|
||||
// module for analyzing Ogg Vorbis, OggFLAC and Speex files //
|
||||
// dependencies: module.audio.flac.php //
|
||||
// ///
|
||||
/////////////////////////////////////////////////////////////////
|
||||
|
||||
getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.flac.php', __FILE__, true);
|
||||
|
||||
class getid3_ogg extends getid3_handler
|
||||
{
|
||||
// http://xiph.org/vorbis/doc/Vorbis_I_spec.html
|
||||
public function Analyze() {
|
||||
$info = &$this->getid3->info;
|
||||
|
||||
$info['fileformat'] = 'ogg';
|
||||
|
||||
// Warn about illegal tags - only vorbiscomments are allowed
|
||||
if (isset($info['id3v2'])) {
|
||||
$info['warning'][] = 'Illegal ID3v2 tag present.';
|
||||
}
|
||||
if (isset($info['id3v1'])) {
|
||||
$info['warning'][] = 'Illegal ID3v1 tag present.';
|
||||
}
|
||||
if (isset($info['ape'])) {
|
||||
$info['warning'][] = 'Illegal APE tag present.';
|
||||
}
|
||||
|
||||
|
||||
// Page 1 - Stream Header
|
||||
|
||||
$this->fseek($info['avdataoffset']);
|
||||
|
||||
$oggpageinfo = $this->ParseOggPageHeader();
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
|
||||
|
||||
if ($this->ftell() >= $this->getid3->fread_buffer_size()) {
|
||||
$info['error'][] = 'Could not find start of Ogg page in the first '.$this->getid3->fread_buffer_size().' bytes (this might not be an Ogg-Vorbis file?)';
|
||||
unset($info['fileformat']);
|
||||
unset($info['ogg']);
|
||||
return false;
|
||||
}
|
||||
|
||||
$filedata = $this->fread($oggpageinfo['page_length']);
|
||||
$filedataoffset = 0;
|
||||
|
||||
if (substr($filedata, 0, 4) == 'fLaC') {
|
||||
|
||||
$info['audio']['dataformat'] = 'flac';
|
||||
$info['audio']['bitrate_mode'] = 'vbr';
|
||||
$info['audio']['lossless'] = true;
|
||||
|
||||
} elseif (substr($filedata, 1, 6) == 'vorbis') {
|
||||
|
||||
$this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
|
||||
|
||||
} elseif (substr($filedata, 0, 8) == 'Speex ') {
|
||||
|
||||
// http://www.speex.org/manual/node10.html
|
||||
|
||||
$info['audio']['dataformat'] = 'speex';
|
||||
$info['mime_type'] = 'audio/speex';
|
||||
$info['audio']['bitrate_mode'] = 'abr';
|
||||
$info['audio']['lossless'] = false;
|
||||
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_string'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'Speex '
|
||||
$filedataoffset += 8;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version'] = substr($filedata, $filedataoffset, 20);
|
||||
$filedataoffset += 20;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version_id'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['header_size'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode_bitstream_version'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['bitrate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['framesize'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['frames_per_packet'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['extra_headers'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved1'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved2'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
|
||||
$info['speex']['speex_version'] = trim($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']);
|
||||
$info['speex']['sample_rate'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'];
|
||||
$info['speex']['channels'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'];
|
||||
$info['speex']['vbr'] = (bool) $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'];
|
||||
$info['speex']['band_type'] = $this->SpeexBandModeLookup($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']);
|
||||
|
||||
$info['audio']['sample_rate'] = $info['speex']['sample_rate'];
|
||||
$info['audio']['channels'] = $info['speex']['channels'];
|
||||
if ($info['speex']['vbr']) {
|
||||
$info['audio']['bitrate_mode'] = 'vbr';
|
||||
}
|
||||
|
||||
|
||||
} elseif (substr($filedata, 0, 8) == "fishead\x00") {
|
||||
|
||||
// Ogg Skeleton version 3.0 Format Specification
|
||||
// http://xiph.org/ogg/doc/skeleton.html
|
||||
$filedataoffset += 8;
|
||||
$info['ogg']['skeleton']['fishead']['raw']['version_major'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
|
||||
$filedataoffset += 2;
|
||||
$info['ogg']['skeleton']['fishead']['raw']['version_minor'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
|
||||
$filedataoffset += 2;
|
||||
$info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
|
||||
$filedataoffset += 8;
|
||||
$info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
|
||||
$filedataoffset += 8;
|
||||
$info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
|
||||
$filedataoffset += 8;
|
||||
$info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
|
||||
$filedataoffset += 8;
|
||||
$info['ogg']['skeleton']['fishead']['raw']['utc'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 20));
|
||||
$filedataoffset += 20;
|
||||
|
||||
$info['ogg']['skeleton']['fishead']['version'] = $info['ogg']['skeleton']['fishead']['raw']['version_major'].'.'.$info['ogg']['skeleton']['fishead']['raw']['version_minor'];
|
||||
$info['ogg']['skeleton']['fishead']['presentationtime'] = $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'];
|
||||
$info['ogg']['skeleton']['fishead']['basetime'] = $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'];
|
||||
$info['ogg']['skeleton']['fishead']['utc'] = $info['ogg']['skeleton']['fishead']['raw']['utc'];
|
||||
|
||||
|
||||
$counter = 0;
|
||||
do {
|
||||
$oggpageinfo = $this->ParseOggPageHeader();
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno'].'.'.$counter++] = $oggpageinfo;
|
||||
$filedata = $this->fread($oggpageinfo['page_length']);
|
||||
$this->fseek($oggpageinfo['page_end_offset']);
|
||||
|
||||
if (substr($filedata, 0, 8) == "fisbone\x00") {
|
||||
|
||||
$filedataoffset = 8;
|
||||
$info['ogg']['skeleton']['fisbone']['raw']['message_header_offset'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['skeleton']['fisbone']['raw']['serial_number'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['skeleton']['fisbone']['raw']['number_header_packets'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['skeleton']['fisbone']['raw']['granulerate_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
|
||||
$filedataoffset += 8;
|
||||
$info['ogg']['skeleton']['fisbone']['raw']['granulerate_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
|
||||
$filedataoffset += 8;
|
||||
$info['ogg']['skeleton']['fisbone']['raw']['basegranule'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
|
||||
$filedataoffset += 8;
|
||||
$info['ogg']['skeleton']['fisbone']['raw']['preroll'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['skeleton']['fisbone']['raw']['granuleshift'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
|
||||
$filedataoffset += 1;
|
||||
$info['ogg']['skeleton']['fisbone']['raw']['padding'] = substr($filedata, $filedataoffset, 3);
|
||||
$filedataoffset += 3;
|
||||
|
||||
} elseif (substr($filedata, 1, 6) == 'theora') {
|
||||
|
||||
$info['video']['dataformat'] = 'theora';
|
||||
$info['error'][] = 'Ogg Theora not correctly handled in this version of getID3 ['.$this->getid3->version().']';
|
||||
//break;
|
||||
|
||||
} elseif (substr($filedata, 1, 6) == 'vorbis') {
|
||||
|
||||
$this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
|
||||
|
||||
} else {
|
||||
$info['error'][] = 'unexpected';
|
||||
//break;
|
||||
}
|
||||
//} while ($oggpageinfo['page_seqno'] == 0);
|
||||
} while (($oggpageinfo['page_seqno'] == 0) && (substr($filedata, 0, 8) != "fisbone\x00"));
|
||||
|
||||
$this->fseek($oggpageinfo['page_start_offset']);
|
||||
|
||||
$info['error'][] = 'Ogg Skeleton not correctly handled in this version of getID3 ['.$this->getid3->version().']';
|
||||
//return false;
|
||||
|
||||
} else {
|
||||
|
||||
$info['error'][] = 'Expecting either "Speex " or "vorbis" identifier strings, found "'.substr($filedata, 0, 8).'"';
|
||||
unset($info['ogg']);
|
||||
unset($info['mime_type']);
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
// Page 2 - Comment Header
|
||||
$oggpageinfo = $this->ParseOggPageHeader();
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
|
||||
|
||||
switch ($info['audio']['dataformat']) {
|
||||
case 'vorbis':
|
||||
$filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, 0, 1));
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, 1, 6); // hard-coded to 'vorbis'
|
||||
|
||||
$this->ParseVorbisComments();
|
||||
break;
|
||||
|
||||
case 'flac':
|
||||
$flac = new getid3_flac($this->getid3);
|
||||
if (!$flac->parseMETAdata()) {
|
||||
$info['error'][] = 'Failed to parse FLAC headers';
|
||||
return false;
|
||||
}
|
||||
unset($flac);
|
||||
break;
|
||||
|
||||
case 'speex':
|
||||
$this->fseek($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length'], SEEK_CUR);
|
||||
$this->ParseVorbisComments();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// Last Page - Number of Samples
|
||||
if (!getid3_lib::intValueSupported($info['avdataend'])) {
|
||||
|
||||
$info['warning'][] = 'Unable to parse Ogg end chunk file (PHP does not support file operations beyond '.round(PHP_INT_MAX / 1073741824).'GB)';
|
||||
|
||||
} else {
|
||||
|
||||
$this->fseek(max($info['avdataend'] - $this->getid3->fread_buffer_size(), 0));
|
||||
$LastChunkOfOgg = strrev($this->fread($this->getid3->fread_buffer_size()));
|
||||
if ($LastOggSpostion = strpos($LastChunkOfOgg, 'SggO')) {
|
||||
$this->fseek($info['avdataend'] - ($LastOggSpostion + strlen('SggO')));
|
||||
$info['avdataend'] = $this->ftell();
|
||||
$info['ogg']['pageheader']['eos'] = $this->ParseOggPageHeader();
|
||||
$info['ogg']['samples'] = $info['ogg']['pageheader']['eos']['pcm_abs_position'];
|
||||
if ($info['ogg']['samples'] == 0) {
|
||||
$info['error'][] = 'Corrupt Ogg file: eos.number of samples == zero';
|
||||
return false;
|
||||
}
|
||||
if (!empty($info['audio']['sample_rate'])) {
|
||||
$info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['ogg']['samples'] / $info['audio']['sample_rate']);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!empty($info['ogg']['bitrate_average'])) {
|
||||
$info['audio']['bitrate'] = $info['ogg']['bitrate_average'];
|
||||
} elseif (!empty($info['ogg']['bitrate_nominal'])) {
|
||||
$info['audio']['bitrate'] = $info['ogg']['bitrate_nominal'];
|
||||
} elseif (!empty($info['ogg']['bitrate_min']) && !empty($info['ogg']['bitrate_max'])) {
|
||||
$info['audio']['bitrate'] = ($info['ogg']['bitrate_min'] + $info['ogg']['bitrate_max']) / 2;
|
||||
}
|
||||
if (isset($info['audio']['bitrate']) && !isset($info['playtime_seconds'])) {
|
||||
if ($info['audio']['bitrate'] == 0) {
|
||||
$info['error'][] = 'Corrupt Ogg file: bitrate_audio == zero';
|
||||
return false;
|
||||
}
|
||||
$info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $info['audio']['bitrate']);
|
||||
}
|
||||
|
||||
if (isset($info['ogg']['vendor'])) {
|
||||
$info['audio']['encoder'] = preg_replace('/^Encoded with /', '', $info['ogg']['vendor']);
|
||||
|
||||
// Vorbis only
|
||||
if ($info['audio']['dataformat'] == 'vorbis') {
|
||||
|
||||
// Vorbis 1.0 starts with Xiph.Org
|
||||
if (preg_match('/^Xiph.Org/', $info['audio']['encoder'])) {
|
||||
|
||||
if ($info['audio']['bitrate_mode'] == 'abr') {
|
||||
|
||||
// Set -b 128 on abr files
|
||||
$info['audio']['encoder_options'] = '-b '.round($info['ogg']['bitrate_nominal'] / 1000);
|
||||
|
||||
} elseif (($info['audio']['bitrate_mode'] == 'vbr') && ($info['audio']['channels'] == 2) && ($info['audio']['sample_rate'] >= 44100) && ($info['audio']['sample_rate'] <= 48000)) {
|
||||
// Set -q N on vbr files
|
||||
$info['audio']['encoder_options'] = '-q '.$this->get_quality_from_nominal_bitrate($info['ogg']['bitrate_nominal']);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($info['audio']['encoder_options']) && !empty($info['ogg']['bitrate_nominal'])) {
|
||||
$info['audio']['encoder_options'] = 'Nominal bitrate: '.intval(round($info['ogg']['bitrate_nominal'] / 1000)).'kbps';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
|
||||
$info = &$this->getid3->info;
|
||||
$info['audio']['dataformat'] = 'vorbis';
|
||||
$info['audio']['lossless'] = false;
|
||||
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
|
||||
$filedataoffset += 1;
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, $filedataoffset, 6); // hard-coded to 'vorbis'
|
||||
$filedataoffset += 6;
|
||||
$info['ogg']['bitstreamversion'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['numberofchannels'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
|
||||
$filedataoffset += 1;
|
||||
$info['audio']['channels'] = $info['ogg']['numberofchannels'];
|
||||
$info['ogg']['samplerate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
if ($info['ogg']['samplerate'] == 0) {
|
||||
$info['error'][] = 'Corrupt Ogg file: sample rate == zero';
|
||||
return false;
|
||||
}
|
||||
$info['audio']['sample_rate'] = $info['ogg']['samplerate'];
|
||||
$info['ogg']['samples'] = 0; // filled in later
|
||||
$info['ogg']['bitrate_average'] = 0; // filled in later
|
||||
$info['ogg']['bitrate_max'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['bitrate_nominal'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['bitrate_min'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$info['ogg']['blocksize_small'] = pow(2, getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0x0F);
|
||||
$info['ogg']['blocksize_large'] = pow(2, (getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0xF0) >> 4);
|
||||
$info['ogg']['stop_bit'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); // must be 1, marks end of packet
|
||||
|
||||
$info['audio']['bitrate_mode'] = 'vbr'; // overridden if actually abr
|
||||
if ($info['ogg']['bitrate_max'] == 0xFFFFFFFF) {
|
||||
unset($info['ogg']['bitrate_max']);
|
||||
$info['audio']['bitrate_mode'] = 'abr';
|
||||
}
|
||||
if ($info['ogg']['bitrate_nominal'] == 0xFFFFFFFF) {
|
||||
unset($info['ogg']['bitrate_nominal']);
|
||||
}
|
||||
if ($info['ogg']['bitrate_min'] == 0xFFFFFFFF) {
|
||||
unset($info['ogg']['bitrate_min']);
|
||||
$info['audio']['bitrate_mode'] = 'abr';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function ParseOggPageHeader() {
|
||||
// http://xiph.org/ogg/vorbis/doc/framing.html
|
||||
$oggheader['page_start_offset'] = $this->ftell(); // where we started from in the file
|
||||
|
||||
$filedata = $this->fread($this->getid3->fread_buffer_size());
|
||||
$filedataoffset = 0;
|
||||
while ((substr($filedata, $filedataoffset++, 4) != 'OggS')) {
|
||||
if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3->fread_buffer_size()) {
|
||||
// should be found before here
|
||||
return false;
|
||||
}
|
||||
if ((($filedataoffset + 28) > strlen($filedata)) || (strlen($filedata) < 28)) {
|
||||
if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === false)) {
|
||||
// get some more data, unless eof, in which case fail
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
$filedataoffset += strlen('OggS') - 1; // page, delimited by 'OggS'
|
||||
|
||||
$oggheader['stream_structver'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
|
||||
$filedataoffset += 1;
|
||||
$oggheader['flags_raw'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
|
||||
$filedataoffset += 1;
|
||||
$oggheader['flags']['fresh'] = (bool) ($oggheader['flags_raw'] & 0x01); // fresh packet
|
||||
$oggheader['flags']['bos'] = (bool) ($oggheader['flags_raw'] & 0x02); // first page of logical bitstream (bos)
|
||||
$oggheader['flags']['eos'] = (bool) ($oggheader['flags_raw'] & 0x04); // last page of logical bitstream (eos)
|
||||
|
||||
$oggheader['pcm_abs_position'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
|
||||
$filedataoffset += 8;
|
||||
$oggheader['stream_serialno'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$oggheader['page_seqno'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$oggheader['page_checksum'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
|
||||
$filedataoffset += 4;
|
||||
$oggheader['page_segments'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
|
||||
$filedataoffset += 1;
|
||||
$oggheader['page_length'] = 0;
|
||||
for ($i = 0; $i < $oggheader['page_segments']; $i++) {
|
||||
$oggheader['segment_table'][$i] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
|
||||
$filedataoffset += 1;
|
||||
$oggheader['page_length'] += $oggheader['segment_table'][$i];
|
||||
}
|
||||
$oggheader['header_end_offset'] = $oggheader['page_start_offset'] + $filedataoffset;
|
||||
$oggheader['page_end_offset'] = $oggheader['header_end_offset'] + $oggheader['page_length'];
|
||||
$this->fseek($oggheader['header_end_offset']);
|
||||
|
||||
return $oggheader;
|
||||
}
|
||||
|
||||
// http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005
|
||||
public function ParseVorbisComments() {
|
||||
$info = &$this->getid3->info;
|
||||
|
||||
$OriginalOffset = $this->ftell();
|
||||
$commentdataoffset = 0;
|
||||
$VorbisCommentPage = 1;
|
||||
|
||||
switch ($info['audio']['dataformat']) {
|
||||
case 'vorbis':
|
||||
case 'speex':
|
||||
$CommentStartOffset = $info['ogg']['pageheader'][$VorbisCommentPage]['page_start_offset']; // Second Ogg page, after header block
|
||||
$this->fseek($CommentStartOffset);
|
||||
$commentdataoffset = 27 + $info['ogg']['pageheader'][$VorbisCommentPage]['page_segments'];
|
||||
$commentdata = $this->fread(self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1) + $commentdataoffset);
|
||||
|
||||
if ($info['audio']['dataformat'] == 'vorbis') {
|
||||
$commentdataoffset += (strlen('vorbis') + 1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'flac':
|
||||
$CommentStartOffset = $info['flac']['VORBIS_COMMENT']['raw']['offset'] + 4;
|
||||
$this->fseek($CommentStartOffset);
|
||||
$commentdata = $this->fread($info['flac']['VORBIS_COMMENT']['raw']['block_length']);
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
$VendorSize = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
|
||||
$commentdataoffset += 4;
|
||||
|
||||
$info['ogg']['vendor'] = substr($commentdata, $commentdataoffset, $VendorSize);
|
||||
$commentdataoffset += $VendorSize;
|
||||
|
||||
$CommentsCount = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
|
||||
$commentdataoffset += 4;
|
||||
$info['avdataoffset'] = $CommentStartOffset + $commentdataoffset;
|
||||
|
||||
$basicfields = array('TITLE', 'ARTIST', 'ALBUM', 'TRACKNUMBER', 'GENRE', 'DATE', 'DESCRIPTION', 'COMMENT');
|
||||
$ThisFileInfo_ogg_comments_raw = &$info['ogg']['comments_raw'];
|
||||
for ($i = 0; $i < $CommentsCount; $i++) {
|
||||
|
||||
$ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] = $CommentStartOffset + $commentdataoffset;
|
||||
|
||||
if ($this->ftell() < ($ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + 4)) {
|
||||
if ($oggpageinfo = $this->ParseOggPageHeader()) {
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
|
||||
|
||||
$VorbisCommentPage++;
|
||||
|
||||
// First, save what we haven't read yet
|
||||
$AsYetUnusedData = substr($commentdata, $commentdataoffset);
|
||||
|
||||
// Then take that data off the end
|
||||
$commentdata = substr($commentdata, 0, $commentdataoffset);
|
||||
|
||||
// Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
|
||||
$commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
|
||||
$commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
|
||||
|
||||
// Finally, stick the unused data back on the end
|
||||
$commentdata .= $AsYetUnusedData;
|
||||
|
||||
//$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
|
||||
$commentdata .= $this->fread($this->OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1));
|
||||
}
|
||||
|
||||
}
|
||||
$ThisFileInfo_ogg_comments_raw[$i]['size'] = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
|
||||
|
||||
// replace avdataoffset with position just after the last vorbiscomment
|
||||
$info['avdataoffset'] = $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + $ThisFileInfo_ogg_comments_raw[$i]['size'] + 4;
|
||||
|
||||
$commentdataoffset += 4;
|
||||
while ((strlen($commentdata) - $commentdataoffset) < $ThisFileInfo_ogg_comments_raw[$i]['size']) {
|
||||
if (($ThisFileInfo_ogg_comments_raw[$i]['size'] > $info['avdataend']) || ($ThisFileInfo_ogg_comments_raw[$i]['size'] < 0)) {
|
||||
$info['warning'][] = 'Invalid Ogg comment size (comment #'.$i.', claims to be '.number_format($ThisFileInfo_ogg_comments_raw[$i]['size']).' bytes) - aborting reading comments';
|
||||
break 2;
|
||||
}
|
||||
|
||||
$VorbisCommentPage++;
|
||||
|
||||
$oggpageinfo = $this->ParseOggPageHeader();
|
||||
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
|
||||
|
||||
// First, save what we haven't read yet
|
||||
$AsYetUnusedData = substr($commentdata, $commentdataoffset);
|
||||
|
||||
// Then take that data off the end
|
||||
$commentdata = substr($commentdata, 0, $commentdataoffset);
|
||||
|
||||
// Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
|
||||
$commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
|
||||
$commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
|
||||
|
||||
// Finally, stick the unused data back on the end
|
||||
$commentdata .= $AsYetUnusedData;
|
||||
|
||||
//$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
|
||||
if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) {
|
||||
$info['warning'][] = 'undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell();
|
||||
break;
|
||||
}
|
||||
$readlength = self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1);
|
||||
if ($readlength <= 0) {
|
||||
$info['warning'][] = 'invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell();
|
||||
break;
|
||||
}
|
||||
$commentdata .= $this->fread($readlength);
|
||||
|
||||
//$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset'];
|
||||
}
|
||||
$ThisFileInfo_ogg_comments_raw[$i]['offset'] = $commentdataoffset;
|
||||
$commentstring = substr($commentdata, $commentdataoffset, $ThisFileInfo_ogg_comments_raw[$i]['size']);
|
||||
$commentdataoffset += $ThisFileInfo_ogg_comments_raw[$i]['size'];
|
||||
|
||||
if (!$commentstring) {
|
||||
|
||||
// no comment?
|
||||
$info['warning'][] = 'Blank Ogg comment ['.$i.']';
|
||||
|
||||
} elseif (strstr($commentstring, '=')) {
|
||||
|
||||
$commentexploded = explode('=', $commentstring, 2);
|
||||
$ThisFileInfo_ogg_comments_raw[$i]['key'] = strtoupper($commentexploded[0]);
|
||||
$ThisFileInfo_ogg_comments_raw[$i]['value'] = (isset($commentexploded[1]) ? $commentexploded[1] : '');
|
||||
|
||||
if ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'METADATA_BLOCK_PICTURE') {
|
||||
|
||||
// http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
|
||||
// The unencoded format is that of the FLAC picture block. The fields are stored in big endian order as in FLAC, picture data is stored according to the relevant standard.
|
||||
// http://flac.sourceforge.net/format.html#metadata_block_picture
|
||||
$flac = new getid3_flac($this->getid3);
|
||||
$flac->setStringMode(base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']));
|
||||
$flac->parsePICTURE();
|
||||
$info['ogg']['comments']['picture'][] = $flac->getid3->info['flac']['PICTURE'][0];
|
||||
unset($flac);
|
||||
|
||||
} elseif ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'COVERART') {
|
||||
|
||||
$data = base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']);
|
||||
$this->notice('Found deprecated COVERART tag, it should be replaced in honor of METADATA_BLOCK_PICTURE structure');
|
||||
/** @todo use 'coverartmime' where available */
|
||||
$imageinfo = getid3_lib::GetDataImageSize($data);
|
||||
if ($imageinfo === false || !isset($imageinfo['mime'])) {
|
||||
$this->warning('COVERART vorbiscomment tag contains invalid image');
|
||||
continue;
|
||||
}
|
||||
|
||||
$ogg = new self($this->getid3);
|
||||
$ogg->setStringMode($data);
|
||||
$info['ogg']['comments']['picture'][] = array(
|
||||
'image_mime' => $imageinfo['mime'],
|
||||
'data' => $ogg->saveAttachment('coverart', 0, strlen($data), $imageinfo['mime']),
|
||||
);
|
||||
unset($ogg);
|
||||
|
||||
} else {
|
||||
|
||||
$info['ogg']['comments'][strtolower($ThisFileInfo_ogg_comments_raw[$i]['key'])][] = $ThisFileInfo_ogg_comments_raw[$i]['value'];
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
$info['warning'][] = '[known problem with CDex >= v1.40, < v1.50b7] Invalid Ogg comment name/value pair ['.$i.']: '.$commentstring;
|
||||
|
||||
}
|
||||
unset($ThisFileInfo_ogg_comments_raw[$i]);
|
||||
}
|
||||
unset($ThisFileInfo_ogg_comments_raw);
|
||||
|
||||
|
||||
// Replay Gain Adjustment
|
||||
// http://privatewww.essex.ac.uk/~djmrob/replaygain/
|
||||
if (isset($info['ogg']['comments']) && is_array($info['ogg']['comments'])) {
|
||||
foreach ($info['ogg']['comments'] as $index => $commentvalue) {
|
||||
switch ($index) {
|
||||
case 'rg_audiophile':
|
||||
case 'replaygain_album_gain':
|
||||
$info['replay_gain']['album']['adjustment'] = (double) $commentvalue[0];
|
||||
unset($info['ogg']['comments'][$index]);
|
||||
break;
|
||||
|
||||
case 'rg_radio':
|
||||
case 'replaygain_track_gain':
|
||||
$info['replay_gain']['track']['adjustment'] = (double) $commentvalue[0];
|
||||
unset($info['ogg']['comments'][$index]);
|
||||
break;
|
||||
|
||||
case 'replaygain_album_peak':
|
||||
$info['replay_gain']['album']['peak'] = (double) $commentvalue[0];
|
||||
unset($info['ogg']['comments'][$index]);
|
||||
break;
|
||||
|
||||
case 'rg_peak':
|
||||
case 'replaygain_track_peak':
|
||||
$info['replay_gain']['track']['peak'] = (double) $commentvalue[0];
|
||||
unset($info['ogg']['comments'][$index]);
|
||||
break;
|
||||
|
||||
case 'replaygain_reference_loudness':
|
||||
$info['replay_gain']['reference_volume'] = (double) $commentvalue[0];
|
||||
unset($info['ogg']['comments'][$index]);
|
||||
break;
|
||||
|
||||
default:
|
||||
// do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->fseek($OriginalOffset);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function SpeexBandModeLookup($mode) {
|
||||
static $SpeexBandModeLookup = array();
|
||||
if (empty($SpeexBandModeLookup)) {
|
||||
$SpeexBandModeLookup[0] = 'narrow';
|
||||
$SpeexBandModeLookup[1] = 'wide';
|
||||
$SpeexBandModeLookup[2] = 'ultra-wide';
|
||||
}
|
||||
return (isset($SpeexBandModeLookup[$mode]) ? $SpeexBandModeLookup[$mode] : null);
|
||||
}
|
||||
|
||||
|
||||
public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) {
|
||||
for ($i = 0; $i < $SegmentNumber; $i++) {
|
||||
$segmentlength = 0;
|
||||
foreach ($OggInfoArray['segment_table'] as $key => $value) {
|
||||
$segmentlength += $value;
|
||||
if ($value < 255) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $segmentlength;
|
||||
}
|
||||
|
||||
|
||||
public static function get_quality_from_nominal_bitrate($nominal_bitrate) {
|
||||
|
||||
// decrease precision
|
||||
$nominal_bitrate = $nominal_bitrate / 1000;
|
||||
|
||||
if ($nominal_bitrate < 128) {
|
||||
// q-1 to q4
|
||||
$qval = ($nominal_bitrate - 64) / 16;
|
||||
} elseif ($nominal_bitrate < 256) {
|
||||
// q4 to q8
|
||||
$qval = $nominal_bitrate / 32;
|
||||
} elseif ($nominal_bitrate < 320) {
|
||||
// q8 to q9
|
||||
$qval = ($nominal_bitrate + 256) / 64;
|
||||
} else {
|
||||
// q9 to q10
|
||||
$qval = ($nominal_bitrate + 1300) / 180;
|
||||
}
|
||||
//return $qval; // 5.031324
|
||||
//return intval($qval); // 5
|
||||
return round($qval, 1); // 5 or 4.9
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,370 @@
|
|||
<?php
|
||||
/////////////////////////////////////////////////////////////////
|
||||
/// getID3() by James Heinrich <info@getid3.org> //
|
||||
// available at http://getid3.sourceforge.net //
|
||||
// or http://www.getid3.org //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// See readme.txt for more details //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// //
|
||||
// module.tag.apetag.php //
|
||||
// module for analyzing APE tags //
|
||||
// dependencies: NONE //
|
||||
// ///
|
||||
/////////////////////////////////////////////////////////////////
|
||||
|
||||
class getid3_apetag extends getid3_handler
|
||||
{
|
||||
public $inline_attachments = true; // true: return full data for all attachments; false: return no data for all attachments; integer: return data for attachments <= than this; string: save as file to this directory
|
||||
public $overrideendoffset = 0;
|
||||
|
||||
public function Analyze() {
|
||||
$info = &$this->getid3->info;
|
||||
|
||||
if (!getid3_lib::intValueSupported($info['filesize'])) {
|
||||
$info['warning'][] = 'Unable to check for APEtags because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
|
||||
return false;
|
||||
}
|
||||
|
||||
$id3v1tagsize = 128;
|
||||
$apetagheadersize = 32;
|
||||
$lyrics3tagsize = 10;
|
||||
|
||||
if ($this->overrideendoffset == 0) {
|
||||
|
||||
fseek($this->getid3->fp, 0 - $id3v1tagsize - $apetagheadersize - $lyrics3tagsize, SEEK_END);
|
||||
$APEfooterID3v1 = fread($this->getid3->fp, $id3v1tagsize + $apetagheadersize + $lyrics3tagsize);
|
||||
|
||||
//if (preg_match('/APETAGEX.{24}TAG.{125}$/i', $APEfooterID3v1)) {
|
||||
if (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $id3v1tagsize - $apetagheadersize, 8) == 'APETAGEX') {
|
||||
|
||||
// APE tag found before ID3v1
|
||||
$info['ape']['tag_offset_end'] = $info['filesize'] - $id3v1tagsize;
|
||||
|
||||
//} elseif (preg_match('/APETAGEX.{24}$/i', $APEfooterID3v1)) {
|
||||
} elseif (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $apetagheadersize, 8) == 'APETAGEX') {
|
||||
|
||||
// APE tag found, no ID3v1
|
||||
$info['ape']['tag_offset_end'] = $info['filesize'];
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
fseek($this->getid3->fp, $this->overrideendoffset - $apetagheadersize, SEEK_SET);
|
||||
if (fread($this->getid3->fp, 8) == 'APETAGEX') {
|
||||
$info['ape']['tag_offset_end'] = $this->overrideendoffset;
|
||||
}
|
||||
|
||||
}
|
||||
if (!isset($info['ape']['tag_offset_end'])) {
|
||||
|
||||
// APE tag not found
|
||||
unset($info['ape']);
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
// shortcut
|
||||
$thisfile_ape = &$info['ape'];
|
||||
|
||||
fseek($this->getid3->fp, $thisfile_ape['tag_offset_end'] - $apetagheadersize, SEEK_SET);
|
||||
$APEfooterData = fread($this->getid3->fp, 32);
|
||||
if (!($thisfile_ape['footer'] = $this->parseAPEheaderFooter($APEfooterData))) {
|
||||
$info['error'][] = 'Error parsing APE footer at offset '.$thisfile_ape['tag_offset_end'];
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) {
|
||||
fseek($this->getid3->fp, $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize'] - $apetagheadersize, SEEK_SET);
|
||||
$thisfile_ape['tag_offset_start'] = ftell($this->getid3->fp);
|
||||
$APEtagData = fread($this->getid3->fp, $thisfile_ape['footer']['raw']['tagsize'] + $apetagheadersize);
|
||||
} else {
|
||||
$thisfile_ape['tag_offset_start'] = $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize'];
|
||||
fseek($this->getid3->fp, $thisfile_ape['tag_offset_start'], SEEK_SET);
|
||||
$APEtagData = fread($this->getid3->fp, $thisfile_ape['footer']['raw']['tagsize']);
|
||||
}
|
||||
$info['avdataend'] = $thisfile_ape['tag_offset_start'];
|
||||
|
||||
if (isset($info['id3v1']['tag_offset_start']) && ($info['id3v1']['tag_offset_start'] < $thisfile_ape['tag_offset_end'])) {
|
||||
$info['warning'][] = 'ID3v1 tag information ignored since it appears to be a false synch in APEtag data';
|
||||
unset($info['id3v1']);
|
||||
foreach ($info['warning'] as $key => $value) {
|
||||
if ($value == 'Some ID3v1 fields do not use NULL characters for padding') {
|
||||
unset($info['warning'][$key]);
|
||||
sort($info['warning']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$offset = 0;
|
||||
if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) {
|
||||
if ($thisfile_ape['header'] = $this->parseAPEheaderFooter(substr($APEtagData, 0, $apetagheadersize))) {
|
||||
$offset += $apetagheadersize;
|
||||
} else {
|
||||
$info['error'][] = 'Error parsing APE header at offset '.$thisfile_ape['tag_offset_start'];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// shortcut
|
||||
$info['replay_gain'] = array();
|
||||
$thisfile_replaygain = &$info['replay_gain'];
|
||||
|
||||
for ($i = 0; $i < $thisfile_ape['footer']['raw']['tag_items']; $i++) {
|
||||
$value_size = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4));
|
||||
$offset += 4;
|
||||
$item_flags = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4));
|
||||
$offset += 4;
|
||||
if (strstr(substr($APEtagData, $offset), "\x00") === 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, "\x00", $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
|
||||
case 3: // Locator (URL, filename, etc), UTF-8 encoded
|
||||
$thisfile_ape_items_current['data'] = explode("\x00", trim($thisfile_ape_items_current['data']));
|
||||
break;
|
||||
|
||||
default: // binary data
|
||||
break;
|
||||
}
|
||||
|
||||
switch (strtolower($item_key)) {
|
||||
case 'replaygain_track_gain':
|
||||
$thisfile_replaygain['track']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
|
||||
$thisfile_replaygain['track']['originator'] = 'unspecified';
|
||||
break;
|
||||
|
||||
case 'replaygain_track_peak':
|
||||
$thisfile_replaygain['track']['peak'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
|
||||
$thisfile_replaygain['track']['originator'] = 'unspecified';
|
||||
if ($thisfile_replaygain['track']['peak'] <= 0) {
|
||||
$info['warning'][] = 'ReplayGain Track peak from APEtag appears invalid: '.$thisfile_replaygain['track']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'replaygain_album_gain':
|
||||
$thisfile_replaygain['album']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
|
||||
$thisfile_replaygain['album']['originator'] = 'unspecified';
|
||||
break;
|
||||
|
||||
case 'replaygain_album_peak':
|
||||
$thisfile_replaygain['album']['peak'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
|
||||
$thisfile_replaygain['album']['originator'] = 'unspecified';
|
||||
if ($thisfile_replaygain['album']['peak'] <= 0) {
|
||||
$info['warning'][] = 'ReplayGain Album peak from APEtag appears invalid: '.$thisfile_replaygain['album']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'mp3gain_undo':
|
||||
list($mp3gain_undo_left, $mp3gain_undo_right, $mp3gain_undo_wrap) = explode(',', $thisfile_ape_items_current['data'][0]);
|
||||
$thisfile_replaygain['mp3gain']['undo_left'] = intval($mp3gain_undo_left);
|
||||
$thisfile_replaygain['mp3gain']['undo_right'] = intval($mp3gain_undo_right);
|
||||
$thisfile_replaygain['mp3gain']['undo_wrap'] = (($mp3gain_undo_wrap == 'Y') ? true : false);
|
||||
break;
|
||||
|
||||
case 'mp3gain_minmax':
|
||||
list($mp3gain_globalgain_min, $mp3gain_globalgain_max) = explode(',', $thisfile_ape_items_current['data'][0]);
|
||||
$thisfile_replaygain['mp3gain']['globalgain_track_min'] = intval($mp3gain_globalgain_min);
|
||||
$thisfile_replaygain['mp3gain']['globalgain_track_max'] = intval($mp3gain_globalgain_max);
|
||||
break;
|
||||
|
||||
case 'mp3gain_album_minmax':
|
||||
list($mp3gain_globalgain_album_min, $mp3gain_globalgain_album_max) = explode(',', $thisfile_ape_items_current['data'][0]);
|
||||
$thisfile_replaygain['mp3gain']['globalgain_album_min'] = intval($mp3gain_globalgain_album_min);
|
||||
$thisfile_replaygain['mp3gain']['globalgain_album_max'] = intval($mp3gain_globalgain_album_max);
|
||||
break;
|
||||
|
||||
case 'tracknumber':
|
||||
if (is_array($thisfile_ape_items_current['data'])) {
|
||||
foreach ($thisfile_ape_items_current['data'] as $comment) {
|
||||
$thisfile_ape['comments']['track'][] = $comment;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'cover art (artist)':
|
||||
case 'cover art (back)':
|
||||
case 'cover art (band logo)':
|
||||
case 'cover art (band)':
|
||||
case 'cover art (colored fish)':
|
||||
case 'cover art (composer)':
|
||||
case 'cover art (conductor)':
|
||||
case 'cover art (front)':
|
||||
case 'cover art (icon)':
|
||||
case 'cover art (illustration)':
|
||||
case 'cover art (lead)':
|
||||
case 'cover art (leaflet)':
|
||||
case 'cover art (lyricist)':
|
||||
case 'cover art (media)':
|
||||
case 'cover art (movie scene)':
|
||||
case 'cover art (other icon)':
|
||||
case 'cover art (other)':
|
||||
case 'cover art (performance)':
|
||||
case 'cover art (publisher logo)':
|
||||
case 'cover art (recording)':
|
||||
case 'cover art (studio)':
|
||||
// list of possible cover arts from http://taglib-sharp.sourcearchive.com/documentation/2.0.3.0-2/Ape_2Tag_8cs-source.html
|
||||
list($thisfile_ape_items_current['filename'], $thisfile_ape_items_current['data']) = explode("\x00", $thisfile_ape_items_current['data'], 2);
|
||||
$thisfile_ape_items_current['data_offset'] = $thisfile_ape_items_current['offset'] + strlen($thisfile_ape_items_current['filename']."\x00");
|
||||
$thisfile_ape_items_current['data_length'] = strlen($thisfile_ape_items_current['data']);
|
||||
|
||||
$thisfile_ape_items_current['image_mime'] = '';
|
||||
$imageinfo = array();
|
||||
$imagechunkcheck = getid3_lib::GetDataImageSize($thisfile_ape_items_current['data'], $imageinfo);
|
||||
$thisfile_ape_items_current['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]);
|
||||
|
||||
do {
|
||||
if ($this->inline_attachments === false) {
|
||||
// skip entirely
|
||||
unset($thisfile_ape_items_current['data']);
|
||||
break;
|
||||
}
|
||||
if ($this->inline_attachments === true) {
|
||||
// great
|
||||
} elseif (is_int($this->inline_attachments)) {
|
||||
if ($this->inline_attachments < $thisfile_ape_items_current['data_length']) {
|
||||
// too big, skip
|
||||
$info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' is too large to process inline ('.number_format($thisfile_ape_items_current['data_length']).' bytes)';
|
||||
unset($thisfile_ape_items_current['data']);
|
||||
break;
|
||||
}
|
||||
} elseif (is_string($this->inline_attachments)) {
|
||||
$this->inline_attachments = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->inline_attachments), DIRECTORY_SEPARATOR);
|
||||
if (!is_dir($this->inline_attachments) || !is_writable($this->inline_attachments)) {
|
||||
// cannot write, skip
|
||||
$info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$this->inline_attachments.'" (not writable)';
|
||||
unset($thisfile_ape_items_current['data']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// if we get this far, must be OK
|
||||
if (is_string($this->inline_attachments)) {
|
||||
$destination_filename = $this->inline_attachments.DIRECTORY_SEPARATOR.md5($info['filenamepath']).'_'.$thisfile_ape_items_current['data_offset'];
|
||||
if (!file_exists($destination_filename) || is_writable($destination_filename)) {
|
||||
file_put_contents($destination_filename, $thisfile_ape_items_current['data']);
|
||||
} else {
|
||||
$info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$destination_filename.'" (not writable)';
|
||||
}
|
||||
$thisfile_ape_items_current['data_filename'] = $destination_filename;
|
||||
unset($thisfile_ape_items_current['data']);
|
||||
} else {
|
||||
if (!isset($info['ape']['comments']['picture'])) {
|
||||
$info['ape']['comments']['picture'] = array();
|
||||
}
|
||||
$info['ape']['comments']['picture'][] = array('data'=>$thisfile_ape_items_current['data'], 'image_mime'=>$thisfile_ape_items_current['image_mime']);
|
||||
}
|
||||
} while (false);
|
||||
break;
|
||||
|
||||
default:
|
||||
if (is_array($thisfile_ape_items_current['data'])) {
|
||||
foreach ($thisfile_ape_items_current['data'] as $comment) {
|
||||
$thisfile_ape['comments'][strtolower($item_key)][] = $comment;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
if (empty($thisfile_replaygain)) {
|
||||
unset($info['replay_gain']);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function parseAPEheaderFooter($APEheaderFooterData) {
|
||||
// http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html
|
||||
|
||||
// shortcut
|
||||
$headerfooterinfo['raw'] = array();
|
||||
$headerfooterinfo_raw = &$headerfooterinfo['raw'];
|
||||
|
||||
$headerfooterinfo_raw['footer_tag'] = substr($APEheaderFooterData, 0, 8);
|
||||
if ($headerfooterinfo_raw['footer_tag'] != 'APETAGEX') {
|
||||
return false;
|
||||
}
|
||||
$headerfooterinfo_raw['version'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 8, 4));
|
||||
$headerfooterinfo_raw['tagsize'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 12, 4));
|
||||
$headerfooterinfo_raw['tag_items'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 16, 4));
|
||||
$headerfooterinfo_raw['global_flags'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 20, 4));
|
||||
$headerfooterinfo_raw['reserved'] = substr($APEheaderFooterData, 24, 8);
|
||||
|
||||
$headerfooterinfo['tag_version'] = $headerfooterinfo_raw['version'] / 1000;
|
||||
if ($headerfooterinfo['tag_version'] >= 2) {
|
||||
$headerfooterinfo['flags'] = $this->parseAPEtagFlags($headerfooterinfo_raw['global_flags']);
|
||||
}
|
||||
return $headerfooterinfo;
|
||||
}
|
||||
|
||||
public function parseAPEtagFlags($rawflagint) {
|
||||
// "Note: APE Tags 1.0 do not use any of the APE Tag flags.
|
||||
// All are set to zero on creation and ignored on reading."
|
||||
// http://www.uni-jena.de/~pfk/mpp/sv8/apetagflags.html
|
||||
$flags['header'] = (bool) ($rawflagint & 0x80000000);
|
||||
$flags['footer'] = (bool) ($rawflagint & 0x40000000);
|
||||
$flags['this_is_header'] = (bool) ($rawflagint & 0x20000000);
|
||||
$flags['item_contents_raw'] = ($rawflagint & 0x00000006) >> 1;
|
||||
$flags['read_only'] = (bool) ($rawflagint & 0x00000001);
|
||||
|
||||
$flags['item_contents'] = $this->APEcontentTypeFlagLookup($flags['item_contents_raw']);
|
||||
|
||||
return $flags;
|
||||
}
|
||||
|
||||
public function APEcontentTypeFlagLookup($contenttypeid) {
|
||||
static $APEcontentTypeFlagLookup = array(
|
||||
0 => 'utf-8',
|
||||
1 => 'binary',
|
||||
2 => 'external',
|
||||
3 => 'reserved'
|
||||
);
|
||||
return (isset($APEcontentTypeFlagLookup[$contenttypeid]) ? $APEcontentTypeFlagLookup[$contenttypeid] : 'invalid');
|
||||
}
|
||||
|
||||
public function APEtagItemIsUTF8Lookup($itemkey) {
|
||||
static $APEtagItemIsUTF8Lookup = array(
|
||||
'title',
|
||||
'subtitle',
|
||||
'artist',
|
||||
'album',
|
||||
'debut album',
|
||||
'publisher',
|
||||
'conductor',
|
||||
'track',
|
||||
'composer',
|
||||
'comment',
|
||||
'copyright',
|
||||
'publicationright',
|
||||
'file',
|
||||
'year',
|
||||
'record date',
|
||||
'record location',
|
||||
'genre',
|
||||
'media',
|
||||
'related',
|
||||
'isrc',
|
||||
'abstract',
|
||||
'language',
|
||||
'bibliography'
|
||||
);
|
||||
return in_array(strtolower($itemkey), $APEtagItemIsUTF8Lookup);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,359 @@
|
|||
<?php
|
||||
/////////////////////////////////////////////////////////////////
|
||||
/// getID3() by James Heinrich <info@getid3.org> //
|
||||
// available at http://getid3.sourceforge.net //
|
||||
// or http://www.getid3.org //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// See readme.txt for more details //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// //
|
||||
// module.tag.id3v1.php //
|
||||
// module for analyzing ID3v1 tags //
|
||||
// dependencies: NONE //
|
||||
// ///
|
||||
/////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
class getid3_id3v1 extends getid3_handler
|
||||
{
|
||||
|
||||
public function Analyze() {
|
||||
$info = &$this->getid3->info;
|
||||
|
||||
if (!getid3_lib::intValueSupported($info['filesize'])) {
|
||||
$info['warning'][] = 'Unable to check for ID3v1 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
|
||||
return false;
|
||||
}
|
||||
|
||||
fseek($this->getid3->fp, -256, SEEK_END);
|
||||
$preid3v1 = fread($this->getid3->fp, 128);
|
||||
$id3v1tag = fread($this->getid3->fp, 128);
|
||||
|
||||
if (substr($id3v1tag, 0, 3) == 'TAG') {
|
||||
|
||||
$info['avdataend'] = $info['filesize'] - 128;
|
||||
|
||||
$ParsedID3v1['title'] = $this->cutfield(substr($id3v1tag, 3, 30));
|
||||
$ParsedID3v1['artist'] = $this->cutfield(substr($id3v1tag, 33, 30));
|
||||
$ParsedID3v1['album'] = $this->cutfield(substr($id3v1tag, 63, 30));
|
||||
$ParsedID3v1['year'] = $this->cutfield(substr($id3v1tag, 93, 4));
|
||||
$ParsedID3v1['comment'] = substr($id3v1tag, 97, 30); // can't remove nulls yet, track detection depends on them
|
||||
$ParsedID3v1['genreid'] = ord(substr($id3v1tag, 127, 1));
|
||||
|
||||
// If second-last byte of comment field is null and last byte of comment field is non-null
|
||||
// then this is ID3v1.1 and the comment field is 28 bytes long and the 30th byte is the track number
|
||||
if (($id3v1tag{125} === "\x00") && ($id3v1tag{126} !== "\x00")) {
|
||||
$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 static function cutfield($str) {
|
||||
return trim(substr($str, 0, strcspn($str, "\x00")));
|
||||
}
|
||||
|
||||
public static function ArrayOfGenres($allowSCMPXextended=false) {
|
||||
static $GenreLookup = array(
|
||||
0 => 'Blues',
|
||||
1 => 'Classic Rock',
|
||||
2 => 'Country',
|
||||
3 => 'Dance',
|
||||
4 => 'Disco',
|
||||
5 => 'Funk',
|
||||
6 => 'Grunge',
|
||||
7 => 'Hip-Hop',
|
||||
8 => 'Jazz',
|
||||
9 => 'Metal',
|
||||
10 => 'New Age',
|
||||
11 => 'Oldies',
|
||||
12 => 'Other',
|
||||
13 => 'Pop',
|
||||
14 => 'R&B',
|
||||
15 => 'Rap',
|
||||
16 => 'Reggae',
|
||||
17 => 'Rock',
|
||||
18 => 'Techno',
|
||||
19 => 'Industrial',
|
||||
20 => 'Alternative',
|
||||
21 => 'Ska',
|
||||
22 => 'Death Metal',
|
||||
23 => 'Pranks',
|
||||
24 => 'Soundtrack',
|
||||
25 => 'Euro-Techno',
|
||||
26 => 'Ambient',
|
||||
27 => 'Trip-Hop',
|
||||
28 => 'Vocal',
|
||||
29 => 'Jazz+Funk',
|
||||
30 => 'Fusion',
|
||||
31 => 'Trance',
|
||||
32 => 'Classical',
|
||||
33 => 'Instrumental',
|
||||
34 => 'Acid',
|
||||
35 => 'House',
|
||||
36 => 'Game',
|
||||
37 => 'Sound Clip',
|
||||
38 => 'Gospel',
|
||||
39 => 'Noise',
|
||||
40 => 'Alt. Rock',
|
||||
41 => 'Bass',
|
||||
42 => 'Soul',
|
||||
43 => 'Punk',
|
||||
44 => 'Space',
|
||||
45 => 'Meditative',
|
||||
46 => 'Instrumental Pop',
|
||||
47 => 'Instrumental Rock',
|
||||
48 => 'Ethnic',
|
||||
49 => 'Gothic',
|
||||
50 => 'Darkwave',
|
||||
51 => 'Techno-Industrial',
|
||||
52 => 'Electronic',
|
||||
53 => 'Pop-Folk',
|
||||
54 => 'Eurodance',
|
||||
55 => 'Dream',
|
||||
56 => 'Southern Rock',
|
||||
57 => 'Comedy',
|
||||
58 => 'Cult',
|
||||
59 => 'Gangsta Rap',
|
||||
60 => 'Top 40',
|
||||
61 => 'Christian Rap',
|
||||
62 => 'Pop/Funk',
|
||||
63 => 'Jungle',
|
||||
64 => 'Native American',
|
||||
65 => 'Cabaret',
|
||||
66 => 'New Wave',
|
||||
67 => 'Psychedelic',
|
||||
68 => 'Rave',
|
||||
69 => 'Showtunes',
|
||||
70 => 'Trailer',
|
||||
71 => 'Lo-Fi',
|
||||
72 => 'Tribal',
|
||||
73 => 'Acid Punk',
|
||||
74 => 'Acid Jazz',
|
||||
75 => 'Polka',
|
||||
76 => 'Retro',
|
||||
77 => 'Musical',
|
||||
78 => 'Rock & Roll',
|
||||
79 => 'Hard Rock',
|
||||
80 => 'Folk',
|
||||
81 => 'Folk/Rock',
|
||||
82 => 'National Folk',
|
||||
83 => 'Swing',
|
||||
84 => 'Fast-Fusion',
|
||||
85 => 'Bebob',
|
||||
86 => 'Latin',
|
||||
87 => 'Revival',
|
||||
88 => 'Celtic',
|
||||
89 => 'Bluegrass',
|
||||
90 => 'Avantgarde',
|
||||
91 => 'Gothic Rock',
|
||||
92 => 'Progressive Rock',
|
||||
93 => 'Psychedelic Rock',
|
||||
94 => 'Symphonic Rock',
|
||||
95 => 'Slow Rock',
|
||||
96 => 'Big Band',
|
||||
97 => 'Chorus',
|
||||
98 => 'Easy Listening',
|
||||
99 => 'Acoustic',
|
||||
100 => 'Humour',
|
||||
101 => 'Speech',
|
||||
102 => 'Chanson',
|
||||
103 => 'Opera',
|
||||
104 => 'Chamber Music',
|
||||
105 => 'Sonata',
|
||||
106 => 'Symphony',
|
||||
107 => 'Booty Bass',
|
||||
108 => 'Primus',
|
||||
109 => 'Porn Groove',
|
||||
110 => 'Satire',
|
||||
111 => 'Slow Jam',
|
||||
112 => 'Club',
|
||||
113 => 'Tango',
|
||||
114 => 'Samba',
|
||||
115 => 'Folklore',
|
||||
116 => 'Ballad',
|
||||
117 => 'Power Ballad',
|
||||
118 => 'Rhythmic Soul',
|
||||
119 => 'Freestyle',
|
||||
120 => 'Duet',
|
||||
121 => 'Punk Rock',
|
||||
122 => 'Drum Solo',
|
||||
123 => 'A Cappella',
|
||||
124 => 'Euro-House',
|
||||
125 => 'Dance Hall',
|
||||
126 => 'Goa',
|
||||
127 => 'Drum & Bass',
|
||||
128 => 'Club-House',
|
||||
129 => 'Hardcore',
|
||||
130 => 'Terror',
|
||||
131 => 'Indie',
|
||||
132 => 'BritPop',
|
||||
133 => 'Negerpunk',
|
||||
134 => 'Polsk Punk',
|
||||
135 => 'Beat',
|
||||
136 => 'Christian Gangsta Rap',
|
||||
137 => 'Heavy Metal',
|
||||
138 => 'Black Metal',
|
||||
139 => 'Crossover',
|
||||
140 => 'Contemporary Christian',
|
||||
141 => 'Christian Rock',
|
||||
142 => 'Merengue',
|
||||
143 => 'Salsa',
|
||||
144 => 'Thrash Metal',
|
||||
145 => 'Anime',
|
||||
146 => 'JPop',
|
||||
147 => 'Synthpop',
|
||||
|
||||
255 => 'Unknown',
|
||||
|
||||
'CR' => 'Cover',
|
||||
'RX' => 'Remix'
|
||||
);
|
||||
|
||||
static $GenreLookupSCMPX = array();
|
||||
if ($allowSCMPXextended && empty($GenreLookupSCMPX)) {
|
||||
$GenreLookupSCMPX = $GenreLookup;
|
||||
// http://www.geocities.co.jp/SiliconValley-Oakland/3664/alittle.html#GenreExtended
|
||||
// Extended ID3v1 genres invented by SCMPX
|
||||
// Note that 255 "Japanese Anime" conflicts with standard "Unknown"
|
||||
$GenreLookupSCMPX[240] = 'Sacred';
|
||||
$GenreLookupSCMPX[241] = 'Northern Europe';
|
||||
$GenreLookupSCMPX[242] = 'Irish & Scottish';
|
||||
$GenreLookupSCMPX[243] = 'Scotland';
|
||||
$GenreLookupSCMPX[244] = 'Ethnic Europe';
|
||||
$GenreLookupSCMPX[245] = 'Enka';
|
||||
$GenreLookupSCMPX[246] = 'Children\'s Song';
|
||||
$GenreLookupSCMPX[247] = 'Japanese Sky';
|
||||
$GenreLookupSCMPX[248] = 'Japanese Heavy Rock';
|
||||
$GenreLookupSCMPX[249] = 'Japanese Doom Rock';
|
||||
$GenreLookupSCMPX[250] = 'Japanese J-POP';
|
||||
$GenreLookupSCMPX[251] = 'Japanese Seiyu';
|
||||
$GenreLookupSCMPX[252] = 'Japanese Ambient Techno';
|
||||
$GenreLookupSCMPX[253] = 'Japanese Moemoe';
|
||||
$GenreLookupSCMPX[254] = 'Japanese Tokusatsu';
|
||||
//$GenreLookupSCMPX[255] = 'Japanese Anime';
|
||||
}
|
||||
|
||||
return ($allowSCMPXextended ? $GenreLookupSCMPX : $GenreLookup);
|
||||
}
|
||||
|
||||
public static function LookupGenreName($genreid, $allowSCMPXextended=true) {
|
||||
switch ($genreid) {
|
||||
case 'RX':
|
||||
case 'CR':
|
||||
break;
|
||||
default:
|
||||
if (!is_numeric($genreid)) {
|
||||
return false;
|
||||
}
|
||||
$genreid = intval($genreid); // to handle 3 or '3' or '03'
|
||||
break;
|
||||
}
|
||||
$GenreLookup = self::ArrayOfGenres($allowSCMPXextended);
|
||||
return (isset($GenreLookup[$genreid]) ? $GenreLookup[$genreid] : false);
|
||||
}
|
||||
|
||||
public static function LookupGenreID($genre, $allowSCMPXextended=false) {
|
||||
$GenreLookup = self::ArrayOfGenres($allowSCMPXextended);
|
||||
$LowerCaseNoSpaceSearchTerm = strtolower(str_replace(' ', '', $genre));
|
||||
foreach ($GenreLookup as $key => $value) {
|
||||
if (strtolower(str_replace(' ', '', $value)) == $LowerCaseNoSpaceSearchTerm) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function StandardiseID3v1GenreName($OriginalGenre) {
|
||||
if (($GenreID = self::LookupGenreID($OriginalGenre)) !== false) {
|
||||
return self::LookupGenreName($GenreID);
|
||||
}
|
||||
return $OriginalGenre;
|
||||
}
|
||||
|
||||
public static function GenerateID3v1Tag($title, $artist, $album, $year, $genreid, $comment, $track='') {
|
||||
$ID3v1Tag = 'TAG';
|
||||
$ID3v1Tag .= str_pad(trim(substr($title, 0, 30)), 30, "\x00", STR_PAD_RIGHT);
|
||||
$ID3v1Tag .= str_pad(trim(substr($artist, 0, 30)), 30, "\x00", STR_PAD_RIGHT);
|
||||
$ID3v1Tag .= str_pad(trim(substr($album, 0, 30)), 30, "\x00", STR_PAD_RIGHT);
|
||||
$ID3v1Tag .= str_pad(trim(substr($year, 0, 4)), 4, "\x00", STR_PAD_LEFT);
|
||||
if (!empty($track) && ($track > 0) && ($track <= 255)) {
|
||||
$ID3v1Tag .= str_pad(trim(substr($comment, 0, 28)), 28, "\x00", STR_PAD_RIGHT);
|
||||
$ID3v1Tag .= "\x00";
|
||||
if (gettype($track) == 'string') {
|
||||
$track = (int) $track;
|
||||
}
|
||||
$ID3v1Tag .= chr($track);
|
||||
} else {
|
||||
$ID3v1Tag .= str_pad(trim(substr($comment, 0, 30)), 30, "\x00", STR_PAD_RIGHT);
|
||||
}
|
||||
if (($genreid < 0) || ($genreid > 147)) {
|
||||
$genreid = 255; // 'unknown' genre
|
||||
}
|
||||
switch (gettype($genreid)) {
|
||||
case 'string':
|
||||
case 'integer':
|
||||
$ID3v1Tag .= chr(intval($genreid));
|
||||
break;
|
||||
default:
|
||||
$ID3v1Tag .= chr(255); // 'unknown' genre
|
||||
break;
|
||||
}
|
||||
|
||||
return $ID3v1Tag;
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,294 @@
|
|||
<?php
|
||||
/////////////////////////////////////////////////////////////////
|
||||
/// getID3() by James Heinrich <info@getid3.org> //
|
||||
// available at http://getid3.sourceforge.net //
|
||||
// or http://www.getid3.org //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// See readme.txt for more details //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
/// //
|
||||
// module.tag.lyrics3.php //
|
||||
// module for analyzing Lyrics3 tags //
|
||||
// dependencies: module.tag.apetag.php (optional) //
|
||||
// ///
|
||||
/////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
class getid3_lyrics3 extends getid3_handler
|
||||
{
|
||||
|
||||
public function Analyze() {
|
||||
$info = &$this->getid3->info;
|
||||
|
||||
// http://www.volweb.cz/str/tags.htm
|
||||
|
||||
if (!getid3_lib::intValueSupported($info['filesize'])) {
|
||||
$info['warning'][] = 'Unable to check for Lyrics3 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
|
||||
return false;
|
||||
}
|
||||
|
||||
fseek($this->getid3->fp, (0 - 128 - 9 - 6), SEEK_END); // end - ID3v1 - "LYRICSEND" - [Lyrics3size]
|
||||
$lyrics3_id3v1 = fread($this->getid3->fp, 128 + 9 + 6);
|
||||
$lyrics3lsz = substr($lyrics3_id3v1, 0, 6); // Lyrics3size
|
||||
$lyrics3end = substr($lyrics3_id3v1, 6, 9); // LYRICSEND or LYRICS200
|
||||
$id3v1tag = substr($lyrics3_id3v1, 15, 128); // ID3v1
|
||||
|
||||
if ($lyrics3end == 'LYRICSEND') {
|
||||
// Lyrics3v1, ID3v1, no APE
|
||||
|
||||
$lyrics3size = 5100;
|
||||
$lyrics3offset = $info['filesize'] - 128 - $lyrics3size;
|
||||
$lyrics3version = 1;
|
||||
|
||||
} elseif ($lyrics3end == 'LYRICS200') {
|
||||
// Lyrics3v2, ID3v1, no APE
|
||||
|
||||
// LSZ = lyrics + 'LYRICSBEGIN'; add 6-byte size field; add 'LYRICS200'
|
||||
$lyrics3size = $lyrics3lsz + 6 + strlen('LYRICS200');
|
||||
$lyrics3offset = $info['filesize'] - 128 - $lyrics3size;
|
||||
$lyrics3version = 2;
|
||||
|
||||
} elseif (substr(strrev($lyrics3_id3v1), 0, 9) == strrev('LYRICSEND')) {
|
||||
// Lyrics3v1, no ID3v1, no APE
|
||||
|
||||
$lyrics3size = 5100;
|
||||
$lyrics3offset = $info['filesize'] - $lyrics3size;
|
||||
$lyrics3version = 1;
|
||||
$lyrics3offset = $info['filesize'] - $lyrics3size;
|
||||
|
||||
} elseif (substr(strrev($lyrics3_id3v1), 0, 9) == strrev('LYRICS200')) {
|
||||
|
||||
// Lyrics3v2, no ID3v1, no APE
|
||||
|
||||
$lyrics3size = strrev(substr(strrev($lyrics3_id3v1), 9, 6)) + 6 + strlen('LYRICS200'); // LSZ = lyrics + 'LYRICSBEGIN'; add 6-byte size field; add 'LYRICS200'
|
||||
$lyrics3offset = $info['filesize'] - $lyrics3size;
|
||||
$lyrics3version = 2;
|
||||
|
||||
} else {
|
||||
|
||||
if (isset($info['ape']['tag_offset_start']) && ($info['ape']['tag_offset_start'] > 15)) {
|
||||
|
||||
fseek($this->getid3->fp, $info['ape']['tag_offset_start'] - 15, SEEK_SET);
|
||||
$lyrics3lsz = fread($this->getid3->fp, 6);
|
||||
$lyrics3end = fread($this->getid3->fp, 9);
|
||||
|
||||
if ($lyrics3end == 'LYRICSEND') {
|
||||
// Lyrics3v1, APE, maybe ID3v1
|
||||
|
||||
$lyrics3size = 5100;
|
||||
$lyrics3offset = $info['ape']['tag_offset_start'] - $lyrics3size;
|
||||
$info['avdataend'] = $lyrics3offset;
|
||||
$lyrics3version = 1;
|
||||
$info['warning'][] = 'APE tag located after Lyrics3, will probably break Lyrics3 compatability';
|
||||
|
||||
} elseif ($lyrics3end == 'LYRICS200') {
|
||||
// Lyrics3v2, APE, maybe ID3v1
|
||||
|
||||
$lyrics3size = $lyrics3lsz + 6 + strlen('LYRICS200'); // LSZ = lyrics + 'LYRICSBEGIN'; add 6-byte size field; add 'LYRICS200'
|
||||
$lyrics3offset = $info['ape']['tag_offset_start'] - $lyrics3size;
|
||||
$lyrics3version = 2;
|
||||
$info['warning'][] = 'APE tag located after Lyrics3, will probably break Lyrics3 compatability';
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (isset($lyrics3offset)) {
|
||||
$info['avdataend'] = $lyrics3offset;
|
||||
$this->getLyrics3Data($lyrics3offset, $lyrics3version, $lyrics3size);
|
||||
|
||||
if (!isset($info['ape'])) {
|
||||
$GETID3_ERRORARRAY = &$info['warning'];
|
||||
if (getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.apetag.php', __FILE__, false)) {
|
||||
$getid3_temp = new getID3();
|
||||
$getid3_temp->openfile($this->getid3->filename);
|
||||
$getid3_apetag = new getid3_apetag($getid3_temp);
|
||||
$getid3_apetag->overrideendoffset = $info['lyrics3']['tag_offset_start'];
|
||||
$getid3_apetag->Analyze();
|
||||
if (!empty($getid3_temp->info['ape'])) {
|
||||
$info['ape'] = $getid3_temp->info['ape'];
|
||||
}
|
||||
if (!empty($getid3_temp->info['replay_gain'])) {
|
||||
$info['replay_gain'] = $getid3_temp->info['replay_gain'];
|
||||
}
|
||||
unset($getid3_temp, $getid3_apetag);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getLyrics3Data($endoffset, $version, $length) {
|
||||
// http://www.volweb.cz/str/tags.htm
|
||||
|
||||
$info = &$this->getid3->info;
|
||||
|
||||
if (!getid3_lib::intValueSupported($endoffset)) {
|
||||
$info['warning'][] = 'Unable to check for Lyrics3 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
|
||||
return false;
|
||||
}
|
||||
|
||||
fseek($this->getid3->fp, $endoffset, SEEK_SET);
|
||||
if ($length <= 0) {
|
||||
return false;
|
||||
}
|
||||
$rawdata = fread($this->getid3->fp, $length);
|
||||
|
||||
$ParsedLyrics3['raw']['lyrics3version'] = $version;
|
||||
$ParsedLyrics3['raw']['lyrics3tagsize'] = $length;
|
||||
$ParsedLyrics3['tag_offset_start'] = $endoffset;
|
||||
$ParsedLyrics3['tag_offset_end'] = $endoffset + $length - 1;
|
||||
|
||||
if (substr($rawdata, 0, 11) != 'LYRICSBEGIN') {
|
||||
if (strpos($rawdata, 'LYRICSBEGIN') !== false) {
|
||||
|
||||
$info['warning'][] = '"LYRICSBEGIN" expected at '.$endoffset.' but actually found at '.($endoffset + strpos($rawdata, 'LYRICSBEGIN')).' - this is invalid for Lyrics3 v'.$version;
|
||||
$info['avdataend'] = $endoffset + strpos($rawdata, 'LYRICSBEGIN');
|
||||
$rawdata = substr($rawdata, strpos($rawdata, 'LYRICSBEGIN'));
|
||||
$length = strlen($rawdata);
|
||||
$ParsedLyrics3['tag_offset_start'] = $info['avdataend'];
|
||||
$ParsedLyrics3['raw']['lyrics3tagsize'] = $length;
|
||||
|
||||
} else {
|
||||
|
||||
$info['error'][] = '"LYRICSBEGIN" expected at '.$endoffset.' but found "'.substr($rawdata, 0, 11).'" instead';
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
switch ($version) {
|
||||
|
||||
case 1:
|
||||
if (substr($rawdata, strlen($rawdata) - 9, 9) == 'LYRICSEND') {
|
||||
$ParsedLyrics3['raw']['LYR'] = trim(substr($rawdata, 11, strlen($rawdata) - 11 - 9));
|
||||
$this->Lyrics3LyricsTimestampParse($ParsedLyrics3);
|
||||
} else {
|
||||
$info['error'][] = '"LYRICSEND" expected at '.(ftell($this->getid3->fp) - 11 + $length - 9).' but found "'.substr($rawdata, strlen($rawdata) - 9, 9).'" instead';
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
if (substr($rawdata, strlen($rawdata) - 9, 9) == 'LYRICS200') {
|
||||
$ParsedLyrics3['raw']['unparsed'] = substr($rawdata, 11, strlen($rawdata) - 11 - 9 - 6); // LYRICSBEGIN + LYRICS200 + LSZ
|
||||
$rawdata = $ParsedLyrics3['raw']['unparsed'];
|
||||
while (strlen($rawdata) > 0) {
|
||||
$fieldname = substr($rawdata, 0, 3);
|
||||
$fieldsize = (int) substr($rawdata, 3, 5);
|
||||
$ParsedLyrics3['raw'][$fieldname] = substr($rawdata, 8, $fieldsize);
|
||||
$rawdata = substr($rawdata, 3 + 5 + $fieldsize);
|
||||
}
|
||||
|
||||
if (isset($ParsedLyrics3['raw']['IND'])) {
|
||||
$i = 0;
|
||||
$flagnames = array('lyrics', 'timestamps', 'inhibitrandom');
|
||||
foreach ($flagnames as $flagname) {
|
||||
if (strlen($ParsedLyrics3['raw']['IND']) > $i++) {
|
||||
$ParsedLyrics3['flags'][$flagname] = $this->IntString2Bool(substr($ParsedLyrics3['raw']['IND'], $i, 1 - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$fieldnametranslation = array('ETT'=>'title', 'EAR'=>'artist', 'EAL'=>'album', 'INF'=>'comment', 'AUT'=>'author');
|
||||
foreach ($fieldnametranslation as $key => $value) {
|
||||
if (isset($ParsedLyrics3['raw'][$key])) {
|
||||
$ParsedLyrics3['comments'][$value][] = trim($ParsedLyrics3['raw'][$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($ParsedLyrics3['raw']['IMG'])) {
|
||||
$imagestrings = explode("\r\n", $ParsedLyrics3['raw']['IMG']);
|
||||
foreach ($imagestrings as $key => $imagestring) {
|
||||
if (strpos($imagestring, '||') !== false) {
|
||||
$imagearray = explode('||', $imagestring);
|
||||
$ParsedLyrics3['images'][$key]['filename'] = (isset($imagearray[0]) ? $imagearray[0] : '');
|
||||
$ParsedLyrics3['images'][$key]['description'] = (isset($imagearray[1]) ? $imagearray[1] : '');
|
||||
$ParsedLyrics3['images'][$key]['timestamp'] = $this->Lyrics3Timestamp2Seconds(isset($imagearray[2]) ? $imagearray[2] : '');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($ParsedLyrics3['raw']['LYR'])) {
|
||||
$this->Lyrics3LyricsTimestampParse($ParsedLyrics3);
|
||||
}
|
||||
} else {
|
||||
$info['error'][] = '"LYRICS200" expected at '.(ftell($this->getid3->fp) - 11 + $length - 9).' but found "'.substr($rawdata, strlen($rawdata) - 9, 9).'" instead';
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$info['error'][] = 'Cannot process Lyrics3 version '.$version.' (only v1 and v2)';
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (isset($info['id3v1']['tag_offset_start']) && ($info['id3v1']['tag_offset_start'] <= $ParsedLyrics3['tag_offset_end'])) {
|
||||
$info['warning'][] = 'ID3v1 tag information ignored since it appears to be a false synch in Lyrics3 tag data';
|
||||
unset($info['id3v1']);
|
||||
foreach ($info['warning'] as $key => $value) {
|
||||
if ($value == 'Some ID3v1 fields do not use NULL characters for padding') {
|
||||
unset($info['warning'][$key]);
|
||||
sort($info['warning']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$info['lyrics3'] = $ParsedLyrics3;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function Lyrics3Timestamp2Seconds($rawtimestamp) {
|
||||
if (preg_match('#^\\[([0-9]{2}):([0-9]{2})\\]$#', $rawtimestamp, $regs)) {
|
||||
return (int) (($regs[1] * 60) + $regs[2]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function Lyrics3LyricsTimestampParse(&$Lyrics3data) {
|
||||
$lyricsarray = explode("\r\n", $Lyrics3data['raw']['LYR']);
|
||||
foreach ($lyricsarray as $key => $lyricline) {
|
||||
$regs = array();
|
||||
unset($thislinetimestamps);
|
||||
while (preg_match('#^(\\[[0-9]{2}:[0-9]{2}\\])#', $lyricline, $regs)) {
|
||||
$thislinetimestamps[] = $this->Lyrics3Timestamp2Seconds($regs[0]);
|
||||
$lyricline = str_replace($regs[0], '', $lyricline);
|
||||
}
|
||||
$notimestamplyricsarray[$key] = $lyricline;
|
||||
if (isset($thislinetimestamps) && is_array($thislinetimestamps)) {
|
||||
sort($thislinetimestamps);
|
||||
foreach ($thislinetimestamps as $timestampkey => $timestamp) {
|
||||
if (isset($Lyrics3data['synchedlyrics'][$timestamp])) {
|
||||
// timestamps only have a 1-second resolution, it's possible that multiple lines
|
||||
// could have the same timestamp, if so, append
|
||||
$Lyrics3data['synchedlyrics'][$timestamp] .= "\r\n".$lyricline;
|
||||
} else {
|
||||
$Lyrics3data['synchedlyrics'][$timestamp] = $lyricline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$Lyrics3data['unsynchedlyrics'] = implode("\r\n", $notimestamplyricsarray);
|
||||
if (isset($Lyrics3data['synchedlyrics']) && is_array($Lyrics3data['synchedlyrics'])) {
|
||||
ksort($Lyrics3data['synchedlyrics']);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function IntString2Bool($char) {
|
||||
if ($char == '1') {
|
||||
return true;
|
||||
} elseif ($char == '0') {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue