}
};
- const int versionIndex = d->version == Version1 ? 0 : 1;
- const int layerIndex = d->layer > 0 ? d->layer - 1 : 0;
+ const int versionIndex = (d->version == Version1) ? 0 : 1;
+ const int layerIndex = (d->layer > 0) ? d->layer - 1 : 0;
// The bitrate index is encoded as the first 4 bits of the 3rd byte,
// i.e. 1111xxxx
d->isCopyrighted = flags[3];
d->isPadded = flags[9];
- // Calculate the frame length
-
- if(d->layer == 1)
- d->frameLength = 24000 * 2 * d->bitrate / d->sampleRate + int(d->isPadded);
- else
- d->frameLength = 72000 * d->bitrate / d->sampleRate + int(d->isPadded);
-
// Samples per frame
static const int samplesPerFrame[3][2] = {
d->samplesPerFrame = samplesPerFrame[layerIndex][versionIndex];
+ // Calculate the frame length
+
+ static const int paddingSize[3] = { 4, 1, 1 };
+
+ d->frameLength = d->samplesPerFrame * d->bitrate * 125 / d->sampleRate;
+
+ if(d->isPadded)
+ d->frameLength += paddingSize[layerIndex];
+
// Now that we're done parsing, set this to be a valid frame.
d->isValid = true;
bool isOriginal() const;
/*!
- * Returns the frame length.
+ * Returns the frame length in bytes.
*/
int frameLength() const;
#include "mpegproperties.h"
#include "mpegfile.h"
#include "xingheader.h"
+#include "id3v2tag.h"
+#include "id3v2header.h"
+#include "apetag.h"
+#include "apefooter.h"
using namespace TagLib;
class MPEG::Properties::PropertiesPrivate
{
public:
- PropertiesPrivate(File *f, ReadStyle s) :
- file(f),
+ PropertiesPrivate() :
xingHeader(0),
- style(s),
length(0),
bitrate(0),
sampleRate(0),
delete xingHeader;
}
- File *file;
XingHeader *xingHeader;
- ReadStyle style;
int length;
int bitrate;
int sampleRate;
// public members
////////////////////////////////////////////////////////////////////////////////
-MPEG::Properties::Properties(File *file, ReadStyle style) : AudioProperties(style)
+MPEG::Properties::Properties(File *file, ReadStyle style) :
+ AudioProperties(style),
+ d(new PropertiesPrivate())
{
- d = new PropertiesPrivate(file, style);
-
- if(file && file->isOpen())
- read();
+ read(file);
}
MPEG::Properties::~Properties()
}
int MPEG::Properties::length() const
+{
+ return lengthInSeconds();
+}
+
+int MPEG::Properties::lengthInSeconds() const
+{
+ return d->length / 1000;
+}
+
+int MPEG::Properties::lengthInMilliseconds() const
{
return d->length;
}
// private members
////////////////////////////////////////////////////////////////////////////////
-void MPEG::Properties::read()
+void MPEG::Properties::read(File *file)
{
- // Since we've likely just looked for the ID3v1 tag, start at the end of the
- // file where we're least likely to have to have to move the disk head.
-
- long last = d->file->lastFrameOffset();
-
- if(last < 0) {
- debug("MPEG::Properties::read() -- Could not find a valid last MPEG frame in the stream.");
- return;
- }
-
- d->file->seek(last);
- Header lastHeader(d->file->readBlock(4));
-
- long first = d->file->firstFrameOffset();
+ // Only the first frame is required if we have a VBR header.
+ const long first = file->firstFrameOffset();
if(first < 0) {
debug("MPEG::Properties::read() -- Could not find a valid first MPEG frame in the stream.");
return;
}
- if(!lastHeader.isValid()) {
-
- long pos = last;
-
- while(pos > first) {
-
- pos = d->file->previousFrameOffset(pos);
-
- if(pos < 0)
- break;
+ file->seek(first);
+ const Header firstHeader(file->readBlock(4));
- d->file->seek(pos);
- Header header(d->file->readBlock(4));
-
- if(header.isValid()) {
- lastHeader = header;
- last = pos;
- break;
- }
- }
- }
-
- // Now jump back to the front of the file and read what we need from there.
-
- d->file->seek(first);
- Header firstHeader(d->file->readBlock(4));
-
- if(!firstHeader.isValid() || !lastHeader.isValid()) {
- debug("MPEG::Properties::read() -- Page headers were invalid.");
+ if(!firstHeader.isValid()) {
+ debug("MPEG::Properties::read() -- The first page header is invalid.");
return;
}
- // Check for a Xing header that will help us in gathering information about a
+ // Check for a VBR header that will help us in gathering information about a
// VBR stream.
- int xingHeaderOffset = MPEG::XingHeader::xingHeaderOffset(firstHeader.version(),
- firstHeader.channelMode());
-
- d->file->seek(first + xingHeaderOffset);
- d->xingHeader = new XingHeader(d->file->readBlock(16));
-
- // Read the length and the bitrate from the Xing header.
+ file->seek(first + 4);
+ d->xingHeader = new XingHeader(file->readBlock(firstHeader.frameLength() - 4));
if(d->xingHeader->isValid() &&
- firstHeader.sampleRate() > 0 &&
- d->xingHeader->totalFrames() > 0)
- {
- double timePerFrame =
- double(firstHeader.samplesPerFrame()) / firstHeader.sampleRate();
+ firstHeader.samplesPerFrame() > 0 &&
+ firstHeader.sampleRate() > 0) {
- double length = timePerFrame * d->xingHeader->totalFrames();
+ // Read the length and the bitrate from the VBR header.
- d->length = int(length);
- d->bitrate = d->length > 0 ? (int)(d->xingHeader->totalSize() * 8 / length / 1000) : 0;
+ const double timePerFrame = firstHeader.samplesPerFrame() * 1000.0 / firstHeader.sampleRate();
+ const double length = timePerFrame * d->xingHeader->totalFrames();
+
+ d->length = static_cast<int>(length + 0.5);
+ d->bitrate = static_cast<int>(d->xingHeader->totalSize() * 8.0 / length + 0.5);
}
- else {
- // Since there was no valid Xing header found, we hope that we're in a constant
- // bitrate file.
+ else if(firstHeader.bitrate() > 0) {
- delete d->xingHeader;
- d->xingHeader = 0;
+ // Since there was no valid VBR header found, we hope that we're in a constant
+ // bitrate file.
// TODO: Make this more robust with audio property detection for VBR without a
// Xing header.
- if(firstHeader.frameLength() > 0 && firstHeader.bitrate() > 0) {
- int frames = (last - first) / firstHeader.frameLength() + 1;
+ d->bitrate = firstHeader.bitrate();
- d->length = int(float(firstHeader.frameLength() * frames) /
- float(firstHeader.bitrate() * 125) + 0.5);
- d->bitrate = firstHeader.bitrate();
- }
- }
+ long long streamLength = file->length();
+ if(file->hasID3v1Tag())
+ streamLength -= 128;
+
+ if(file->hasID3v2Tag())
+ streamLength -= file->ID3v2Tag()->header()->completeTagSize();
+
+ if(file->hasAPETag())
+ streamLength -= file->APETag()->footer()->completeTagSize();
+
+ if(streamLength > 0)
+ d->length = static_cast<int>(streamLength * 8.0 / d->bitrate + 0.5);
+ }
- d->sampleRate = firstHeader.sampleRate();
- d->channels = firstHeader.channelMode() == Header::SingleChannel ? 1 : 2;
- d->version = firstHeader.version();
- d->layer = firstHeader.layer();
+ d->sampleRate = firstHeader.sampleRate();
+ d->channels = firstHeader.channelMode() == Header::SingleChannel ? 1 : 2;
+ d->version = firstHeader.version();
+ d->layer = firstHeader.layer();
d->protectionEnabled = firstHeader.protectionEnabled();
- d->channelMode = firstHeader.channelMode();
- d->isCopyrighted = firstHeader.isCopyrighted();
- d->isOriginal = firstHeader.isOriginal();
+ d->channelMode = firstHeader.channelMode();
+ d->isCopyrighted = firstHeader.isCopyrighted();
+ d->isOriginal = firstHeader.isOriginal();
}
*/
virtual ~Properties();
- // Reimplementations.
-
+ /*!
+ * Returns the length of the file in seconds. The length is rounded down to
+ * the nearest whole second.
+ *
+ * \note This method is just an alias of lengthInSeconds().
+ *
+ * \deprecated
+ */
virtual int length() const;
+
+ /*!
+ * Returns the length of the file in seconds. The length is rounded down to
+ * the nearest whole second.
+ *
+ * \see lengthInMilliseconds()
+ */
+ // BIC: make virtual
+ int lengthInSeconds() const;
+
+ /*!
+ * Returns the length of the file in milliseconds.
+ *
+ * \see lengthInSeconds()
+ */
+ // BIC: make virtual
+ int lengthInMilliseconds() const;
+
+ /*!
+ * Returns the average bit rate of the file in kb/s.
+ */
virtual int bitrate() const;
+
+ /*!
+ * Returns the sample rate in Hz.
+ */
virtual int sampleRate() const;
- virtual int channels() const;
/*!
- * Returns a pointer to the XingHeader if one exists or null if no
- * XingHeader was found.
+ * Returns the number of audio channels.
*/
+ virtual int channels() const;
+ /*!
+ * Returns a pointer to the Xing/VBRI header if one exists or null if no
+ * Xing/VBRI header was found.
+ */
const XingHeader *xingHeader() const;
/*!
Properties(const Properties &);
Properties &operator=(const Properties &);
- void read();
+ void read(File *file);
class PropertiesPrivate;
PropertiesPrivate *d;
#include <tdebug.h>
#include "xingheader.h"
+#include "mpegfile.h"
using namespace TagLib;
XingHeaderPrivate() :
frames(0),
size(0),
- valid(false)
- {}
+ type(MPEG::XingHeader::Invalid) {}
uint frames;
uint size;
- bool valid;
+
+ MPEG::XingHeader::HeaderType type;
};
-MPEG::XingHeader::XingHeader(const ByteVector &data)
+////////////////////////////////////////////////////////////////////////////////
+// public members
+////////////////////////////////////////////////////////////////////////////////
+
+MPEG::XingHeader::XingHeader(const ByteVector &data) :
+ d(new XingHeaderPrivate())
{
- d = new XingHeaderPrivate;
parse(data);
}
bool MPEG::XingHeader::isValid() const
{
- return d->valid;
+ return (d->type != Invalid && d->frames > 0 && d->size > 0);
}
TagLib::uint MPEG::XingHeader::totalFrames() const
return d->size;
}
-int MPEG::XingHeader::xingHeaderOffset(TagLib::MPEG::Header::Version v,
- TagLib::MPEG::Header::ChannelMode c)
+MPEG::XingHeader::HeaderType MPEG::XingHeader::type() const
{
- if(v == MPEG::Header::Version1) {
- if(c == MPEG::Header::SingleChannel)
- return 0x15;
- else
- return 0x24;
- }
- else {
- if(c == MPEG::Header::SingleChannel)
- return 0x0D;
- else
- return 0x15;
- }
+ return d->type;
}
+int MPEG::XingHeader::xingHeaderOffset(TagLib::MPEG::Header::Version /*v*/,
+ TagLib::MPEG::Header::ChannelMode /*c*/)
+{
+ return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// private members
+////////////////////////////////////////////////////////////////////////////////
+
void MPEG::XingHeader::parse(const ByteVector &data)
{
- // Check to see if a valid Xing header is available.
+ // Look for a Xing header.
- if(!data.startsWith("Xing") && !data.startsWith("Info"))
- return;
+ long offset = data.find("Xing");
+ if(offset < 0)
+ offset = data.find("Info");
- // If the XingHeader doesn't contain the number of frames and the total stream
- // info it's invalid.
+ if(offset >= 0) {
- if(!(data[7] & 0x01)) {
- debug("MPEG::XingHeader::parse() -- Xing header doesn't contain the total number of frames.");
- return;
- }
+ // Xing header found.
+
+ if(data.size() < offset + 16) {
+ debug("MPEG::XingHeader::parse() -- Xing header found but too short.");
+ return;
+ }
+
+ if((data[offset + 7] & 0x03) != 0x03) {
+ debug("MPEG::XingHeader::parse() -- Xing header doesn't contain the required information.");
+ return;
+ }
- if(!(data[7] & 0x02)) {
- debug("MPEG::XingHeader::parse() -- Xing header doesn't contain the total stream size.");
- return;
+ d->frames = data.toUInt(offset + 8, true);
+ d->size = data.toUInt(offset + 12, true);
+ d->type = Xing;
}
+ else {
+
+ // Xing header not found. Then look for a VBRI header.
- d->frames = data.toUInt(8U);
- d->size = data.toUInt(12U);
+ offset = data.find("VBRI");
- d->valid = true;
+ if(offset >= 0) {
+
+ // VBRI header found.
+
+ if(data.size() < offset + 32) {
+ debug("MPEG::XingHeader::parse() -- VBRI header found but too short.");
+ return;
+ }
+
+ d->frames = data.toUInt(offset + 14, true);
+ d->size = data.toUInt(offset + 10, true);
+ d->type = VBRI;
+ }
+ }
}
namespace MPEG {
- //! An implementation of the Xing VBR headers
+ class File;
+
+ //! An implementation of the Xing/VBRI headers
/*!
- * This is a minimalistic implementation of the Xing VBR headers. Xing
- * headers are often added to VBR (variable bit rate) MP3 streams to make it
- * easy to compute the length and quality of a VBR stream. Our implementation
- * is only concerned with the total size of the stream (so that we can
- * calculate the total playing time and the average bitrate). It uses
- * <a href="http://home.pcisys.net/~melanson/codecs/mp3extensions.txt">this text</a>
- * and the XMMS sources as references.
+ * This is a minimalistic implementation of the Xing/VBRI VBR headers.
+ * Xing/VBRI headers are often added to VBR (variable bit rate) MP3 streams
+ * to make it easy to compute the length and quality of a VBR stream. Our
+ * implementation is only concerned with the total size of the stream (so
+ * that we can calculate the total playing time and the average bitrate).
+ * It uses <a href="http://home.pcisys.net/~melanson/codecs/mp3extensions.txt">
+ * this text</a> and the XMMS sources as references.
*/
class TAGLIB_EXPORT XingHeader
{
public:
/*!
- * Parses a Xing header based on \a data. The data must be at least 16
- * bytes long (anything longer than this is discarded).
+ * The type of the VBR header.
+ */
+ enum HeaderType
+ {
+ /*!
+ * Invalid header or no VBR header found.
+ */
+ Invalid = 0,
+
+ /*!
+ * Xing header.
+ */
+ Xing = 1,
+
+ /*!
+ * VBRI header.
+ */
+ VBRI = 2,
+ };
+
+ /*!
+ * Parses an Xing/VBRI header based on \a data which contains the entire
+ * first MPEG frame.
*/
XingHeader(const ByteVector &data);
/*!
* Returns true if the data was parsed properly and if there is a valid
- * Xing header present.
+ * Xing/VBRI header present.
*/
bool isValid() const;
*/
uint totalSize() const;
+ /*!
+ * Returns the type of the VBR header.
+ */
+ HeaderType type() const;
+
/*!
* Returns the offset for the start of this Xing header, given the
* version and channels of the frame
+ *
+ * \deprecated Always returns 0.
*/
- // BIC: rename to offset()
static int xingHeaderOffset(TagLib::MPEG::Header::Version v,
TagLib::MPEG::Header::ChannelMode c);
#include <tstring.h>
#include <mpegfile.h>
#include <id3v2tag.h>
+#include <mpegproperties.h>
+#include <xingheader.h>
+#include <mpegheader.h>
#include <cppunit/extensions/HelperMacros.h>
#include "utils.h"
class TestMPEG : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(TestMPEG);
+ CPPUNIT_TEST(testAudioPropertiesXingHeaderCBR);
+ CPPUNIT_TEST(testAudioPropertiesXingHeaderVBR);
+ CPPUNIT_TEST(testAudioPropertiesVBRIHeader);
+ CPPUNIT_TEST(testAudioPropertiesNoVBRHeaders);
CPPUNIT_TEST(testVersion2DurationWithXingHeader);
CPPUNIT_TEST(testSaveID3v24);
CPPUNIT_TEST(testSaveID3v24WrongParam);
public:
+ void testAudioPropertiesXingHeaderCBR()
+ {
+ MPEG::File f(TEST_FILE_PATH_C("lame_cbr.mp3"));
+ CPPUNIT_ASSERT(f.audioProperties());
+ CPPUNIT_ASSERT_EQUAL(1887, f.audioProperties()->length());
+ CPPUNIT_ASSERT_EQUAL(1887, f.audioProperties()->lengthInSeconds());
+ CPPUNIT_ASSERT_EQUAL(1887164, f.audioProperties()->lengthInMilliseconds());
+ CPPUNIT_ASSERT_EQUAL(64, f.audioProperties()->bitrate());
+ CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->channels());
+ CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate());
+ CPPUNIT_ASSERT_EQUAL(MPEG::XingHeader::Xing, f.audioProperties()->xingHeader()->type());
+ }
+
+ void testAudioPropertiesXingHeaderVBR()
+ {
+ MPEG::File f(TEST_FILE_PATH_C("lame_vbr.mp3"));
+ CPPUNIT_ASSERT(f.audioProperties());
+ CPPUNIT_ASSERT_EQUAL(1887, f.audioProperties()->length());
+ CPPUNIT_ASSERT_EQUAL(1887, f.audioProperties()->lengthInSeconds());
+ CPPUNIT_ASSERT_EQUAL(1887164, f.audioProperties()->lengthInMilliseconds());
+ CPPUNIT_ASSERT_EQUAL(70, f.audioProperties()->bitrate());
+ CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->channels());
+ CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate());
+ CPPUNIT_ASSERT_EQUAL(MPEG::XingHeader::Xing, f.audioProperties()->xingHeader()->type());
+ }
+
+ void testAudioPropertiesVBRIHeader()
+ {
+ MPEG::File f(TEST_FILE_PATH_C("vbri.mp3"));
+ CPPUNIT_ASSERT(f.audioProperties());
+ CPPUNIT_ASSERT_EQUAL(222, f.audioProperties()->length());
+ CPPUNIT_ASSERT_EQUAL(222, f.audioProperties()->lengthInSeconds());
+ CPPUNIT_ASSERT_EQUAL(222198, f.audioProperties()->lengthInMilliseconds());
+ CPPUNIT_ASSERT_EQUAL(233, f.audioProperties()->bitrate());
+ CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels());
+ CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate());
+ CPPUNIT_ASSERT_EQUAL(MPEG::XingHeader::VBRI, f.audioProperties()->xingHeader()->type());
+ }
+
+ void testAudioPropertiesNoVBRHeaders()
+ {
+ MPEG::File f(TEST_FILE_PATH_C("bladeenc.mp3"));
+ CPPUNIT_ASSERT(f.audioProperties());
+ CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->length());
+ CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->lengthInSeconds());
+ CPPUNIT_ASSERT_EQUAL(3553, f.audioProperties()->lengthInMilliseconds());
+ CPPUNIT_ASSERT_EQUAL(64, f.audioProperties()->bitrate());
+ CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->channels());
+ CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate());
+ CPPUNIT_ASSERT(!f.audioProperties()->xingHeader()->isValid());
+
+ long last = f.lastFrameOffset();
+
+ f.seek(last);
+ MPEG::Header lastHeader(f.readBlock(4));
+
+ while (!lastHeader.isValid()) {
+
+ last = f.previousFrameOffset(last);
+
+ f.seek(last);
+ lastHeader = MPEG::Header(f.readBlock(4));
+ }
+
+ CPPUNIT_ASSERT_EQUAL(28213L, last);
+ CPPUNIT_ASSERT_EQUAL(209, lastHeader.frameLength());
+ }
+
void testVersion2DurationWithXingHeader()
{
MPEG::File f(TEST_FILE_PATH_C("mpeg2.mp3"));
+ CPPUNIT_ASSERT(f.audioProperties());
CPPUNIT_ASSERT_EQUAL(5387, f.audioProperties()->length());
+ CPPUNIT_ASSERT_EQUAL(5387, f.audioProperties()->lengthInSeconds());
+ CPPUNIT_ASSERT_EQUAL(5387285, f.audioProperties()->lengthInMilliseconds());
}
void testSaveID3v24()