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

@@ -43,7 +43,7 @@ require_once("ID3/Object.php");
* A base class for all ID3v2 frames as described in the
* {@link http://www.id3.org/id3v2.4.0-frames ID3v2 frames document}.
*
*
*
* @package php-reader
* @subpackage ID3
* @author Sven Vollbehr <svollbehr@gmail.com>
@@ -128,11 +128,11 @@ class ID3_Frame extends ID3_Object
/**
* Raw content of the frame.
*
*
* @var string
*/
protected $_data = "";
/**
* Constructs the class with given parameters and reads object related data
* from the ID3v2 tag.
@@ -144,15 +144,15 @@ class ID3_Frame extends ID3_Object
public function __construct($reader = null, &$options = array())
{
parent::__construct($reader, $options);
if ($reader === null) {
$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,14 +168,26 @@ 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);
}
}
/**
* Returns the frame identifier string.
*
@@ -227,14 +239,14 @@ class ID3_Frame extends ID3_Object
/**
* Sets the frame raw data.
*
* @return string
* @param string $data
*/
protected function setData($data)
{
$this->_data = $data;
$this->_size = strlen($data);
}
/**
* Returns the frame raw 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;
@@ -288,9 +291,9 @@ final class ID3v1
if ($filename === false && ($filename = $this->_filename) === false)
throw new ID3_Exception("No file given to write the tag to");
if (($fd = fopen
($filename, file_exists($filename) ? "r+b" : "wb")) === false)
throw new ID3_Exception("Unable to open file for writing: " . $filename);
if (($fd = fopen
($filename, file_exists($filename) ? "r+b" : "wb")) === false)
throw new ID3_Exception("Unable to open file for writing: " . $filename);
fseek($fd, $this->_reader !== false ? -128 : 0, SEEK_END);
fwrite($fd, $this, 128);

View File

@@ -3,7 +3,7 @@
* PHP Reader Library
*
* Copyright (c) 2008 The PHP Reader Project Workgroup. All rights reserved.
*
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
@@ -59,10 +59,11 @@ require_once("ID3/Frame.php");
* need not be known to the software that encounters them. Each frame has an
* unique and predefined identifier which allows software to skip unknown
* frames.
*
*
* @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$
@@ -77,26 +78,26 @@ final class ID3v2
/** @var ID3_ExtendedHeader */
private $_extendedHeader;
/** @var ID3_Header */
private $_footer;
/** @var Array */
private $_frames = array();
/** @var string */
private $_filename = false;
/** @var Array */
private $_options;
/**
* Constructs the ID3v2 class with given file and options. The options array
* may also be given as the only parameter.
*
* The following options are currently recognized:
* o version -- The ID3v2 tag version to use in write operation. This option
* is automatically set when a tag is read from a file and defaults to
* is automatically set when a tag is read from a file and defaults to
* version 4.0 for tag write.
* o readonly -- Indicates that the tag is read from a temporary file or
* another source it cannot be written back to. The tag can, however,
@@ -105,9 +106,9 @@ 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 Array $options The options array.
* @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())
{
@@ -115,43 +116,50 @@ final class ID3v2
$options = $filename;
$filename = false;
}
$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);
if ($this->_header->hasFlag(ID3_Header::FOOTER))
$this->_footer = &$this->_header; // skip footer, and rather copy header
while (true) {
$offset = $this->_reader->getOffset();
// Jump off the loop if we reached the end of the tag
if ($offset - $startOffset - 10 >= $this->_header->getSize() -
($this->hasFooter() ? 10 : 0))
break;
// Jump off the loop if we reached the last frame
if ($this->_reader->available() < 4 || Transform::fromUInt32BE
($identifier = $this->_reader->read(4)) == 0)
@@ -165,7 +173,7 @@ final class ID3v2
$frame = new $classname($this->_reader, $options);
else
$frame = new ID3_Frame($this->_reader, $options);
if (!isset($this->_frames[$frame->getIdentifier()]))
$this->_frames[$frame->getIdentifier()] = array();
$this->_frames[$frame->getIdentifier()][] = $frame;
@@ -175,15 +183,15 @@ final class ID3v2
/**
* Returns the header object.
*
*
* @return ID3_Header
*/
public function getHeader() { return $this->_header; }
/**
* Checks whether there is an extended header present in the tag. Returns
* <var>true</var> if the header is present, <var>false</var> otherwise.
*
*
* @return boolean
*/
public function hasExtendedHeader()
@@ -191,11 +199,11 @@ final class ID3v2
if ($this->_header)
return $this->_header->hasFlag(ID3_Header::EXTENDEDHEADER);
}
/**
* Returns the extended header object if present, or <var>false</var>
* otherwise.
*
*
* @return ID3_ExtendedHeader|false
*/
public function getExtendedHeader()
@@ -219,27 +227,27 @@ final class ID3v2
$this->_extendedHeader = $extendedHeader;
} else throw new ID3_Exception("Invalid argument");
}
/**
* Checks whether there is a frame given as an argument defined in the tag.
* Returns <var>true</var> if one ore more frames are present,
* <var>false</var> otherwise.
*
*
* @return boolean
*/
public function hasFrame($identifier)
{
return isset($this->_frames[$identifier]);
}
/**
* Returns all the frames the tag contains as an associate array. The frame
* identifiers work as keys having an array of frames as associated value.
*
*
* @return Array
*/
public function getFrames() { return $this->_frames; }
/**
* Returns an array of frames matching the given identifier or an empty array
* if no frames matched the identifier.
@@ -251,7 +259,7 @@ final class ID3v2
* Please note that one may also use the shorthand $obj->identifier to access
* the first frame with the identifier given. Wildcards cannot be used with
* the shorthand.
*
*
* @return Array
*/
public function getFramesByIdentifier($identifier)
@@ -265,7 +273,7 @@ final class ID3v2
$matches[] = $frame;
return $matches;
}
/**
* Adds a new frame to the tag and returns it.
*
@@ -279,18 +287,18 @@ final class ID3v2
$this->_frames[$frame->getIdentifier()] = array();
return $this->_frames[$frame->getIdentifier()][] = $frame;
}
/**
* Checks whether there is a footer present in the tag. Returns
* <var>true</var> if the footer is present, <var>false</var> otherwise.
*
*
* @return boolean
*/
public function hasFooter()
{
return $this->_header->hasFlag(ID3_Header::FOOTER);
}
/**
* Returns the footer object if present, or <var>false</var> otherwise.
*
@@ -302,7 +310,7 @@ final class ID3v2
return $this->_footer;
return false;
}
/**
* Sets whether the tag should have a footer defined.
*
@@ -319,13 +327,13 @@ final class ID3v2
overridden upon re-write */
if ($this->hasFooter())
$this->_header->setSize($this->_header->getSize() + 10);
$this->_header->setFlags
($this->_header->getFlags() & ~ID3_Header::FOOTER);
$this->_footer = null;
}
}
/**
* Writes the possibly altered ID3v2 tag back to the file where it was read.
* If the class was constructed without a file name, one can be provided here
@@ -341,11 +349,16 @@ 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)
throw new ID3_Exception("Unable to open file for writing: " . $filename);
$oldTagSize = $this->_header->getSize();
$tag = "" . $this;
$tagSize = empty($this->_frames) ? 0 : strlen($tag);
@@ -368,7 +381,7 @@ final class ID3v2
$this->_filename = $filename;
}
/**
* Magic function so that $obj->value will work. The method will attempt to
* return the first frame that matches the identifier.
@@ -393,7 +406,7 @@ final class ID3v2
return $this->addFrame(new $classname());
throw new ID3_Exception("Unknown frame/field: " . $name);
}
/**
* Magic function so that isset($obj->value) will work. This method checks
* whether the frame matching the identifier exists.
@@ -401,16 +414,19 @@ 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
* all the frames matching the identifier.
*
* @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
@@ -438,11 +461,11 @@ final class ID3v2
else
$padlen = ceil(log(0.2 * ($datalen / 1024 + 10), 10) * 1024);
}
/* ID3v2.4.0 CRC calculated w/ padding */
if (!isset($this->_options["version"]) || $this->_options["version"] >= 4)
$data = str_pad($data, $datalen + $padlen, "\0");
if ($this->hasExtendedHeader()) {
$this->_extendedHeader->setPadding($padlen);
if ($this->_extendedHeader->hasFlag(ID3_ExtendedHeader::CRC32)) {
@@ -453,13 +476,13 @@ final class ID3v2
}
$data = $this->getExtendedHeader() . $data;
}
/* ID3v2.3.0 CRC calculated w/o padding */
if (isset($this->_options["version"]) && $this->_options["version"] < 4)
$data = str_pad($data, $datalen + $padlen, "\0");
$this->_header->setSize(strlen($data));
return "ID3" . $this->_header . $data .
($this->hasFooter() ? "3DI" . $this->getFooter() : "");
}