wip
This commit is contained in:
parent
194b0fc284
commit
cbae496cf8
1 changed files with 118 additions and 48 deletions
|
@ -401,6 +401,8 @@ class MP3FrameHeader {
|
||||||
|
|
||||||
public $valid = false;
|
public $valid = false;
|
||||||
public $size = 0;
|
public $size = 0;
|
||||||
|
public $samples = 0;
|
||||||
|
public $duration = 0.0;
|
||||||
|
|
||||||
public $sync;
|
public $sync;
|
||||||
public $mpeg;
|
public $mpeg;
|
||||||
|
@ -428,6 +430,33 @@ class MP3FrameHeader {
|
||||||
|
|
||||||
private const SYNC_MASK = 0x7ff;
|
private const SYNC_MASK = 0x7ff;
|
||||||
|
|
||||||
|
private static $versions = [
|
||||||
|
'MPEG-2.5',
|
||||||
|
'reserved',
|
||||||
|
'MPEG-2',
|
||||||
|
'MPEG-1',
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $layers = [
|
||||||
|
'reserved',
|
||||||
|
'III',
|
||||||
|
'II',
|
||||||
|
'I',
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $samplesPerFrame = [
|
||||||
|
// invalid / layer 3 / 2 / 1
|
||||||
|
|
||||||
|
// MPEG-2.5
|
||||||
|
[ 0, 576, 1152, 384 ],
|
||||||
|
// Reserved
|
||||||
|
[ 0, 0, 0, 0 ],
|
||||||
|
// MPEG-2
|
||||||
|
[ 0, 576, 1152, 384 ],
|
||||||
|
// MPEG-1
|
||||||
|
[ 0, 1152, 384, 384 ],
|
||||||
|
];
|
||||||
|
|
||||||
// 1s used for reserved slots to avoid exploding
|
// 1s used for reserved slots to avoid exploding
|
||||||
// in case of invalid input
|
// in case of invalid input
|
||||||
private static $sampleRates = [
|
private static $sampleRates = [
|
||||||
|
@ -442,9 +471,9 @@ class MP3FrameHeader {
|
||||||
];
|
];
|
||||||
|
|
||||||
private static $bitrates = [
|
private static $bitrates = [
|
||||||
|
// MPEG-2
|
||||||
[
|
[
|
||||||
// MPEG-2
|
// invalid layer
|
||||||
// invalid
|
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ],
|
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ],
|
||||||
// layer 3
|
// layer 3
|
||||||
[ 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 ],
|
[ 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 ],
|
||||||
|
@ -453,9 +482,9 @@ class MP3FrameHeader {
|
||||||
// layer 1
|
// layer 1
|
||||||
[ 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0 ],
|
[ 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0 ],
|
||||||
],
|
],
|
||||||
|
// MPEG-1
|
||||||
[
|
[
|
||||||
// MPEG-1
|
// invalid layer
|
||||||
// invalid
|
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ],
|
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ],
|
||||||
// layer 3
|
// layer 3
|
||||||
[ 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 192, 224, 256, 320, 0 ],
|
[ 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 192, 224, 256, 320, 0 ],
|
||||||
|
@ -475,31 +504,47 @@ class MP3FrameHeader {
|
||||||
public function __construct( $header ) {
|
public function __construct( $header ) {
|
||||||
$this->header = $header;
|
$this->header = $header;
|
||||||
|
|
||||||
$this->sync = $this->field( 'sync' ) == self::SYNC_MASK;
|
$sync = $this->field( 'sync' );
|
||||||
|
$this->sync = $sync == self::SYNC_MASK;
|
||||||
if ( $this->sync ) {
|
if ( $this->sync ) {
|
||||||
$this->mpeg = $this->field( 'mpeg' );
|
$mpeg = $this->field( 'mpeg' );
|
||||||
$this->layer = $this->field( 'layer' );
|
$this->mpeg = self::$versions[$mpeg];
|
||||||
$this->protection = $this->field( 'protection' );
|
|
||||||
|
$layer = $this->field( 'layer' );
|
||||||
|
$this->layer = self::$layers[$layer];
|
||||||
|
|
||||||
|
$protection = $this->field( 'protection' );
|
||||||
|
$this->protection = !$protection;
|
||||||
|
|
||||||
$br = $this->field( 'bitrate' );
|
$br = $this->field( 'bitrate' );
|
||||||
$this->bitrate = 1000 * self::$bitrates[$this->mpeg & 1][$this->layer][$br];
|
$this->bitrate = 1000 * self::$bitrates[$mpeg & 1][$layer][$br];
|
||||||
|
|
||||||
$sr = $this->field( 'sampleRate' );
|
$sr = $this->field( 'sampleRate' );
|
||||||
$this->sampleRate = self::$sampleRates[$this->mpeg][$sr];
|
$this->sampleRate = self::$sampleRates[$mpeg][$sr];
|
||||||
|
|
||||||
$this->padding = $this->field( 'padding' );
|
$this->padding = $this->field( 'padding' );
|
||||||
|
|
||||||
$this->valid = $this->sync;
|
if ( $this->sync ) {
|
||||||
if ( $this->bitrate == 0 ) {
|
if ( $this->bitrate == 0 ) {
|
||||||
var_dump( $this );
|
$this->valid = false;
|
||||||
echo "br: $br\n";
|
var_dump( $this );
|
||||||
//throw new Exception( "Invalid bitrate" );
|
echo "br: $br\n";
|
||||||
$this->valid = false;
|
throw new Exception( "Invalid bitrate" );
|
||||||
|
}
|
||||||
|
if ( $this->sampleRate == 1 ) {
|
||||||
|
$this->valid = false;
|
||||||
|
var_dump( $this );
|
||||||
|
echo "sr: $sr\n";
|
||||||
|
throw new Exception( "Invalid sample rate" );
|
||||||
|
}
|
||||||
|
$this->valid = true;
|
||||||
}
|
}
|
||||||
if ( $this->sampleRate == 1 ) {
|
|
||||||
var_dump( $this );
|
$this->samples = self::$samplesPerFrame[$mpeg][$layer];
|
||||||
echo "sr: $sr\n";
|
$this->duration = $this->samples / $this->sampleRate;
|
||||||
//throw new Exception( "Invalid sample rate" );
|
$nbits = $this->duration * $this->bitrate;
|
||||||
$this->valid = false;
|
$nbytes = $nbits / 8;
|
||||||
}
|
$this->size = intval( $nbytes );
|
||||||
$this->size = intval( 144.0 * $this->bitrate / $this->sampleRate );
|
|
||||||
if ( $this->protection ) {
|
if ( $this->protection ) {
|
||||||
$this->size += 2;
|
$this->size += 2;
|
||||||
}
|
}
|
||||||
|
@ -513,6 +558,7 @@ class MP3FrameHeader {
|
||||||
class MP3Reader {
|
class MP3Reader {
|
||||||
|
|
||||||
private $file;
|
private $file;
|
||||||
|
private $timestamp = 0.0;
|
||||||
|
|
||||||
public function __construct( $file ) {
|
public function __construct( $file ) {
|
||||||
$this->file = $file;
|
$this->file = $file;
|
||||||
|
@ -524,33 +570,60 @@ class MP3Reader {
|
||||||
|
|
||||||
public function readFrame() {
|
public function readFrame() {
|
||||||
while ( true ) {
|
while ( true ) {
|
||||||
// Look for the sync pattern
|
|
||||||
$start = $this->pos();
|
$start = $this->pos();
|
||||||
$bytes = fread( $this->file, 4 );
|
$lookahead = 10;
|
||||||
if ( $bytes === false || strlen( $bytes ) < 4 ) {
|
$bytes = fread( $this->file, $lookahead );
|
||||||
|
if ( $bytes === false || strlen( $bytes ) < $lookahead ) {
|
||||||
// end of file
|
// end of file
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for MP3 frame header sync pattern
|
||||||
$data = unpack( "Nval", $bytes );
|
$data = unpack( "Nval", $bytes );
|
||||||
$header = new MP3FrameHeader( $data['val'] );
|
$header = new MP3FrameHeader( $data['val'] );
|
||||||
if ( $header->valid ) {
|
if ( $header->sync ) {
|
||||||
|
$segments[] = [
|
||||||
|
'start' => $start,
|
||||||
|
'size' => $header->size,
|
||||||
|
'timestamp' => $this->timestamp,
|
||||||
|
'duration' => $header->duration,
|
||||||
|
];
|
||||||
|
$this->timestamp += $header->duration;
|
||||||
// Note we don't need the data at this time.
|
// Note we don't need the data at this time.
|
||||||
//fseek( $this->file, $start + $header->size, SEEK_SET );
|
fseek( $this->file, $start + $header->size, SEEK_SET );
|
||||||
var_dump ( $header );
|
continue;
|
||||||
fread( $this->file, $header->size - 4 );
|
|
||||||
return $header;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$x = hexdump( $bytes );
|
// ID3v2.3
|
||||||
print "BACKUP $x\n";
|
// https://web.archive.org/web/20081008034714/http://www.id3.org/id3v2.3.0
|
||||||
|
// ID3v2/file identifier "ID3"
|
||||||
if ( $header->sync && !$header->valid ) {
|
// ID3v2 version $03 00
|
||||||
$x = hexdump( $bytes );
|
// ID3v2 flags %abc00000
|
||||||
die('xxx ' . $x);
|
// ID3v2 size 4 * %0xxxxxxx
|
||||||
|
$id3 = unpack( "a3tag/nversion/Cflags/C4size", $bytes );
|
||||||
|
if ( $id3['tag'] === 'ID3' ) {
|
||||||
|
$size = $lookahead +
|
||||||
|
( $id3['size4'] |
|
||||||
|
( $id3['size3'] << 7) |
|
||||||
|
( $id3['size2'] << 14) |
|
||||||
|
( $id3['size1'] << 21) );
|
||||||
|
// For byte range purposes; count as zero duration
|
||||||
|
$segments[] = [
|
||||||
|
'start' => $start,
|
||||||
|
'size' => $size,
|
||||||
|
'timestamp' => $this->timestamp,
|
||||||
|
'duration' => 0.0,
|
||||||
|
];
|
||||||
|
fseek( $this->file, $start + $size, SEEK_SET );
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// back up 3 bytes and try again
|
$hex = hexdump( $bytes );
|
||||||
|
$safe = safestr ( $bytes );
|
||||||
|
var_dump($segments);
|
||||||
|
throw new Exception("Invalid packet at $start? $hex $safe");
|
||||||
|
|
||||||
|
// back up and try again
|
||||||
fseek( $this->file, $start + 1, SEEK_SET );
|
fseek( $this->file, $start + 1, SEEK_SET );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -567,19 +640,13 @@ function extractMP3( $filename ) {
|
||||||
$timestamp = 0.0;
|
$timestamp = 0.0;
|
||||||
while ( true ) {
|
while ( true ) {
|
||||||
$start = $reader->pos();
|
$start = $reader->pos();
|
||||||
$frame = $reader->readFrame();
|
$segment = $reader->readFrame();
|
||||||
if ( !$frame ) {
|
if ( !$segment ) {
|
||||||
return $segments;
|
return $segments;
|
||||||
}
|
}
|
||||||
$duration = 144.0 / $frame->sampleRate;
|
$segments[] = $segment;
|
||||||
$segments[] = [
|
|
||||||
'start' => $start,
|
|
||||||
'size' => $frame->size,
|
|
||||||
'timestamp' => $timestamp,
|
|
||||||
'duration' => $duration,
|
|
||||||
];
|
|
||||||
$timestamp += $duration;
|
|
||||||
}
|
}
|
||||||
|
return $segments;
|
||||||
} finally {
|
} finally {
|
||||||
fclose( $file );
|
fclose( $file );
|
||||||
}
|
}
|
||||||
|
@ -590,6 +657,9 @@ function consolidate( $target, $segments ) {
|
||||||
if ( isset( $segments['init'] ) ) {
|
if ( isset( $segments['init'] ) ) {
|
||||||
$out['init'] = $segments['init'];
|
$out['init'] = $segments['init'];
|
||||||
}
|
}
|
||||||
|
if ( count( $segments ) < 2 ) {
|
||||||
|
return $segments;
|
||||||
|
}
|
||||||
|
|
||||||
$n = count( $segments ) - 1;
|
$n = count( $segments ) - 1;
|
||||||
$start = $segments[0]['start'];
|
$start = $segments[0]['start'];
|
||||||
|
|
Loading…
Reference in a new issue