diff --git a/taglib/mpeg/mpegfile.cpp b/taglib/mpeg/mpegfile.cpp index 637dfa9b..43075cfc 100644 --- a/taglib/mpeg/mpegfile.cpp +++ b/taglib/mpeg/mpegfile.cpp @@ -31,14 +31,31 @@ #include #include -#include - #include "mpegfile.h" #include "mpegheader.h" #include "tpropertymap.h" using namespace TagLib; +namespace +{ + /*! + * MPEG frames can be recognized by the bit pattern 11111111 111, so the + * first byte is easy to check for, however checking to see if the second byte + * starts with \e 111 is a bit more tricky, hence these functions. + */ + + inline bool firstSyncByte(uchar byte) + { + return (byte == 0xFF); + } + + inline bool secondSynchByte(uchar byte) + { + return ((byte & 0xE0) == 0xE0); + } +} + namespace { enum { ID3v2Index = 0, APEIndex = 1, ID3v1Index = 2 }; @@ -60,7 +77,6 @@ public: hasAPE(false), properties(0) { - } ~FilePrivate() @@ -95,33 +111,30 @@ public: // public members //////////////////////////////////////////////////////////////////////////////// -MPEG::File::File(FileName file, bool readProperties, - Properties::ReadStyle propertiesStyle) : TagLib::File(file) +MPEG::File::File(FileName file, bool readProperties, Properties::ReadStyle) : + TagLib::File(file), + d(new FilePrivate()) { - d = new FilePrivate; - if(isOpen()) - read(readProperties, propertiesStyle); + read(readProperties); } MPEG::File::File(FileName file, ID3v2::FrameFactory *frameFactory, - bool readProperties, Properties::ReadStyle propertiesStyle) : - TagLib::File(file) + bool readProperties, Properties::ReadStyle) : + TagLib::File(file), + d(new FilePrivate(frameFactory)) { - d = new FilePrivate(frameFactory); - if(isOpen()) - read(readProperties, propertiesStyle); + read(readProperties); } MPEG::File::File(IOStream *stream, ID3v2::FrameFactory *frameFactory, - bool readProperties, Properties::ReadStyle propertiesStyle) : - TagLib::File(stream) + bool readProperties, Properties::ReadStyle) : + TagLib::File(stream), + d(new FilePrivate(frameFactory)) { - d = new FilePrivate(frameFactory); - if(isOpen()) - read(readProperties, propertiesStyle); + read(readProperties); } MPEG::File::~File() @@ -392,11 +405,11 @@ long MPEG::File::nextFrameOffset(long position) return position - 1; for(uint i = 0; i < buffer.size() - 1; i++) { - if(uchar(buffer[i]) == 0xff && secondSynchByte(buffer[i + 1])) + if(firstSyncByte(buffer[i]) && secondSynchByte(buffer[i + 1])) return position + i; } - foundLastSyncPattern = uchar(buffer[buffer.size() - 1]) == 0xff; + foundLastSyncPattern = firstSyncByte(buffer[buffer.size() - 1]); position += buffer.size(); } } @@ -416,11 +429,11 @@ long MPEG::File::previousFrameOffset(long position) if(buffer.size() <= 0) break; - if(foundFirstSyncPattern && uchar(buffer[buffer.size() - 1]) == 0xff) + if(foundFirstSyncPattern && firstSyncByte(buffer[buffer.size() - 1])) return position + buffer.size() - 1; for(int i = buffer.size() - 2; i >= 0; i--) { - if(uchar(buffer[i]) == 0xff && secondSynchByte(buffer[i + 1])) + if(firstSyncByte(buffer[i]) && secondSynchByte(buffer[i + 1])) return position + i; } @@ -478,7 +491,7 @@ bool MPEG::File::hasAPETag() const // private members //////////////////////////////////////////////////////////////////////////////// -void MPEG::File::read(bool readProperties, Properties::ReadStyle propertiesStyle) +void MPEG::File::read(bool readProperties) { // Look for an ID3v2 tag @@ -517,7 +530,7 @@ void MPEG::File::read(bool readProperties, Properties::ReadStyle propertiesStyle } if(readProperties) - d->properties = new Properties(this, propertiesStyle); + d->properties = new Properties(this); // Make sure that we have our default tag types available. @@ -528,7 +541,7 @@ void MPEG::File::read(bool readProperties, Properties::ReadStyle propertiesStyle long MPEG::File::findID3v2(long offset) { // This method is based on the contents of TagLib::File::find(), but because - // of some subtlteies -- specifically the need to look for the bit pattern of + // of some subtleties -- specifically the need to look for the bit pattern of // an MPEG sync, it has been modified for use here. if(isValid() && ID3v2::Header::fileIdentifier().size() <= bufferSize()) { @@ -668,11 +681,3 @@ void MPEG::File::findAPE() d->APELocation = -1; d->APEFooterLocation = -1; } - -bool MPEG::File::secondSynchByte(char byte) -{ - std::bitset<8> b(byte); - - // check to see if the byte matches 111xxxxx - return b.test(7) && b.test(6) && b.test(5); -} diff --git a/taglib/mpeg/mpegfile.h b/taglib/mpeg/mpegfile.h index 55c83db1..01f81d1c 100644 --- a/taglib/mpeg/mpegfile.h +++ b/taglib/mpeg/mpegfile.h @@ -370,18 +370,11 @@ namespace TagLib { File(const File &); File &operator=(const File &); - void read(bool readProperties, Properties::ReadStyle propertiesStyle); + void read(bool readProperties); long findID3v2(long offset); long findID3v1(); void findAPE(); - /*! - * MPEG frames can be recognized by the bit pattern 11111111 111, so the - * first byte is easy to check for, however checking to see if the second byte - * starts with \e 111 is a bit more tricky, hence this member function. - */ - static bool secondSynchByte(char byte); - class FilePrivate; FilePrivate *d; }; diff --git a/taglib/mpeg/mpegheader.cpp b/taglib/mpeg/mpegheader.cpp index a582f758..0c9a0f1c 100644 --- a/taglib/mpeg/mpegheader.cpp +++ b/taglib/mpeg/mpegheader.cpp @@ -213,8 +213,8 @@ void MPEG::Header::parse(const ByteVector &data) } }; - 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 @@ -253,13 +253,6 @@ void MPEG::Header::parse(const ByteVector &data) 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] = { @@ -271,6 +264,15 @@ void MPEG::Header::parse(const ByteVector &data) 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; diff --git a/taglib/mpeg/mpegheader.h b/taglib/mpeg/mpegheader.h index 020ebd06..a55cac09 100644 --- a/taglib/mpeg/mpegheader.h +++ b/taglib/mpeg/mpegheader.h @@ -140,7 +140,7 @@ namespace TagLib { bool isOriginal() const; /*! - * Returns the frame length. + * Returns the frame length in bytes. */ int frameLength() const; diff --git a/taglib/mpeg/mpegproperties.cpp b/taglib/mpeg/mpegproperties.cpp index 3af95664..9fece404 100644 --- a/taglib/mpeg/mpegproperties.cpp +++ b/taglib/mpeg/mpegproperties.cpp @@ -29,16 +29,16 @@ #include "mpegproperties.h" #include "mpegfile.h" #include "xingheader.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), @@ -55,9 +55,7 @@ public: delete xingHeader; } - File *file; XingHeader *xingHeader; - ReadStyle style; int length; int bitrate; int sampleRate; @@ -74,12 +72,11 @@ public: // 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() @@ -88,6 +85,16 @@ 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; } @@ -146,109 +153,72 @@ bool MPEG::Properties::isOriginal() const // 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()) { + file->seek(first); + const Header firstHeader(file->readBlock(4)); - long pos = last; - - while(pos > first) { - - pos = d->file->previousFrameOffset(pos); - - if(pos < 0) - break; - - 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. - - if(d->xingHeader->isValid() && - firstHeader.sampleRate() > 0 && - d->xingHeader->totalFrames() > 0) - { - double timePerFrame = - double(firstHeader.samplesPerFrame()) / firstHeader.sampleRate(); - - double length = timePerFrame * d->xingHeader->totalFrames(); - - d->length = int(length); - d->bitrate = d->length > 0 ? (int)(d->xingHeader->totalSize() * 8 / length / 1000) : 0; - } - else { - // Since there was no valid Xing header found, we hope that we're in a constant - // bitrate file. - + file->seek(first + 4); + d->xingHeader = new XingHeader(file->readBlock(firstHeader.frameLength() - 4)); + if(!d->xingHeader->isValid()) { delete d->xingHeader; d->xingHeader = 0; + } + + if(d->xingHeader && firstHeader.samplesPerFrame() > 0 && firstHeader.sampleRate() > 0) { + + // Read the length and the bitrate from the VBR header. + + const double timePerFrame = firstHeader.samplesPerFrame() * 1000.0 / firstHeader.sampleRate(); + const double length = timePerFrame * d->xingHeader->totalFrames(); + + d->length = static_cast(length + 0.5); + d->bitrate = static_cast(d->xingHeader->totalSize() * 8.0 / length + 0.5); + } + else if(firstHeader.bitrate() > 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 streamLength = file->length() - first; + + if(file->hasID3v1Tag()) + streamLength -= 128; + + if(file->hasAPETag()) + streamLength -= file->APETag()->footer()->completeTagSize(); + + if(streamLength > 0) + d->length = static_cast(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(); } diff --git a/taglib/mpeg/mpegproperties.h b/taglib/mpeg/mpegproperties.h index 72e594ff..f11fad11 100644 --- a/taglib/mpeg/mpegproperties.h +++ b/taglib/mpeg/mpegproperties.h @@ -59,18 +59,52 @@ namespace TagLib { */ 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; + + /*! + * Returns the number of audio channels. + */ virtual int channels() const; /*! - * Returns a pointer to the XingHeader if one exists or null if no - * XingHeader was found. + * Returns a pointer to the Xing/VBRI header if one exists or null if no + * Xing/VBRI header was found. */ - const XingHeader *xingHeader() const; /*! @@ -107,7 +141,7 @@ namespace TagLib { Properties(const Properties &); Properties &operator=(const Properties &); - void read(); + void read(File *file); class PropertiesPrivate; PropertiesPrivate *d; diff --git a/taglib/mpeg/xingheader.cpp b/taglib/mpeg/xingheader.cpp index 9e20127e..5a496184 100644 --- a/taglib/mpeg/xingheader.cpp +++ b/taglib/mpeg/xingheader.cpp @@ -28,6 +28,7 @@ #include #include "xingheader.h" +#include "mpegfile.h" using namespace TagLib; @@ -37,17 +38,21 @@ public: 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); } @@ -58,7 +63,7 @@ MPEG::XingHeader::~XingHeader() bool MPEG::XingHeader::isValid() const { - return d->valid; + return (d->type != Invalid && d->frames > 0 && d->size > 0); } TagLib::uint MPEG::XingHeader::totalFrames() const @@ -71,45 +76,65 @@ TagLib::uint MPEG::XingHeader::totalSize() 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() < static_cast(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; + } + + d->frames = data.toUInt(offset + 8, true); + d->size = data.toUInt(offset + 12, true); + d->type = Xing; } + else { - if(!(data[7] & 0x02)) { - debug("MPEG::XingHeader::parse() -- Xing header doesn't contain the total stream size."); - return; + // Xing header not found. Then look for a VBRI header. + + offset = data.find("VBRI"); + + if(offset >= 0) { + + // VBRI header found. + + if(data.size() < static_cast(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; + } } - - d->frames = data.toUInt(8U); - d->size = data.toUInt(12U); - - d->valid = true; } diff --git a/taglib/mpeg/xingheader.h b/taglib/mpeg/xingheader.h index ffe7494d..cd417157 100644 --- a/taglib/mpeg/xingheader.h +++ b/taglib/mpeg/xingheader.h @@ -35,24 +35,47 @@ namespace TagLib { 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 - * this text - * 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 + * this text 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); @@ -63,7 +86,7 @@ namespace TagLib { /*! * Returns true if the data was parsed properly and if there is a valid - * Xing header present. + * Xing/VBRI header present. */ bool isValid() const; @@ -77,11 +100,17 @@ namespace TagLib { */ 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); diff --git a/tests/data/bladeenc.mp3 b/tests/data/bladeenc.mp3 new file mode 100644 index 00000000..e3d1a4b5 Binary files /dev/null and b/tests/data/bladeenc.mp3 differ diff --git a/tests/data/lame_cbr.mp3 b/tests/data/lame_cbr.mp3 new file mode 100644 index 00000000..b7badeb0 Binary files /dev/null and b/tests/data/lame_cbr.mp3 differ diff --git a/tests/data/lame_vbr.mp3 b/tests/data/lame_vbr.mp3 new file mode 100644 index 00000000..643056ef Binary files /dev/null and b/tests/data/lame_vbr.mp3 differ diff --git a/tests/data/vbri.mp3 b/tests/data/vbri.mp3 new file mode 100644 index 00000000..ea14d61e Binary files /dev/null and b/tests/data/vbri.mp3 differ diff --git a/tests/test_mpeg.cpp b/tests/test_mpeg.cpp index d0065365..8d49a8c7 100644 --- a/tests/test_mpeg.cpp +++ b/tests/test_mpeg.cpp @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include #include #include "utils.h" @@ -12,6 +15,10 @@ using namespace TagLib; 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); @@ -23,10 +30,81 @@ class TestMPEG : public CppUnit::TestFixture 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()); + + 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()