MP4: Add support for NI STEM (#1299)

This commit is contained in:
Antoine Colombier
2026-01-25 12:33:54 +00:00
committed by GitHub
parent 2c01b63433
commit 11e3eb05bd
11 changed files with 411 additions and 22 deletions

View File

@ -194,6 +194,7 @@ if(WITH_MP4)
mp4/mp4item.h
mp4/mp4properties.h
mp4/mp4coverart.h
mp4/mp4stem.h
mp4/mp4itemfactory.h
)
endif()
@ -369,6 +370,7 @@ if(WITH_MP4)
mp4/mp4item.cpp
mp4/mp4properties.cpp
mp4/mp4coverart.cpp
mp4/mp4stem.cpp
mp4/mp4itemfactory.cpp
)
endif()

View File

@ -37,7 +37,7 @@ namespace {
constexpr std::array containers {
"moov", "udta", "mdia", "meta", "ilst",
"stbl", "minf", "moof", "traf", "trak",
"stsd"
"stsd", "stem"
};
} // namespace
@ -86,6 +86,11 @@ MP4::Atom::Atom(File *file)
d->name = header.mid(4, 4);
if(d->name == "stem") {
file->seek(d->length - 8, File::Current);
return;
}
for(auto c : containers) {
if(d->name == c) {
if(d->name == "meta") {

View File

@ -44,6 +44,7 @@ public:
StringList m_stringList;
ByteVectorList m_byteVectorList;
MP4::CoverArtList m_coverArtList;
MP4::Stem m_stem;
};
MP4::Item::Item() :
@ -130,6 +131,13 @@ MP4::Item::Item(const MP4::CoverArtList &value) :
d->m_coverArtList = value;
}
MP4::Item::Item(const MP4::Stem &value) :
d(std::make_shared<ItemPrivate>())
{
d->type = Type::Stem;
d->m_stem = value;
}
void MP4::Item::setAtomDataType(MP4::AtomDataType type)
{
d->atomDataType = type;
@ -194,6 +202,12 @@ MP4::Item::toCoverArtList() const
return d->m_coverArtList;
}
MP4::Stem
MP4::Item::toStem() const
{
return d->m_stem;
}
bool
MP4::Item::isValid() const
{
@ -234,6 +248,8 @@ bool MP4::Item::operator==(const Item &other) const
return toByteVectorList() == other.toByteVectorList();
case Type::CoverArtList:
return toCoverArtList() == other.toCoverArtList();
case Type::Stem:
return toStem() == other.toStem();
}
}
return false;

View File

