diff --git a/taglib/mp4/mp4properties.cpp b/taglib/mp4/mp4properties.cpp index c932ed0d..38f83269 100644 --- a/taglib/mp4/mp4properties.cpp +++ b/taglib/mp4/mp4properties.cpp @@ -27,6 +27,7 @@ #include "tdebug.h" #include "tstring.h" +#include "tmap.h" #include "mp4file.h" #include "mp4atom.h" @@ -63,6 +64,7 @@ public: int bitsPerSample { 0 }; bool encrypted { false }; Codec codec { MP4::Properties::Unknown }; + String codecId; }; //////////////////////////////////////////////////////////////////////////////// @@ -120,6 +122,12 @@ MP4::Properties::codec() const return d->codec; } +String +MP4::Properties::codecId() const +{ + return d->codecId; +} + //////////////////////////////////////////////////////////////////////////////// // private members //////////////////////////////////////////////////////////////////////////////// @@ -205,6 +213,12 @@ MP4::Properties::read(File *file, const Atoms *atoms) file->seek(atom->offset()); data = file->readBlock(atom->length()); + + // The four character code of the first sample entry identifies the codec and + // is exposed directly, also for codecs not represented by the Codec enum. + if(data.size() >= 24) + d->codecId = String(data.mid(20, 4)); + if(data.containsAt("mp4a", 20)) { d->codec = AAC; d->channels = data.toShort(40U); @@ -249,6 +263,53 @@ MP4::Properties::read(File *file, const Atoms *atoms) } } } + else if(data.size() >= 50) { + // Other audio codecs use the same AudioSampleEntry layout as above, so the + // basic properties can be read from the same offsets. The nominal bitrate + // is not parsed from the codec specific configuration box; it is estimated + // from the audio data size and the duration. + static const Map codecMap { + {"ac-3", AC3}, + {"ec-3", EAC3}, + {"fLaC", FLAC}, + {"Opus", Opus}, + {"dtsc", DTS}, + {"dtse", DTS}, + {"dtsh", DTS}, + {"dtsl", DTS} + }; + d->codec = codecMap.value(data.mid(20, 4), Unknown); + d->channels = data.toShort(40U); + d->bitsPerSample = data.toShort(42U); + d->sampleRate = data.toUInt(46U); + + if(d->codec == FLAC) { + // The AudioSampleEntry sample rate field is only 16 bits wide, so read + // the exact values from the FLAC STREAMINFO metadata block in the 'dfLa' + // box. This is required for high resolution files (e.g. 96 kHz, 24 bit). + const auto dflaPos = data.find("dfLa"); + if(const auto dflaOffset = static_cast(dflaPos); + dflaPos >= 0 && data.size() >= dflaOffset + 26 && + (static_cast(data.at(dflaOffset + 8)) & 0x7f) == 0) { + const auto streamInfo = data.toUInt(dflaOffset + 22); + if(const auto sampleRate = streamInfo >> 12; sampleRate != 0) + d->sampleRate = sampleRate; + d->channels = ((streamInfo >> 9) & 0x7) + 1; + d->bitsPerSample = ((streamInfo >> 4) & 0x1f) + 1; + } + } + else if(d->codec == EAC3) { + // The 'dec3' box (EC3SpecificBox) starts with the nominal data rate in + // kbit/s as a 13 bit value. + const auto dec3Pos = data.find("dec3"); + if(const auto dec3Offset = static_cast(dec3Pos); + dec3Pos >= 0 && data.size() >= dec3Offset + 6) + d->bitrate = data.toUShort(dec3Offset + 4) >> 3; + } + + if(d->bitrate == 0 && d->length > 0) + d->bitrate = static_cast(calculateMdatLength(atoms->atoms()) * 8 / d->length); + } if(atom->find("drms")) { d->encrypted = true; diff --git a/taglib/mp4/mp4properties.h b/taglib/mp4/mp4properties.h index a2f1de8d..33b4805b 100644 --- a/taglib/mp4/mp4properties.h +++ b/taglib/mp4/mp4properties.h @@ -27,6 +27,7 @@ #define TAGLIB_MP4PROPERTIES_H #include "taglib_export.h" +#include "tstring.h" #include "audioproperties.h" namespace TagLib { @@ -41,7 +42,12 @@ namespace TagLib { enum Codec { Unknown = 0, AAC, - ALAC + ALAC, + AC3, + EAC3, + FLAC, + DTS, + Opus }; Properties(File *file, const Atoms *atoms, ReadStyle style = Average); @@ -87,6 +93,13 @@ namespace TagLib { */ Codec codec() const; + /*! + * Returns the four character code identifying the codec, as found in the + * sample description atom (e.g. "mp4a", "alac", "ec-3"). This is useful + * for codecs which are not represented by a value of the Codec enum. + */ + String codecId() const; + private: void read(File *file, const Atoms *atoms); diff --git a/tests/data/ac3.m4a b/tests/data/ac3.m4a new file mode 100644 index 00000000..ba4a717c Binary files /dev/null and b/tests/data/ac3.m4a differ diff --git a/tests/data/eac3.m4a b/tests/data/eac3.m4a new file mode 100644 index 00000000..0f45ad06 Binary files /dev/null and b/tests/data/eac3.m4a differ diff --git a/tests/data/flac.m4a b/tests/data/flac.m4a new file mode 100644 index 00000000..4da82388 Binary files /dev/null and b/tests/data/flac.m4a differ diff --git a/tests/data/flac96.m4a b/tests/data/flac96.m4a new file mode 100644 index 00000000..0c693f1b Binary files /dev/null and b/tests/data/flac96.m4a differ diff --git a/tests/data/opus.m4a b/tests/data/opus.m4a new file mode 100644 index 00000000..110234df Binary files /dev/null and b/tests/data/opus.m4a differ diff --git a/tests/test_mp4.cpp b/tests/test_mp4.cpp index a1f396d8..c64e2a40 100644 --- a/tests/test_mp4.cpp +++ b/tests/test_mp4.cpp @@ -130,6 +130,11 @@ class TestMP4 : public CppUnit::TestFixture CPPUNIT_TEST(testPropertiesALAC); CPPUNIT_TEST(testPropertiesALACWithoutBitrate); CPPUNIT_TEST(testPropertiesAACWithoutLength); + CPPUNIT_TEST(testPropertiesAC3); + CPPUNIT_TEST(testPropertiesEAC3); + CPPUNIT_TEST(testPropertiesFLAC); + CPPUNIT_TEST(testPropertiesFLACHighResolution); + CPPUNIT_TEST(testPropertiesOpus); CPPUNIT_TEST(testPropertiesM4V); CPPUNIT_TEST(testFreeForm); CPPUNIT_TEST(testCheckValid); @@ -271,6 +276,75 @@ public: CPPUNIT_ASSERT_EQUAL(MP4::Properties::AAC, f.audioProperties()->codec()); } + void testPropertiesAC3() + { + MP4::File f(TEST_FILE_PATH_C("ac3.m4a")); + CPPUNIT_ASSERT(f.audioProperties()); + CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(48000, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT(f.audioProperties()->bitrate() > 0); + CPPUNIT_ASSERT_EQUAL(false, f.audioProperties()->isEncrypted()); + CPPUNIT_ASSERT_EQUAL(MP4::Properties::AC3, f.audioProperties()->codec()); + CPPUNIT_ASSERT_EQUAL(String("ac-3"), f.audioProperties()->codecId()); + } + + void testPropertiesEAC3() + { + MP4::File f(TEST_FILE_PATH_C("eac3.m4a")); + CPPUNIT_ASSERT(f.audioProperties()); + CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(48000, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(128, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(false, f.audioProperties()->isEncrypted()); + CPPUNIT_ASSERT_EQUAL(MP4::Properties::EAC3, f.audioProperties()->codec()); + CPPUNIT_ASSERT_EQUAL(String("ec-3"), f.audioProperties()->codecId()); + } + + void testPropertiesFLAC() + { + MP4::File f(TEST_FILE_PATH_C("flac.m4a")); + CPPUNIT_ASSERT(f.audioProperties()); + CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(48000, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(16, f.audioProperties()->bitsPerSample()); + CPPUNIT_ASSERT(f.audioProperties()->bitrate() > 0); + CPPUNIT_ASSERT_EQUAL(false, f.audioProperties()->isEncrypted()); + CPPUNIT_ASSERT_EQUAL(MP4::Properties::FLAC, f.audioProperties()->codec()); + CPPUNIT_ASSERT_EQUAL(String("fLaC"), f.audioProperties()->codecId()); + } + + void testPropertiesFLACHighResolution() + { + // The AudioSampleEntry sample rate field cannot hold rates above 65535 Hz, + // so the exact values must come from the FLAC STREAMINFO in the 'dfLa' box. + MP4::File f(TEST_FILE_PATH_C("flac96.m4a")); + CPPUNIT_ASSERT(f.audioProperties()); + CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(96000, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(24, f.audioProperties()->bitsPerSample()); + CPPUNIT_ASSERT(f.audioProperties()->bitrate() > 0); + CPPUNIT_ASSERT_EQUAL(false, f.audioProperties()->isEncrypted()); + CPPUNIT_ASSERT_EQUAL(MP4::Properties::FLAC, f.audioProperties()->codec()); + CPPUNIT_ASSERT_EQUAL(String("fLaC"), f.audioProperties()->codecId()); + } + + void testPropertiesOpus() + { + MP4::File f(TEST_FILE_PATH_C("opus.m4a")); + CPPUNIT_ASSERT(f.audioProperties()); + CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(48000, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT(f.audioProperties()->bitrate() > 0); + CPPUNIT_ASSERT_EQUAL(false, f.audioProperties()->isEncrypted()); + CPPUNIT_ASSERT_EQUAL(MP4::Properties::Opus, f.audioProperties()->codec()); + CPPUNIT_ASSERT_EQUAL(String("Opus"), f.audioProperties()->codecId()); + } + void testPropertiesM4V() { MP4::File f(TEST_FILE_PATH_C("blank_video.m4v"));