diff --git a/taglib/ape/apefile.cpp b/taglib/ape/apefile.cpp index 3cb9d9a7..c774f516 100644 --- a/taglib/ape/apefile.cpp +++ b/taglib/ape/apefile.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include "apefile.h" @@ -57,12 +58,17 @@ public: APELocation(-1), APESize(0), ID3v1Location(-1), + ID3v2Header(0), + ID3v2Location(-1), + ID3v2Size(0), properties(0), hasAPE(false), - hasID3v1(false) {} + hasID3v1(false), + hasID3v2(false) {} ~FilePrivate() { + delete ID3v2Header; delete properties; } @@ -71,6 +77,10 @@ public: long ID3v1Location; + ID3v2::Header *ID3v2Header; + long ID3v2Location; + uint ID3v2Size; + TagUnion tag; Properties *properties; @@ -80,26 +90,27 @@ public: bool hasAPE; bool hasID3v1; + bool hasID3v2; }; //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// -APE::File::File(FileName file, bool readProperties, - Properties::ReadStyle propertiesStyle) : TagLib::File(file) +APE::File::File(FileName file, bool readProperties, Properties::ReadStyle) : + TagLib::File(file), + d(new FilePrivate()) { - d = new FilePrivate; if(isOpen()) - read(readProperties, propertiesStyle); + read(readProperties); } -APE::File::File(IOStream *stream, bool readProperties, - Properties::ReadStyle propertiesStyle) : TagLib::File(stream) +APE::File::File(IOStream *stream, bool readProperties, Properties::ReadStyle) : + TagLib::File(stream), + d(new FilePrivate()) { - d = new FilePrivate; if(isOpen()) - read(readProperties, propertiesStyle); + read(readProperties); } APE::File::~File() @@ -249,8 +260,19 @@ bool APE::File::hasID3v1Tag() const // private members //////////////////////////////////////////////////////////////////////////////// -void APE::File::read(bool readProperties, Properties::ReadStyle /* propertiesStyle */) +void APE::File::read(bool readProperties) { + // Look for an ID3v2 tag + + d->ID3v2Location = findID3v2(); + + if(d->ID3v2Location >= 0) { + seek(d->ID3v2Location); + d->ID3v2Header = new ID3v2::Header(readBlock(ID3v2::Header::size())); + d->ID3v2Size = d->ID3v2Header->completeTagSize(); + d->hasID3v2 = true; + } + // Look for an ID3v1 tag d->ID3v1Location = findID3v1(); @@ -277,7 +299,25 @@ void APE::File::read(bool readProperties, Properties::ReadStyle /* propertiesSty // Look for APE audio properties if(readProperties) { - d->properties = new Properties(this); + + long streamLength; + + if(d->hasAPE) + streamLength = d->APELocation; + else if(d->hasID3v1) + streamLength = d->ID3v1Location; + else + streamLength = length(); + + if(d->hasID3v2) { + seek(d->ID3v2Location + d->ID3v2Size); + streamLength -= (d->ID3v2Location + d->ID3v2Size); + } + else { + seek(0); + } + + d->properties = new Properties(this, streamLength); } } @@ -312,3 +352,16 @@ long APE::File::findID3v1() return -1; } + +long APE::File::findID3v2() +{ + if(!isValid()) + return -1; + + seek(0); + + if(readBlock(3) == ID3v2::Header::fileIdentifier()) + return 0; + + return -1; +} diff --git a/taglib/ape/apefile.h b/taglib/ape/apefile.h index 4d806d1a..1a64f8b5 100644 --- a/taglib/ape/apefile.h +++ b/taglib/ape/apefile.h @@ -215,9 +215,10 @@ namespace TagLib { File(const File &); File &operator=(const File &); - void read(bool readProperties, Properties::ReadStyle propertiesStyle); - long findID3v1(); + void read(bool readProperties); long findAPE(); + long findID3v1(); + long findID3v2(); class FilePrivate; FilePrivate *d; diff --git a/taglib/ape/apeproperties.cpp b/taglib/ape/apeproperties.cpp index cf829fe5..4a339456 100644 --- a/taglib/ape/apeproperties.cpp +++ b/taglib/ape/apeproperties.cpp @@ -33,22 +33,22 @@ #include "id3v2tag.h" #include "apeproperties.h" #include "apefile.h" +#include "apetag.h" +#include "apefooter.h" using namespace TagLib; class APE::Properties::PropertiesPrivate { public: - PropertiesPrivate(File *file, long streamLength) : + PropertiesPrivate() : length(0), bitrate(0), sampleRate(0), channels(0), version(0), bitsPerSample(0), - sampleFrames(0), - file(file), - streamLength(streamLength) {} + sampleFrames(0) {} int length; int bitrate; @@ -57,18 +57,24 @@ public: int version; int bitsPerSample; uint sampleFrames; - File *file; - long streamLength; }; //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// -APE::Properties::Properties(File *file, ReadStyle style) : AudioProperties(style) +APE::Properties::Properties(File *, ReadStyle style) : + AudioProperties(style), + d(new PropertiesPrivate()) { - d = new PropertiesPrivate(file, file->length()); - read(); + debug("APE::Properties::Properties() -- This constructor is no longer used."); +} + +APE::Properties::Properties(File *file, long streamLength, ReadStyle style) : + AudioProperties(style), + d(new PropertiesPrivate()) +{ + read(file, streamLength); } APE::Properties::~Properties() @@ -77,6 +83,16 @@ APE::Properties::~Properties() } int APE::Properties::length() const +{ + return lengthInSeconds(); +} + +int APE::Properties::lengthInSeconds() const +{ + return d->length / 1000; +} + +int APE::Properties::lengthInMilliseconds() const { return d->length; } @@ -115,98 +131,93 @@ TagLib::uint APE::Properties::sampleFrames() const // private members //////////////////////////////////////////////////////////////////////////////// - -void APE::Properties::read() +namespace { - // First we are searching the descriptor - long offset = findDescriptor(); - if(offset < 0) - return; + inline int headerVersion(const ByteVector &header) + { + if(header.size() < 6 || !header.startsWith("MAC ")) + return -1; - // Then we read the header common for all versions of APE - d->file->seek(offset); - ByteVector commonHeader = d->file->readBlock(6); - if(!commonHeader.startsWith("MAC ")) - return; - d->version = commonHeader.toUShort(4, false); - - if(d->version >= 3980) { - analyzeCurrent(); - } - else { - analyzeOld(); + return header.toUShort(4, false); } } -long APE::Properties::findDescriptor() +void APE::Properties::read(File *file, long streamLength) { - long ID3v2Location = findID3v2(); - long ID3v2OriginalSize = 0; - bool hasID3v2 = false; - if(ID3v2Location >= 0) { - ID3v2::Tag tag(d->file, ID3v2Location); - ID3v2OriginalSize = tag.header()->completeTagSize(); - if(tag.header()->tagSize() > 0) - hasID3v2 = true; + // First, we assume that the file pointer is set at the first descriptor. + long offset = file->tell(); + int version = headerVersion(file->readBlock(6)); + + // Next, we look for the descriptor. + if(version < 0) { + offset = file->find("MAC ", offset); + file->seek(offset); + version = headerVersion(file->readBlock(6)); } - long offset = 0; - if(hasID3v2) - offset = d->file->find("MAC ", ID3v2Location + ID3v2OriginalSize); + if(version < 0) { + debug("APE::Properties::read() -- APE descriptor not found"); + return; + } + + d->version = version; + + if(d->version >= 3980) + analyzeCurrent(file); else - offset = d->file->find("MAC "); + analyzeOld(file); - if(offset < 0) { - debug("APE::Properties::findDescriptor() -- APE descriptor not found"); - return -1; + if(d->sampleFrames > 0 && d->sampleRate > 0) { + const double length = d->sampleFrames * 1000.0 / d->sampleRate; + d->length = static_cast(length + 0.5); + d->bitrate = static_cast(streamLength * 8.0 / length + 0.5); } - - return offset; } -long APE::Properties::findID3v2() -{ - if(!d->file->isValid()) - return -1; - - d->file->seek(0); - - if(d->file->readBlock(3) == ID3v2::Header::fileIdentifier()) - return 0; - - return -1; -} - -void APE::Properties::analyzeCurrent() +void APE::Properties::analyzeCurrent(File *file) { // Read the descriptor - d->file->seek(2, File::Current); - ByteVector descriptor = d->file->readBlock(44); + file->seek(2, File::Current); + const ByteVector descriptor = file->readBlock(44); + if(descriptor.size() < 44) { + debug("APE::Properties::analyzeCurrent() -- descriptor is too short."); + return; + } + const uint descriptorBytes = descriptor.toUInt(0, false); - if ((descriptorBytes - 52) > 0) - d->file->seek(descriptorBytes - 52, File::Current); + if((descriptorBytes - 52) > 0) + file->seek(descriptorBytes - 52, File::Current); // Read the header - ByteVector header = d->file->readBlock(24); + const ByteVector header = file->readBlock(24); + if(header.size() < 24) { + debug("APE::Properties::analyzeCurrent() -- MAC header is too short."); + return; + } // Get the APE info d->channels = header.toShort(18, false); d->sampleRate = header.toUInt(20, false); d->bitsPerSample = header.toShort(16, false); - //d->compressionLevel = - const uint totalFrames = header.toUInt(12, false); + const uint totalFrames = header.toUInt(12, false); + if(totalFrames == 0) + return; + const uint blocksPerFrame = header.toUInt(4, false); const uint finalFrameBlocks = header.toUInt(8, false); - d->sampleFrames = totalFrames > 0 ? (totalFrames - 1) * blocksPerFrame + finalFrameBlocks : 0; - d->length = d->sampleRate > 0 ? d->sampleFrames / d->sampleRate : 0; - d->bitrate = d->length > 0 ? ((d->streamLength * 8L) / d->length) / 1000 : 0; + d->sampleFrames = (totalFrames - 1) * blocksPerFrame + finalFrameBlocks; } -void APE::Properties::analyzeOld() +void APE::Properties::analyzeOld(File *file) { - ByteVector header = d->file->readBlock(26); + const ByteVector header = file->readBlock(26); + if(header.size() < 26) { + debug("APE::Properties::analyzeOld() -- MAC header is too short."); + return; + } + const uint totalFrames = header.toUInt(18, false); // Fail on 0 length APE files (catches non-finalized APE files) @@ -222,19 +233,20 @@ void APE::Properties::analyzeOld() else blocksPerFrame = 9216; + // Get the APE info d->channels = header.toShort(4, false); d->sampleRate = header.toUInt(6, false); const uint finalFrameBlocks = header.toUInt(22, false); + d->sampleFrames = (totalFrames - 1) * blocksPerFrame + finalFrameBlocks; - uint totalBlocks = 0; - if(totalFrames > 0) - totalBlocks = (totalFrames - 1) * blocksPerFrame + finalFrameBlocks; + // Get the bit depth from the RIFF-fmt chunk. + file->seek(16, File::Current); + const ByteVector fmt = file->readBlock(28); + if(fmt.size() < 28 || !fmt.startsWith("WAVEfmt ")) { + debug("APE::Properties::analyzeOld() -- fmt header is too short."); + return; + } - if(d->sampleRate > 0) - d->length = totalBlocks / d->sampleRate; - - if(d->length > 0) - d->bitrate = ((d->streamLength * 8L) / d->length) / 1000; + d->bitsPerSample = fmt.toShort(26, false); } - diff --git a/taglib/ape/apeproperties.h b/taglib/ape/apeproperties.h index f154ec34..fcf125ff 100644 --- a/taglib/ape/apeproperties.h +++ b/taglib/ape/apeproperties.h @@ -51,26 +51,73 @@ namespace TagLib { public: /*! * Create an instance of APE::Properties with the data read from the - * ByteVector \a data. + * APE::File \a file. + * + * \deprecated */ - Properties(File *f, ReadStyle style = Average); + Properties(File *file, ReadStyle style = Average); + + /*! + * Create an instance of APE::Properties with the data read from the + * APE::File \a file. + */ + Properties(File *file, long streamLength, ReadStyle style = Average); /*! * Destroys this APE::Properties instance. */ 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 number of bits per sample. + * Returns the number of bits per audio sample. */ int bitsPerSample() const; + + /*! + * Returns the total number of audio samples in file. + */ uint sampleFrames() const; /*! @@ -82,13 +129,10 @@ namespace TagLib { Properties(const Properties &); Properties &operator=(const Properties &); - void read(); + void read(File *file, long streamLength); - long findDescriptor(); - long findID3v2(); - - void analyzeCurrent(); - void analyzeOld(); + void analyzeCurrent(File *file); + void analyzeOld(File *file); class PropertiesPrivate; PropertiesPrivate *d; diff --git a/tests/data/mac-399-id3v2.ape b/tests/data/mac-399-id3v2.ape new file mode 100644 index 00000000..2ea97fc4 Binary files /dev/null and b/tests/data/mac-399-id3v2.ape differ diff --git a/tests/data/mac-399-tagged.ape b/tests/data/mac-399-tagged.ape new file mode 100644 index 00000000..3f5a656e Binary files /dev/null and b/tests/data/mac-399-tagged.ape differ diff --git a/tests/data/mac-399.ape b/tests/data/mac-399.ape index ae895ba2..3b0661ee 100644 Binary files a/tests/data/mac-399.ape and b/tests/data/mac-399.ape differ diff --git a/tests/test_ape.cpp b/tests/test_ape.cpp index 2f842f65..3ab3197b 100644 --- a/tests/test_ape.cpp +++ b/tests/test_ape.cpp @@ -14,6 +14,8 @@ class TestAPE : public CppUnit::TestFixture { CPPUNIT_TEST_SUITE(TestAPE); CPPUNIT_TEST(testProperties399); + CPPUNIT_TEST(testProperties399Tagged); + CPPUNIT_TEST(testProperties399Id3v2); CPPUNIT_TEST(testProperties396); CPPUNIT_TEST(testProperties390); CPPUNIT_TEST(testFuzzedFile1); @@ -25,28 +27,76 @@ public: void testProperties399() { APE::File f(TEST_FILE_PATH_C("mac-399.ape")); + CPPUNIT_ASSERT(f.audioProperties()); CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->length()); - CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(3550, f.audioProperties()->lengthInMilliseconds()); + CPPUNIT_ASSERT_EQUAL(192, f.audioProperties()->bitrate()); CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(16, f.audioProperties()->bitsPerSample()); + CPPUNIT_ASSERT_EQUAL(156556U, f.audioProperties()->sampleFrames()); + CPPUNIT_ASSERT_EQUAL(3990, f.audioProperties()->version()); + } + + void testProperties399Tagged() + { + APE::File f(TEST_FILE_PATH_C("mac-399-tagged.ape")); + CPPUNIT_ASSERT(f.audioProperties()); + CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(3550, f.audioProperties()->lengthInMilliseconds()); + CPPUNIT_ASSERT_EQUAL(192, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(16, f.audioProperties()->bitsPerSample()); + CPPUNIT_ASSERT_EQUAL(156556U, f.audioProperties()->sampleFrames()); + CPPUNIT_ASSERT_EQUAL(3990, f.audioProperties()->version()); + } + + void testProperties399Id3v2() + { + APE::File f(TEST_FILE_PATH_C("mac-399-id3v2.ape")); + CPPUNIT_ASSERT(f.audioProperties()); + CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(3550, f.audioProperties()->lengthInMilliseconds()); + CPPUNIT_ASSERT_EQUAL(192, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(16, f.audioProperties()->bitsPerSample()); + CPPUNIT_ASSERT_EQUAL(156556U, f.audioProperties()->sampleFrames()); + CPPUNIT_ASSERT_EQUAL(3990, f.audioProperties()->version()); } void testProperties396() { APE::File f(TEST_FILE_PATH_C("mac-396.ape")); + CPPUNIT_ASSERT(f.audioProperties()); CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(3685, f.audioProperties()->lengthInMilliseconds()); CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->bitrate()); CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(16, f.audioProperties()->bitsPerSample()); + CPPUNIT_ASSERT_EQUAL(162496U, f.audioProperties()->sampleFrames()); + CPPUNIT_ASSERT_EQUAL(3960, f.audioProperties()->version()); } void testProperties390() { APE::File f(TEST_FILE_PATH_C("mac-390-hdr.ape")); + CPPUNIT_ASSERT(f.audioProperties()); CPPUNIT_ASSERT_EQUAL(15, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(15, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(15630, f.audioProperties()->lengthInMilliseconds()); CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->bitrate()); CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(16, f.audioProperties()->bitsPerSample()); + CPPUNIT_ASSERT_EQUAL(689262U, f.audioProperties()->sampleFrames()); + CPPUNIT_ASSERT_EQUAL(3900, f.audioProperties()->version()); } void testFuzzedFile1()