Support additional MP4 codecs (#1293, #1338)

Adds MP4::Properties::Codec enum values AC3, EAC3, FLAC, DTS, Opus and a
MP4::Properties::codecId() method.
Parse high-res FLAC bitrate from MP4 dfLa box.
Parse E-AC-3 bitrate from MP4 dec3 box.

The test files were generated using ffmpeg:

ffmpeg -hide_banner -y -f lavfi \
  -i "sine=frequency=440:sample_rate=48000:duration=1" \
  -c:a flac -ac 2 -flags +bitexact -fflags +bitexact \
  -metadata:s:a:0 encoder= -metadata encoder= \
  -f mp4 tests/data/flac.m4a

ffmpeg -hide_banner -y -f lavfi \
  -i "sine=frequency=440:sample_rate=48000:duration=1" \
  -c:a libopus -ac 2 -flags +bitexact -fflags +bitexact \
  -metadata:s:a:0 encoder= -metadata encoder= \
  -f mp4 tests/data/opus.m4a

ffmpeg -hide_banner -y -f lavfi \
  -i "sine=frequency=440:sample_rate=48000:duration=1" \
  -c:a ac3 -b:a 128k -ac 2 -strict experimental \
  -flags +bitexact -fflags +bitexact \
  -metadata:s:a:0 encoder= -metadata encoder= \
  -f mp4 tests/data/ac3.m4a

ffmpeg -hide_banner -y -f lavfi \
  -i "sine=frequency=440:sample_rate=48000:duration=1" \
  -c:a eac3 -b:a 128k -ac 2 -strict experimental \
  -flags +bitexact -fflags +bitexact \
  -metadata:s:a:0 encoder= -metadata encoder= \
  -f mp4 tests/data/eac3.m4a

ffmpeg -hide_banner -y -f lavfi \
  -i "sine=frequency=440:sample_rate=96000:duration=1" \
  -c:a flac -sample_fmt s32 -ac 2 -flags +bitexact -fflags +bitexact \
  -metadata:s:a:0 encoder= -metadata encoder= \
  -f mp4 tests/data/flac96.m4a
This commit is contained in:
Urs Fleisch
2026-08-07 14:54:50 +02:00
committed by GitHub
parent ce3b45f186
commit 2a5fc12b68
8 changed files with 149 additions and 1 deletions
+61
View File
@@ -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<ByteVector, Codec> 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<unsigned int>(dflaPos);
dflaPos >= 0 && data.size() >= dflaOffset + 26 &&
(static_cast<unsigned char>(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<unsigned int>(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<int>(calculateMdatLength(atoms->atoms()) * 8 / d->length);
}
if(atom->find("drms")) {
d->encrypted = true;
+14 -1
View File
@@ -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);
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+74
View File
@@ -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"));