Support for decoding and encoding frames with unsynchronisation schema (ID3v2.4 only)
git-svn-id: http://php-reader.googlecode.com/svn/trunk@107 51a70ab9-7547-0410-9469-37e369ee0574
This commit is contained in:
@@ -149,10 +149,10 @@ class ID3_Frame extends ID3_Object
|
||||
$this->_identifier = substr(get_class($this), -4);
|
||||
} else {
|
||||
$this->_identifier = $this->_reader->readString8(4);
|
||||
$this->_size = $this->decodeSynchsafe32($this->_reader->readUInt32BE());
|
||||
|
||||
/* ID3v2.3.0 Flags; convert to 2.4.0 format */
|
||||
/* ID3v2.3.0 size and flags; convert flags to 2.4.0 format */
|
||||
if ($this->getOption("version", 4) < 4) {
|
||||
$this->_size = $this->_reader->readUInt32BE();
|
||||
$flags = $this->_reader->readUInt16BE();
|
||||
if (($flags & 0x8000) == 0x8000)
|
||||
$this->_flags |= self::DISCARD_ON_TAGCHANGE;
|
||||
@@ -168,11 +168,23 @@ class ID3_Frame extends ID3_Object
|
||||
$this->_flags |= self::GROUPING_IDENTITY;
|
||||
}
|
||||
|
||||
/* ID3v2.4.0 Flags */
|
||||
else
|
||||
/* ID3v2.4.0 size and flags */
|
||||
else {
|
||||
$this->_size = $this->decodeSynchsafe32($this->_reader->readUInt32BE());
|
||||
$this->_flags = $this->_reader->readUInt16BE();
|
||||
}
|
||||
|
||||
$dataLength = $this->_size;
|
||||
if ($this->hasFlag(self::DATA_LENGTH_INDICATOR)) {
|
||||
$dataLength = $this->decodeSynchsafe32($this->_reader->readUInt32BE());
|
||||
$this->_size -= 4;
|
||||
}
|
||||
$this->_data = $this->_reader->read($this->_size);
|
||||
$this->_size = $dataLength;
|
||||
|
||||
if ($this->hasFlag(self::UNSYNCHRONISATION) ||
|
||||
$this->getOption("unsyncronisation", false) === true)
|
||||
$this->_data = $this->decodeUnsynchronisation($this->_data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +239,7 @@ class ID3_Frame extends ID3_Object
|
||||
/**
|
||||
* Sets the frame raw data.
|
||||
*
|
||||
* @return string
|
||||
* @param string $data
|
||||
*/
|
||||
protected function setData($data)
|
||||
{
|
||||
@@ -263,8 +275,21 @@ class ID3_Frame extends ID3_Object
|
||||
else
|
||||
$flags = $this->_flags;
|
||||
|
||||
$size = $this->_size;
|
||||
if ($this->getOption("version", 4) < 4)
|
||||
$data = $this->_data;
|
||||
else {
|
||||
$data = $this->encodeUnsynchronisation($this->_data);
|
||||
if (($dataLength = strlen($data)) != $size) {
|
||||
$size = 4 + $dataLength;
|
||||
$data = Transform::toUInt32BE($this->encodeSynchsafe32($this->_size)) .
|
||||
$data;
|
||||
$flags |= self::DATA_LENGTH_INDICATOR | self::UNSYNCHRONISATION;
|
||||
$this->setOption("unsyncronisation", true);
|
||||
}
|
||||
}
|
||||
return Transform::toString8(substr($this->_identifier, 0, 4), 4) .
|
||||
Transform::toUInt32BE($this->encodeSynchsafe32($this->_size)) .
|
||||
Transform::toUInt16BE($flags) . $this->_data;
|
||||
Transform::toUInt32BE($this->encodeSynchsafe32($size)) .
|
||||
Transform::toUInt16BE($flags) . $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ abstract class ID3_Frame_AbstractText extends ID3_Frame
|
||||
case self::UTF16LE:
|
||||
$array = $this->_text;
|
||||
foreach ($array as &$text)
|
||||
$text = Transform::toString16($str);
|
||||
$text = Transform::toString16($text);
|
||||
$data .= Transform::toString16
|
||||
(implode("\0\0", $array), $this->_encoding == self::UTF16 ?
|
||||
Transform::MACHINE_ENDIAN_ORDER : Transform::LITTLE_ENDIAN_ORDER);
|
||||
|
||||
@@ -58,7 +58,6 @@ require_once("ID3/Timing.php");
|
||||
* a time-period is at the same time as the beat description occurs. There may
|
||||
* only be one SYTC frame in each tag.
|
||||
*
|
||||
* @todo The data could be parsed further; data samples needed
|
||||
* @package php-reader
|
||||
* @subpackage ID3
|
||||
* @author Sven Vollbehr <svollbehr@gmail.com>
|
||||
@@ -99,7 +98,7 @@ final class ID3_Frame_SYTC extends ID3_Frame
|
||||
$this->_format = Transform::fromUInt8($this->_data[$offset++]);
|
||||
while ($offset < strlen($this->_data)) {
|
||||
$tempo = Transform::fromUInt8($this->_data[$offset++]);
|
||||
if ($tempo == 0xFF)
|
||||
if ($tempo == 0xff)
|
||||
$tempo += Transform::fromUInt8($this->_data[$offset++]);
|
||||
$this->_events
|
||||
[Transform::fromUInt32BE(substr($this->_data, $offset, 4))] = $tempo;
|
||||
|
||||
@@ -89,7 +89,7 @@ final class ID3_Header extends ID3_Object
|
||||
*/
|
||||
public function __construct($reader = null, &$options = array())
|
||||
{
|
||||
parent::__construct($reader);
|
||||
parent::__construct($reader, $options);
|
||||
|
||||
if ($reader === null)
|
||||
return;
|
||||
@@ -98,8 +98,6 @@ final class ID3_Header extends ID3_Object
|
||||
$this->_reader->readInt8() + $this->_reader->readInt8() / 10;
|
||||
$this->_flags = $this->_reader->readInt8();
|
||||
$this->_size = $this->decodeSynchsafe32($this->_reader->readUInt32BE());
|
||||
|
||||
$this->setOptions($options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -72,7 +72,7 @@ abstract class ID3_Object
|
||||
public function __construct($reader = null, &$options = array())
|
||||
{
|
||||
$this->_reader = $reader;
|
||||
$this->_options = $options;
|
||||
$this->_options = &$options;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,7 +101,7 @@ abstract class ID3_Object
|
||||
*
|
||||
* @param Array $options The options array.
|
||||
*/
|
||||
public function setOptions(&$options) { $this->_options = $options; }
|
||||
public function setOptions(&$options) { $this->_options = &$options; }
|
||||
|
||||
/**
|
||||
* Sets the given option the given value.
|
||||
@@ -168,6 +168,49 @@ abstract class ID3_Object
|
||||
($val & 0x7f0000) >> 2 | ($val & 0x7f000000) >> 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the unsynchronisation scheme to the given data string.
|
||||
*
|
||||
* Whenever a false synchronisation is found within the data, one zeroed byte
|
||||
* is inserted after the first false synchronisation byte. This has the side
|
||||
* effect that all 0xff00 combinations have to be altered, so they will not
|
||||
* be affected by the decoding process. Therefore all the 0xff00 combinations
|
||||
* have to be replaced with the 0xff0000 combination during the
|
||||
* unsynchronisation.
|
||||
*
|
||||
* @param string $data The input data.
|
||||
* @return string
|
||||
*/
|
||||
protected function encodeUnsynchronisation(&$data)
|
||||
{
|
||||
$result = "";
|
||||
for ($i = 0, $j = 0; $i < strlen($data) - 1; $i++)
|
||||
if (ord($data[$i]) == 0xff &&
|
||||
((($tmp = ord($data[$i + 1])) & 0xe0) == 0xe0 || $tmp == 0x0)) {
|
||||
$result .= substr($data, $j, $i + 1 - $j) . "\0";
|
||||
$j = $i + 1;
|
||||
}
|
||||
return $result . substr($data, $j);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the unsynchronisation scheme from the given data string.
|
||||
*
|
||||
* @see encodeUnsyncronisation
|
||||
* @param string $data The input data.
|
||||
* @return string
|
||||
*/
|
||||
protected function decodeUnsynchronisation(&$data)
|
||||
{
|
||||
$result = "";
|
||||
for ($i = 0, $j = 0; $i < strlen($data) - 1; $i++)
|
||||
if (ord($data[$i]) == 0xff && ord($data[$i + 1]) == 0x0) {
|
||||
$result .= substr($data, $j, $i + 1 - $j);
|
||||
$j = $i + 2;
|
||||
}
|
||||
return $result . substr($data, $j);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits UTF-16 formatted binary data up according to null terminators
|
||||
* residing in the string, up to a given limit.
|
||||
|
||||
@@ -108,22 +108,25 @@ final class ID3v1
|
||||
private $_reader;
|
||||
|
||||
/** @var string */
|
||||
private $_filename;
|
||||
private $_filename = false;
|
||||
|
||||
/**
|
||||
* Constructs the ID3v1 class with given file. The file is not mandatory
|
||||
* argument and may be omitted. A new tag can be written to a file also by
|
||||
* giving the filename to the {@link #write} method of this class.
|
||||
*
|
||||
* @param string $filename The path to the file.
|
||||
* @param string|Reader $filename The path to the file, file descriptor of an
|
||||
* opened file, or {@link Reader} instance.
|
||||
*/
|
||||
public function __construct($filename = false)
|
||||
{
|
||||
if (($this->_filename = $filename) !== false &&
|
||||
file_exists($filename) !== false)
|
||||
$this->_reader = new Reader($filename);
|
||||
else if ($filename instanceof Reader)
|
||||
if ($filename instanceof Reader)
|
||||
$this->_reader = &$filename;
|
||||
else if ((is_string($filename) && ($this->_filename = $filename) !== false &&
|
||||
file_exists($filename) !== false) ||
|
||||
(is_resource($filename) &&
|
||||
in_array(get_resource_type($filename), array("file", "stream"))))
|
||||
$this->_reader = new Reader($filename);
|
||||
else
|
||||
return;
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ require_once("ID3/Frame.php");
|
||||
* @package php-reader
|
||||
* @subpackage ID3
|
||||
* @author Sven Vollbehr <svollbehr@gmail.com>
|
||||
* @author Ryan Butterfield <buttza@gmail.com>
|
||||
* @copyright Copyright (c) 2008 The PHP Reader Project Workgroup
|
||||
* @license http://code.google.com/p/php-reader/wiki/License New BSD License
|
||||
* @version $Rev$
|
||||
@@ -105,8 +106,8 @@ final class ID3v2
|
||||
* @todo Only limited subset of flags are processed.
|
||||
* @todo Utilize the SEEK frame and search for a footer to find the tag
|
||||
* @todo Utilize the LINK frame to fetch frames from other sources
|
||||
* @param string $filename The path to the file, file descriptor of an opened
|
||||
* file, or {@link Reader} instance.
|
||||
* @param string|Reader $filename The path to the file, file descriptor of an
|
||||
* opened file, or {@link Reader} instance.
|
||||
* @param Array $options The options array.
|
||||
*/
|
||||
public function __construct($filename = false, $options = array())
|
||||
@@ -119,25 +120,32 @@ final class ID3v2
|
||||
$this->_options = &$options;
|
||||
if ($filename === false ||
|
||||
(is_string($filename) && file_exists($filename) === false) ||
|
||||
(is_resource($filename) && get_resource_type($filename) != "file")) {
|
||||
(is_resource($filename) &&
|
||||
in_array(get_resource_type($filename), array("file", "stream")))) {
|
||||
$this->_header = new ID3_Header(null, $options);
|
||||
} else {
|
||||
if (is_string($filename) && !isset($options["readonly"]))
|
||||
$this->_filename = $filename;
|
||||
if ($filename instanceof Reader)
|
||||
$this->_reader = $filename;
|
||||
$this->_reader = &$filename;
|
||||
else
|
||||
$this->_reader = new Reader($filename);
|
||||
if ($this->_reader->readString8(3) != "ID3")
|
||||
throw new ID3_Exception
|
||||
("File does not contain ID3v2 tag: " . $filename);
|
||||
throw new ID3_Exception("File does not contain ID3v2 tag");
|
||||
|
||||
$startOffset = $this->_reader->getOffset();
|
||||
|
||||
$this->_header = new ID3_Header($this->_reader, $options);
|
||||
if ($this->_header->getVersion() < 3 || $this->_header->getVersion() > 4)
|
||||
throw new ID3_Exception
|
||||
("File does not contain ID3v2 tag of supported version: " . $filename);
|
||||
("File does not contain ID3v2 tag of supported version");
|
||||
if ($this->_header->getVersion() < 4 &&
|
||||
$this->_header->hasFlag(ID3_Header::UNSYNCHRONISATION))
|
||||
throw new ID3_Exception
|
||||
("Unsynchronisation not supported for this version of ID3v2 tag");
|
||||
unset($this->_options["unsyncronisation"]);
|
||||
if ($this->_header->hasFlag(ID3_Header::UNSYNCHRONISATION))
|
||||
$this->_options["unsyncronisation"] = true;
|
||||
if ($this->_header->hasFlag(ID3_Header::EXTENDEDHEADER))
|
||||
$this->_extendedHeader =
|
||||
new ID3_ExtendedHeader($this->_reader, $options);
|
||||
@@ -341,6 +349,11 @@ final class ID3v2
|
||||
{
|
||||
if ($filename === false && ($filename = $this->_filename) === false)
|
||||
throw new ID3_Exception("No file given to write the tag to");
|
||||
else if ($filename !== false && $this->_filename !== false &&
|
||||
realpath($filename) != realpath($this->_filename) &&
|
||||
!copy($this->_filename, $filename))
|
||||
throw new ID3_Exception("Unable to copy source to destination: " .
|
||||
realpath($this->_filename) . "->" . realpath($filename));
|
||||
|
||||
if (($fd = fopen
|
||||
($filename, file_exists($filename) ? "r+b" : "wb")) === false)
|
||||
@@ -401,7 +414,10 @@ final class ID3v2
|
||||
* @param string $name The frame identifier.
|
||||
* @return boolean
|
||||
*/
|
||||
public function __isset($name) { return isset($this->_boxes[$name]); }
|
||||
public function __isset($name)
|
||||
{
|
||||
return isset($this->_frames[strtoupper($name)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic function so that unset($obj->value) will work. This method removes
|
||||
@@ -409,7 +425,7 @@ final class ID3v2
|
||||
*
|
||||
* @param string $name The frame identifier.
|
||||
*/
|
||||
public function __unset($name) { unset($this->_boxes[$name]); }
|
||||
public function __unset($name) { unset($this->_frames[strtoupper($name)]); }
|
||||
|
||||
/**
|
||||
* Returns the tag raw data.
|
||||
@@ -418,6 +434,8 @@ final class ID3v2
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
unset($this->_options["unsyncronisation"]);
|
||||
|
||||
$data = "";
|
||||
foreach ($this->_frames as $frames)
|
||||
foreach ($frames as $frame)
|
||||
@@ -426,6 +444,11 @@ final class ID3v2
|
||||
$datalen = strlen($data);
|
||||
$padlen = 0;
|
||||
|
||||
if (isset($this->_options["unsyncronisation"]) &&
|
||||
$this->_options["unsyncronisation"] === true)
|
||||
$this->_header->setFlags
|
||||
($this->_header->getFlags() | ID3_Header::UNSYNCHRONISATION);
|
||||
|
||||
/* The tag padding is calculated as follows. If the tag can be written in
|
||||
the space of the previous tag, the remaining space is used for padding.
|
||||
If there is no previous tag or the new tag is bigger than the space taken
|
||||
|
||||
@@ -40,6 +40,7 @@ require_once("PHPUnit/Framework.php");
|
||||
require_once("PHPUnit/TextUI/TestRunner.php");
|
||||
|
||||
ini_set("include_path", ini_get("include_path") . PATH_SEPARATOR . "../src/");
|
||||
ini_set("memory_limit", "16M");
|
||||
|
||||
$suite = new PHPUnit_Framework_TestSuite("PHP Reader");
|
||||
|
||||
|
||||
@@ -121,4 +121,19 @@ final class TestID3v2 extends PHPUnit_Framework_TestCase
|
||||
$this->assertEquals("13/13", $id3->trck->text);
|
||||
$this->assertEquals("Trance", $id3->tcon->text);
|
||||
}
|
||||
|
||||
function testUnsynchronisation()
|
||||
{
|
||||
$id3 = new ID3v2("id3v2.tag");
|
||||
$id3->tit2->text = "\xff\xf0";
|
||||
$id3->tcon->text = "\xff\xe0\xf0";
|
||||
$id3->write();
|
||||
|
||||
$this->assertEquals
|
||||
("TIT2\0\0\0\x08\0\x03\0\0\0\x03\x03\xff\x00\xf0", "" . $id3->tit2);
|
||||
|
||||
$id3 = new ID3v2("id3v2.tag");
|
||||
$this->assertEquals("\xff\xf0", $id3->tit2->text);
|
||||
$this->assertEquals("\xff\xe0\xf0", $id3->tcon->text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ require_once("Transform.php");
|
||||
* @package php-reader
|
||||
* @subpackage Tests
|
||||
* @author Sven Vollbehr <svollbehr@gmail.com>
|
||||
* @author Ryan Butterfield <buttza@gmail.com>
|
||||
* @copyright Copyright (c) 2008 The PHP Reader Project Workgroup
|
||||
* @license http://code.google.com/p/php-reader/wiki/License New BSD License
|
||||
* @version $Rev$
|
||||
@@ -70,6 +71,8 @@ final class TestTransform extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
$this->assertEquals
|
||||
(0x7fffffff, Transform::fromInt32(Transform::toInt32(0x7fffffff)));
|
||||
$this->assertEquals
|
||||
(-0x7fffffff, Transform::fromInt32(Transform::toInt32(-0x7fffffff)));
|
||||
$this->assertEquals(-1, Transform::fromInt32(Transform::toInt32(-1)));
|
||||
}
|
||||
|
||||
@@ -78,6 +81,8 @@ final class TestTransform extends PHPUnit_Framework_TestCase
|
||||
$this->assertEquals(1, Transform::fromInt32LE("\x01\x00\x00\x00"));
|
||||
$this->assertEquals
|
||||
(0x7fffffff, Transform::fromInt32LE(Transform::toInt32LE(0x7fffffff)));
|
||||
$this->assertEquals
|
||||
(-0x7fffffff, Transform::fromInt32LE(Transform::toInt32LE(-0x7fffffff)));
|
||||
$this->assertEquals(-1, Transform::fromInt32LE(Transform::toInt32LE(-1)));
|
||||
}
|
||||
|
||||
@@ -86,7 +91,8 @@ final class TestTransform extends PHPUnit_Framework_TestCase
|
||||
$this->assertEquals(1, Transform::fromInt32BE("\x00\x00\x00\x01"));
|
||||
$this->assertEquals
|
||||
(0x7fffffff, Transform::fromInt32BE(Transform::toInt32BE(0x7fffffff)));
|
||||
$this->assertEquals(-1, Transform::fromInt32BE(Transform::toInt32BE(-1)));
|
||||
$this->assertEquals
|
||||
(-0x7fffffff, Transform::fromInt32BE(Transform::toInt32BE(-0x7fffffff)));
|
||||
}
|
||||
|
||||
function testUInt32LE()
|
||||
@@ -152,9 +158,25 @@ final class TestTransform extends PHPUnit_Framework_TestCase
|
||||
function testString16()
|
||||
{
|
||||
$this->assertEquals("00e4", Transform::fromHHex
|
||||
(Transform::fromString16(Transform::toString16("\xff\xfe\x00\xe4"))));
|
||||
$this->assertEquals("00e4", Transform::fromHHex
|
||||
(Transform::fromString16(Transform::toString16("\xfe\xff\x00\xe4"))));
|
||||
(Transform::fromString16(Transform::toString16("\x00\xe4"))));
|
||||
$this->assertEquals
|
||||
("\0T\0h\0i\0s\0 \0i\0s\0 \0a\0 \0t\0e\0s\0t\0.",
|
||||
Transform::fromString16(Transform::toString16LE
|
||||
("\xff\xfe\0T\0h\0i\0s\0 \0i\0s\0 \0a\0 \0t\0e\0s\0t\0.")));
|
||||
$this->assertEquals
|
||||
("\0T\0h\0i\0s\0 \0i\0s\0 \0a\0 \0t\0e\0s\0t\0.",
|
||||
Transform::fromString16(Transform::toString16BE
|
||||
("\xff\xfe\0T\0h\0i\0s\0 \0i\0s\0 \0a\0 \0t\0e\0s\0t\0.")));
|
||||
$this->assertEquals
|
||||
("\0T\0h\0i\0s\0 \0i\0s\0 \0a\0 \0t\0e\0s\0t\0.",
|
||||
Transform::fromString16(Transform::toString16
|
||||
("\0T\0h\0i\0s\0 \0i\0s\0 \0a\0 \0t\0e\0s\0t\0.",
|
||||
Transform::LITTLE_ENDIAN_ORDER)));
|
||||
$this->assertEquals
|
||||
("\0T\0h\0i\0s\0 \0i\0s\0 \0a\0 \0t\0e\0s\0t\0.",
|
||||
Transform::fromString16(Transform::toString16
|
||||
("\0T\0h\0i\0s\0 \0i\0s\0 \0a\0 \0t\0e\0s\0t\0.",
|
||||
Transform::BIG_ENDIAN_ORDER)));
|
||||
}
|
||||
|
||||
function testString16LE()
|
||||
@@ -196,5 +218,4 @@ final class TestTransform extends PHPUnit_Framework_TestCase
|
||||
Transform::fromGUID(Transform::toGUID
|
||||
("75b22630-668e-11cf-a6d9-00aa0062ce6c")));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user