@ -29,6 +29,7 @@
#include "tstringlist.h"
#include "taglib_export.h"
#include "mp4coverart.h"
#include "mp4stem.h"
namespace TagLib {
namespace MP4 {
@ -49,7 +50,8 @@ namespace TagLib {
LongLong,
StringList,
ByteVectorList,
CoverArtList
CoverArtList,
Stem,
};
struct IntPair {
@ -80,6 +82,7 @@ namespace TagLib {
Item(const StringList &value);
Item(const ByteVectorList &value);
Item(const CoverArtList &value);
Item(const Stem &value);
void setAtomDataType(AtomDataType type);
AtomDataType atomDataType() const;
@ -93,6 +96,7 @@ namespace TagLib {
StringList toStringList() const;
ByteVectorList toByteVectorList() const;
CoverArtList toCoverArtList() const;
Stem toStem() const;
bool isValid() const;

View File

@ -87,6 +87,8 @@ std::pair<String, Item> ItemFactory::parseItem(
return parseGnre(atom, data);
case ItemHandlerType::Covr:
return parseCovr(atom, data);
case ItemHandlerType::Stem:
return parseStem(atom, data);
case ItemHandlerType::TextImplicit:
return parseText(atom, data, -1);
case ItemHandlerType::Text:
@ -128,6 +130,8 @@ ByteVector ItemFactory::renderItem(
return renderInt(name, item);
case ItemHandlerType::Covr:
return renderCovr(name, item);
case ItemHandlerType::Stem:
return renderStem(name, item);
case ItemHandlerType::TextImplicit:
return renderText(name, item, TypeImplicit);
case ItemHandlerType::Text:
@ -175,8 +179,8 @@ std::pair<ByteVector, Item> ItemFactory::itemFromProperty(
case ItemHandlerType::TextImplicit:
case ItemHandlerType::Text:
return {name, values};
case ItemHandlerType::Covr:
case ItemHandlerType::Stem:
debug("MP4: Invalid item \"" + name + "\" for property");
break;
case ItemHandlerType::Unknown:
@ -222,6 +226,7 @@ std::pair<String, StringList> ItemFactory::itemToProperty(
return {key, item.toStringList()};
case ItemHandlerType::Covr:
case ItemHandlerType::Stem:
debug("MP4: Invalid item \"" + itemName + "\" for property");
break;
case ItemHandlerType::Unknown:
@ -303,6 +308,7 @@ ItemFactory::NameHandlerMap ItemFactory::nameHandlerMap() const
{"akID", ItemHandlerType::Byte},
{"gnre", ItemHandlerType::Gnre},
{"covr", ItemHandlerType::Covr},
{"stem", ItemHandlerType::Stem},
{"purl", ItemHandlerType::TextImplicit},
{"egid", ItemHandlerType::TextImplicit},
};
@ -633,6 +639,12 @@ std::pair<String, Item> ItemFactory::parseCovr(
};
}
std::pair<String, Item> ItemFactory::parseStem(
const MP4::Atom *atom, const ByteVector &data)
{
return {atom->name(), Item(Stem(data))};
}
ByteVector ItemFactory::renderAtom(
const ByteVector &name, const ByteVector &data)
@ -742,6 +754,13 @@ ByteVector ItemFactory::renderCovr(
return renderAtom(name, data);
}
ByteVector ItemFactory::renderStem(
const ByteVector &name, const MP4::Item &item)
{
auto data = item.toStem().data();
return renderAtom(name, data);
}
ByteVector ItemFactory::renderFreeForm(
const String &name, const MP4::Item &item)
{

View File

@ -142,6 +142,7 @@ namespace TagLib {
Byte,
Gnre,
Covr,
Stem,
TextImplicit,
Text
};
@ -214,6 +215,8 @@ namespace TagLib {
const MP4::Atom *atom, const ByteVector &bytes);
static std::pair<String, Item> parseCovr(
const MP4::Atom *atom, const ByteVector &data);
static std::pair<String, Item> parseStem(
const MP4::Atom *atom, const ByteVector &data);
// Functions used by renderItem() to render atom data for items.
static ByteVector renderAtom(
@ -242,6 +245,8 @@ namespace TagLib {
const ByteVector &name, const MP4::Item &item);
static ByteVector renderCovr(
const ByteVector &name, const MP4::Item &item);
static ByteVector renderStem(
const ByteVector &name, const MP4::Item &item);
private:
static ItemFactory factory;

64
taglib/mp4/mp4stem.cpp Normal file
View File

@ -0,0 +1,64 @@
/**************************************************************************
copyright : (C) 2026 by Antoine Colombier
email : antoine@mixxx.org
**************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "mp4stem.h"
using namespace TagLib;
MP4::Stem::Stem(const ByteVector &data) :
d(std::make_shared<StemPrivate>())
{
d->data = data;
}
MP4::Stem::Stem() = default;
MP4::Stem::Stem(const Stem &) = default;
MP4::Stem &MP4::Stem::operator=(const Stem &) = default;
void
MP4::Stem::swap(Stem &item) noexcept
{
using std::swap;
swap(d, item.d);
}
MP4::Stem::~Stem() = default;
ByteVector
MP4::Stem::data() const
{
return d->data;
}
bool MP4::Stem::operator==(const Stem &other) const
{
return data() == other.data();
}
bool MP4::Stem::operator!=(const Stem &other) const
{
return !(*this == other);
}

78
taglib/mp4/mp4stem.h Normal file
View File

@ -0,0 +1,78 @@
/**************************************************************************
copyright : (C) 2026 by Antoine Colombier
email : antoine@mixxx.org
**************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MP4STEM_H
#define TAGLIB_MP4STEM_H
#include "tbytevector.h"
#include "taglib_export.h"
namespace TagLib::MP4 {
//! STEM
class StemPrivate
{
public:
ByteVector data;
};
class TAGLIB_EXPORT Stem
{
public:
Stem();
explicit Stem(const ByteVector &data);
~Stem();
Stem(const Stem &item);
/*!
* Copies the contents of \a item into this Stem.
*/
Stem &operator=(const Stem &item);
/*!
* Exchanges the content of the Stem with the content of \a item.
*/
void swap(Stem &item) noexcept;
//! The Stem data
ByteVector data() const;
/*!
* Returns \c true if the Stem and \a other contain the same data.
*/
bool operator==(const Stem &other) const;
/*!
* Returns \c true if the Stem and \a other have different data.
*/
bool operator!=(const Stem &other) const;
private:
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::shared_ptr<StemPrivate> d;
};
} // namespace TagLib::MP4
#endif

View File

@ -32,6 +32,7 @@
#include "mp4itemfactory.h"
#include "mp4atom.h"
#include "mp4coverart.h"
#include "mp4stem.h"
using namespace TagLib;
@ -65,15 +66,22 @@ MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms,
d->atoms = atoms;
const MP4::Atom *ilst = atoms->find("moov", "udta", "meta", "ilst");
if(!ilst) {
//debug("Atom moov.udta.meta.ilst not found.");
return;
if(ilst) {
for(const auto &atom : ilst->children()) {
file->seek(atom->offset() + 8);
ByteVector data = d->file->readBlock(atom->length() - 8);
if(const auto &[name, itm] = d->factory->parseItem(atom, data);
itm.isValid()) {
addItem(name, itm);
}
}
}
for(const auto &atom : ilst->children()) {
file->seek(atom->offset() + 8);
ByteVector data = d->file->readBlock(atom->length() - 8);
if(const auto &[name, itm] = d->factory->parseItem(atom, data);
const MP4::Atom *stem = atoms->find("moov", "udta", "stem");
if(stem) {
file->seek(stem->offset() + 8);
ByteVector data = d->file->readBlock(stem->length() - 8);
if(const auto &[name, itm] = d->factory->parseItem(stem, data);
itm.isValid()) {
addItem(name, itm);
}
@ -100,18 +108,33 @@ MP4::Tag::renderAtom(const ByteVector &name, const ByteVector &data) const
bool
MP4::Tag::save()
{
ByteVector data;
ByteVector ilstData, stemData;
for(const auto &[name, itm] : std::as_const(d->items)) {
data.append(d->factory->renderItem(name, itm));
if(name == "stem"){
stemData.append(d->factory->renderItem(name, itm));
} else {
ilstData.append(d->factory->renderItem(name, itm));
}
}
data = renderAtom("ilst", data);
ilstData = renderAtom("ilst", ilstData);
AtomList path = d->atoms->path("moov", "udta", "meta", "ilst");
if(path.size() == 4) {
saveExisting(data, path);
saveExisting(ilstData, path);
}
else {
saveNew(data);
ByteVector metaData = renderAtom("meta", ByteVector(4, '\0') +
renderAtom("hdlr", ByteVector(8, '\0') + ByteVector("mdirappl") +
ByteVector(9, '\0')) +
ilstData + padIlst(ilstData));
saveNew(metaData);
}
path = d->atoms->path("moov", "udta", "stem");
if(path.size() == 3) {
saveExisting(stemData, path);
} else if (!stemData.isEmpty()) {
saveNew(stemData);
}
return true;
@ -127,6 +150,11 @@ MP4::Tag::strip()
saveExisting(ByteVector(), path);
}
path = d->atoms->path("moov", "udta", "stem");
if(path.size() == 3) {
saveExisting(ByteVector(), path);
}
return true;
}
@ -227,11 +255,6 @@ MP4::Tag::updateOffsets(offset_t delta, offset_t offset)
void
MP4::Tag::saveNew(ByteVector data)
{
data = renderAtom("meta", ByteVector(4, '\0') +
renderAtom("hdlr", ByteVector(8, '\0') + ByteVector("mdirappl") +
ByteVector(9, '\0')) +
data + padIlst(data));
AtomList path = d->atoms->path("moov", "udta");
if(path.size() != 2) {
path = d->atoms->path("moov");
@ -510,13 +533,17 @@ StringList MP4::Tag::complexPropertyKeys() const
if(d->items.contains("covr")) {
keys.append("PICTURE");
}
if(d->items.contains("stem")) {
keys.append("STEM");
}
return keys;
}
List<VariantMap> MP4::Tag::complexProperties(const String &key) const
{
List<VariantMap> props;
if(const String uppercaseKey = key.upper(); uppercaseKey == "PICTURE") {
const String uppercaseKey = key.upper();
if(uppercaseKey == "PICTURE") {
const CoverArtList pictures = d->items.value("covr").toCoverArtList();
for(const CoverArt &picture : pictures) {
String mimeType = "image/";
@ -543,12 +570,20 @@ List<VariantMap> MP4::Tag::complexProperties(const String &key) const
props.append(property);
}
}
else if(uppercaseKey == "STEM" && d->items.contains("stem")) {
const Stem stem = d->items.value("stem").toStem();
VariantMap property;
property.insert("manifest", stem.data());
props.append(property);
}
return props;
}
bool MP4::Tag::setComplexProperties(const String &key, const List<VariantMap> &value)
{
if(const String uppercaseKey = key.upper(); uppercaseKey == "PICTURE") {
const String uppercaseKey = key.upper();
if(uppercaseKey == "PICTURE") {
CoverArtList pictures;
for(const auto &property : value) {
auto mimeType = property.value("mimeType").value<String>();
@ -568,6 +603,13 @@ bool MP4::Tag::setComplexProperties(const String &key, const List<VariantMap> &v
}
d->items["covr"] = pictures;
}
else if(uppercaseKey == "STEM") {
if (!value.isEmpty()) {
d->items["stem"] = Stem(value.front().value("manifest").value<ByteVector>());
} else {
d->items.erase("stem");
}
}
else {
return false;
}

View File

@ -131,6 +131,7 @@ IF(WITH_MP4)
test_mp4.cpp
test_mp4item.cpp
test_mp4coverart.cpp
test_mp4stem.cpp
)
ENDIF()
IF(WITH_ASF)

153
tests/test_mp4stem.cpp Normal file
View File

@ -0,0 +1,153 @@
/***************************************************************************
copyright : (C) 2026 by Antoine Colombier
email : antoine@mixxx.org
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include <string>
#include "tag.h"
#include "mp4file.h"
#include <cppunit/extensions/HelperMacros.h>
#include "utils.h"
using namespace std;
using namespace TagLib;
namespace {
const String STEM_KEY("STEM");
}
class TestMP4Stem : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(TestMP4Stem);
CPPUNIT_TEST(testCreate);
CPPUNIT_TEST(testUpdate);
CPPUNIT_TEST(testRemove);
CPPUNIT_TEST_SUITE_END();
protected:
static void createTestFile(ScopedFileCopy &copy){
MP4::File f(copy.fileName().c_str());
CPPUNIT_ASSERT(!f.hasMP4Tag());
auto &tag = *f.tag();
CPPUNIT_ASSERT_EQUAL(0U, tag.complexProperties(STEM_KEY).size());
CPPUNIT_ASSERT(tag.setComplexProperties(STEM_KEY, List<VariantMap>({{{"manifest", ByteVector("{some text data}")}}})));
f.save();
}
public:
void testCreate()
{
ScopedFileCopy copy("no-tags", ".m4a");
createTestFile(copy);
// Assert whether the newly created stem content is as expected
{
MP4::File f(copy.fileName().c_str());
auto &tag = *f.tag();
CPPUNIT_ASSERT(f.hasMP4Tag());
auto stems = tag.complexProperties(STEM_KEY);
CPPUNIT_ASSERT_EQUAL(1U, stems.size());
CPPUNIT_ASSERT(stems.front().contains("manifest"));
CPPUNIT_ASSERT_EQUAL(Variant(ByteVector("{some text data}")), stems.front().find("manifest")->second);
}
}
void testUpdate()
{
ScopedFileCopy copy("no-tags", ".m4a");
createTestFile(copy);
// Prepare so large test data, to ensure that free padding is correctly used
char *buffer = new char[1025]{'X'};
buffer[1024] = '\0';
String artist(buffer);
std::memset(buffer, 'Y', 1024);
String title(buffer);
std::memset(buffer, 'Z', 1024);
ByteVector newStem(buffer);
delete [] buffer;
// Update tags and stems to force atom offset recalculation
{
MP4::File f(copy.fileName().c_str());
CPPUNIT_ASSERT(f.setComplexProperties("PICTURE", List<VariantMap>({
{
{"data", ByteVector("DummyData")},
{"pictureType", "Front Cover"},
{"mimeType", String("image/png")},
{"description", String("Test")}
}
})));
CPPUNIT_ASSERT(f.tag()->setComplexProperties(STEM_KEY, List<VariantMap>({{{"manifest", newStem}}})));
f.tag()->setArtist(artist);
f.tag()->setTitle(title);
f.save();
}
// Reload the file to assert its tags
{
MP4::File f(copy.fileName().c_str());
CPPUNIT_ASSERT(f.hasMP4Tag());
auto stems = f.tag()->complexProperties(STEM_KEY);
CPPUNIT_ASSERT_EQUAL(1U, stems.size());
CPPUNIT_ASSERT(stems.front().contains("manifest"));
CPPUNIT_ASSERT_EQUAL(Variant(newStem), stems.front().find("manifest")->second);
auto pictures = f.tag()->complexProperties("PICTURE");
CPPUNIT_ASSERT_EQUAL(1U, pictures.size());
CPPUNIT_ASSERT_EQUAL(VariantMap({
{
{"data", ByteVector("DummyData")},
{"mimeType", String("image/png")},
}
}), pictures.front());
CPPUNIT_ASSERT_EQUAL(title, f.tag()->title());
CPPUNIT_ASSERT_EQUAL(artist, f.tag()->artist());
}
}
void testRemove()
{
ScopedFileCopy copy("no-tags", ".m4a");
createTestFile(copy);
// Remove the stem
{
MP4::File f(copy.fileName().c_str());
CPPUNIT_ASSERT(f.hasMP4Tag());
CPPUNIT_ASSERT(f.tag()->setComplexProperties(STEM_KEY, List<VariantMap>()));
f.save();
}
{
MP4::File f(copy.fileName().c_str());
CPPUNIT_ASSERT(!f.hasMP4Tag());
CPPUNIT_ASSERT(!f.tag()->complexPropertyKeys().contains(STEM_KEY));
}
}
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestMP4Stem);