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:
svollbehr
2008-08-03 19:09:16 +00:00
parent 809bf58885
commit 741de5a2ab
10 changed files with 221 additions and 93 deletions

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
}
/**

View File

@@ -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.

View File

@@ -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;

View File

@@ -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

View File

@@ -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");

View File

@@ -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);
}
}

View File

@@ -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")));
}
}