diff --git a/taglib/wavpack/wavpackproperties.cpp b/taglib/wavpack/wavpackproperties.cpp index c1d04fd2..d5808be5 100644 --- a/taglib/wavpack/wavpackproperties.cpp +++ b/taglib/wavpack/wavpackproperties.cpp @@ -27,6 +27,7 @@ * http://www.mozilla.org/MPL/ * ***************************************************************************/ +#include <stdint.h> #include <tstring.h> #include <tdebug.h> @@ -138,16 +139,10 @@ unsigned int WavPack::Properties::sampleFrames() const // private members //////////////////////////////////////////////////////////////////////////////// -namespace -{ - const unsigned int sample_rates[] = { - 6000, 8000, 9600, 11025, 12000, 16000, 22050, 24000, - 32000, 44100, 48000, 64000, 88200, 96000, 192000, 0 }; -} - #define BYTES_STORED 3 #define MONO_FLAG 4 -#define LOSSLESS_FLAG 8 +#define HYBRID_FLAG 8 +#define DSD_FLAG 0x80000000 // block is encoded DSD (1-bit PCM) #define SHIFT_LSB 13 #define SHIFT_MASK (0x1fL << SHIFT_LSB) @@ -158,8 +153,110 @@ namespace #define MIN_STREAM_VERS 0x402 #define MAX_STREAM_VERS 0x410 +#define INITIAL_BLOCK 0x800 #define FINAL_BLOCK 0x1000 +#define ID_DSD_BLOCK 0x0e +#define ID_OPTIONAL_DATA 0x20 +#define ID_UNIQUE 0x3f +#define ID_ODD_SIZE 0x40 +#define ID_LARGE 0x80 +#define ID_SAMPLE_RATE (ID_OPTIONAL_DATA | 0x7) + +namespace +{ + const unsigned int sampleRates[] = { + 6000, 8000, 9600, 11025, 12000, 16000, 22050, 24000, + 32000, 44100, 48000, 64000, 88200, 96000, 192000, 0 }; + + /*! + * Given a WavPack \a block (complete, but not including the 32-byte header), + * parse the metadata blocks until an \a id block is found and return the + * contained data, or zero if no such block is found. + * Supported values for \a id are ID_SAMPLE_RATE and ID_DSD_BLOCK. + */ + int getMetaDataChunk(const ByteVector &block, unsigned char id) + { + if(id != ID_SAMPLE_RATE && id != ID_DSD_BLOCK) + return 0; + + const int blockSize = static_cast<int>(block.size()); + int index = 0; + + while(index + 1 < blockSize) { + const unsigned char metaId = static_cast<unsigned char>(block[index]); + int metaBc = static_cast<unsigned char>(block[index + 1]) << 1; + index += 2; + + if(metaId & ID_LARGE) { + if(index + 2 > blockSize) + return 0; + + metaBc += (static_cast<uint32_t>(static_cast<unsigned char>(block[index])) << 9) + + (static_cast<uint32_t>(static_cast<unsigned char>(block[index + 1])) << 17); + index += 2; + } + + if(index + metaBc > blockSize) + return 0; + + // if we got a sample rate, return it + + if(id == ID_SAMPLE_RATE && (metaId & ID_UNIQUE) == ID_SAMPLE_RATE && metaBc == 4) { + int sampleRate = static_cast<int32_t>(static_cast<unsigned char>(block[index])); + sampleRate |= static_cast<int32_t>(static_cast<unsigned char>(block[index + 1])) << 8; + sampleRate |= static_cast<int32_t>(static_cast<unsigned char>(block[index + 2])) << 16; + + // only use 4th byte if it's really there + + if(!(metaId & ID_ODD_SIZE)) + sampleRate |= static_cast<int32_t>(static_cast<unsigned char>(block[index + 3]) & 0x7f) << 24; + + return sampleRate; + } + + // if we got DSD block, return the specified rate shift amount + + if(id == ID_DSD_BLOCK && (metaId & ID_UNIQUE) == ID_DSD_BLOCK && metaBc > 0) { + const unsigned char rateShift = static_cast<unsigned char>(block[index]); + if(rateShift <= 31) + return rateShift; + } + + index += metaBc; + } + + return 0; + } + + /*! + * Given a WavPack block (complete, but not including the 32-byte header), + * parse the metadata blocks until an ID_SAMPLE_RATE block is found and + * return the non-standard sample rate contained there, or zero if no such + * block is found. + */ + int getNonStandardRate(const ByteVector &block) + { + return getMetaDataChunk(block, ID_SAMPLE_RATE); + } + + /*! + * Given a WavPack block (complete, but not including the 32-byte header), + * parse the metadata blocks until a DSD audio data block is found and return + * the sample-rate shift value contained there, or zero if no such block is + * found. The nominal sample rate of DSD audio files (found in the header) + * must be left-shifted by this amount to get the actual "byte" sample rate. + * Note that 8-bit bytes are the "atoms" of the DSD audio coding (for + * decoding, seeking, etc), so the shifted rate must be further multiplied by + * 8 to get the actual DSD bit sample rate. + */ + int getDsdRateShifter(const ByteVector &block) + { + return getMetaDataChunk(block, ID_DSD_BLOCK); + } + +} + void WavPack::Properties::read(File *file, long streamLength) { long offset = 0; @@ -178,17 +275,50 @@ void WavPack::Properties::read(File *file, long streamLength) break; } + const unsigned int blockSize = data.toUInt(4, false); + const unsigned int sampleFrames = data.toUInt(12, false); + const unsigned int blockSamples = data.toUInt(20, false); const unsigned int flags = data.toUInt(24, false); + unsigned int sampleRate = sampleRates[(flags & SRATE_MASK) >> SRATE_LSB]; - if(offset == 0) { + if(!blockSamples) { // ignore blocks with no samples + offset += blockSize + 8; + continue; + } + + if(blockSize < 24 || blockSize > 1048576) { + debug("WavPack::Properties::read() -- Invalid block header found."); + break; + } + + // For non-standard sample rates or DSD audio files, we must read and parse the block + // to actually determine the sample rate. + + if(!sampleRate || (flags & DSD_FLAG)) { + const unsigned int adjustedBlockSize = blockSize - 24; + const ByteVector block = file->readBlock(adjustedBlockSize); + + if(block.size() < adjustedBlockSize) { + debug("WavPack::Properties::read() -- block is too short."); + break; + } + + if(!sampleRate) + sampleRate = static_cast<unsigned int>(getNonStandardRate(block)); + + if(sampleRate && (flags & DSD_FLAG)) + sampleRate <<= getDsdRateShifter(block); + } + + if(flags & INITIAL_BLOCK) { d->version = data.toShort(8, false); if(d->version < MIN_STREAM_VERS || d->version > MAX_STREAM_VERS) break; d->bitsPerSample = ((flags & BYTES_STORED) + 1) * 8 - ((flags & SHIFT_MASK) >> SHIFT_LSB); - d->sampleRate = sample_rates[(flags & SRATE_MASK) >> SRATE_LSB]; - d->lossless = !(flags & LOSSLESS_FLAG); - d->sampleFrames = data.toUInt(12, false); + d->sampleRate = static_cast<int>(sampleRate); + d->lossless = !(flags & HYBRID_FLAG); + d->sampleFrames = sampleFrames; } d->channels += (flags & MONO_FLAG) ? 1 : 2; @@ -196,7 +326,6 @@ void WavPack::Properties::read(File *file, long streamLength) if(flags & FINAL_BLOCK) break; - const unsigned int blockSize = data.toUInt(4, false); offset += blockSize + 8; } @@ -212,25 +341,34 @@ void WavPack::Properties::read(File *file, long streamLength) unsigned int WavPack::Properties::seekFinalIndex(File *file, long streamLength) { - const long offset = file->rfind("wvpk", streamLength); - if(offset == -1) - return 0; + long offset = streamLength; - file->seek(offset); - const ByteVector data = file->readBlock(32); - if(data.size() < 32) - return 0; + while (offset >= 32) { + offset = file->rfind("wvpk", offset - 4); - const int version = data.toShort(8, false); - if(version < MIN_STREAM_VERS || version > MAX_STREAM_VERS) - return 0; + if(offset == -1) + return 0; - const unsigned int flags = data.toUInt(24, false); - if(!(flags & FINAL_BLOCK)) - return 0; + file->seek(offset); + const ByteVector data = file->readBlock(32); + if(data.size() < 32) + return 0; - const unsigned int blockIndex = data.toUInt(16, false); - const unsigned int blockSamples = data.toUInt(20, false); + const unsigned int blockSize = data.toUInt(4, false); + const unsigned int blockIndex = data.toUInt(16, false); + const unsigned int blockSamples = data.toUInt(20, false); + const unsigned int flags = data.toUInt(24, false); + const int version = data.toShort(8, false); - return blockIndex + blockSamples; + // try not to trigger on a spurious "wvpk" in WavPack binary block data + + if(version < MIN_STREAM_VERS || version > MAX_STREAM_VERS || (blockSize & 1) || + blockSize < 24 || blockSize >= 1048576 || blockSamples > 131072) + continue; + + if (blockSamples && (flags & FINAL_BLOCK)) + return blockIndex + blockSamples; + } + + return 0; } diff --git a/tests/data/dsd_stereo.wv b/tests/data/dsd_stereo.wv new file mode 100644 index 00000000..80619270 Binary files /dev/null and b/tests/data/dsd_stereo.wv differ diff --git a/tests/data/non_standard_rate.wv b/tests/data/non_standard_rate.wv new file mode 100644 index 00000000..ccc90277 Binary files /dev/null and b/tests/data/non_standard_rate.wv differ diff --git a/tests/test_wavpack.cpp b/tests/test_wavpack.cpp index 6c64f08d..591529fb 100644 --- a/tests/test_wavpack.cpp +++ b/tests/test_wavpack.cpp @@ -41,6 +41,8 @@ class TestWavPack : public CppUnit::TestFixture CPPUNIT_TEST_SUITE(TestWavPack); CPPUNIT_TEST(testNoLengthProperties); CPPUNIT_TEST(testMultiChannelProperties); + CPPUNIT_TEST(testDsdStereoProperties); + CPPUNIT_TEST(testNonStandardRateProperties); CPPUNIT_TEST(testTaggedProperties); CPPUNIT_TEST(testFuzzedFile); CPPUNIT_TEST(testStripAndProperties); @@ -79,6 +81,36 @@ public: CPPUNIT_ASSERT_EQUAL(1031, f.audioProperties()->version()); } + void testDsdStereoProperties() + { + WavPack::File f(TEST_FILE_PATH_C("dsd_stereo.wv")); + CPPUNIT_ASSERT(f.audioProperties()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(200, f.audioProperties()->lengthInMilliseconds()); + CPPUNIT_ASSERT_EQUAL(2096, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(8, f.audioProperties()->bitsPerSample()); + CPPUNIT_ASSERT_EQUAL(true, f.audioProperties()->isLossless()); + CPPUNIT_ASSERT_EQUAL(352800, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(70560U, f.audioProperties()->sampleFrames()); + CPPUNIT_ASSERT_EQUAL(1040, f.audioProperties()->version()); + } + + void testNonStandardRateProperties() + { + WavPack::File f(TEST_FILE_PATH_C("non_standard_rate.wv")); + CPPUNIT_ASSERT(f.audioProperties()); + CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(3675, f.audioProperties()->lengthInMilliseconds()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(16, f.audioProperties()->bitsPerSample()); + CPPUNIT_ASSERT_EQUAL(true, f.audioProperties()->isLossless()); + CPPUNIT_ASSERT_EQUAL(1000, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(3675U, f.audioProperties()->sampleFrames()); + CPPUNIT_ASSERT_EQUAL(1040, f.audioProperties()->version()); + } + void testTaggedProperties() { WavPack::File f(TEST_FILE_PATH_C("tagged.wv"));