Add BEXT and iXML chunk support to WAV files (#1323)

Read, write, and remove Broadcast Audio Extension (BEXT, EBU Tech 3285)
and iXML metadata chunks in WAV files. BEXT is widely used in broadcast
and professional audio for originator, description, time reference, and
loudness metadata. iXML is used by field recorders and DAWs for scene,
take, and track metadata.
This commit is contained in:
Ryan Francesconi
2026-04-04 03:14:34 -07:00
committed by GitHub
parent 49510e7d5a
commit abadbb6768
3 changed files with 262 additions and 0 deletions

View File

@ -61,6 +61,10 @@ class TestWAV : public CppUnit::TestFixture
CPPUNIT_TEST(testWaveFormatExtensible);
CPPUNIT_TEST(testInvalidChunk);
CPPUNIT_TEST(testRIFFInfoProperties);
CPPUNIT_TEST(testBEXTTag);
CPPUNIT_TEST(testBEXTTagWithOtherTags);
CPPUNIT_TEST(testiXMLTag);
CPPUNIT_TEST(testiXMLTagWithOtherTags);
CPPUNIT_TEST_SUITE_END();
public:
@ -482,6 +486,151 @@ public:
}
}
void testBEXTTag()
{
ScopedFileCopy copy("empty", ".wav");
string filename = copy.fileName();
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(!f.hasBEXTData());
CPPUNIT_ASSERT(f.BEXTData().isEmpty());
f.setBEXTData(ByteVector("test bext data"));
f.save();
CPPUNIT_ASSERT(f.hasBEXTData());
}
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasBEXTData());
CPPUNIT_ASSERT_EQUAL(ByteVector("test bext data"), f.BEXTData());
f.setBEXTData(ByteVector());
f.save();
CPPUNIT_ASSERT(!f.hasBEXTData());
}
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(!f.hasBEXTData());
CPPUNIT_ASSERT(f.BEXTData().isEmpty());
}
// Check if file without BEXT is same as original empty file
const ByteVector origData = PlainFile(TEST_FILE_PATH_C("empty.wav")).readAll();
const ByteVector fileData = PlainFile(filename.c_str()).readAll();
CPPUNIT_ASSERT(origData == fileData);
}
void testBEXTTagWithOtherTags()
{
ScopedFileCopy copy("empty", ".wav");
string filename = copy.fileName();
{
RIFF::WAV::File f(filename.c_str());
f.ID3v2Tag()->setTitle("ID3v2 Title");
f.InfoTag()->setTitle("INFO Title");
f.setBEXTData(ByteVector("bext payload"));
f.save();
}
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.hasID3v2Tag());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT(f.hasBEXTData());
CPPUNIT_ASSERT_EQUAL(String("ID3v2 Title"), f.ID3v2Tag()->title());
CPPUNIT_ASSERT_EQUAL(String("INFO Title"), f.InfoTag()->title());
CPPUNIT_ASSERT_EQUAL(ByteVector("bext payload"), f.BEXTData());
}
}
void testiXMLTag()
{
ScopedFileCopy copy("empty", ".wav");
string filename = copy.fileName();
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(!f.hasiXMLData());
CPPUNIT_ASSERT(f.iXMLData().isEmpty());
f.setiXMLData("<BWFXML><IXML_VERSION>1.0</IXML_VERSION></BWFXML>");
f.save();
CPPUNIT_ASSERT(f.hasiXMLData());
}
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasiXMLData());
CPPUNIT_ASSERT_EQUAL(
String("<BWFXML><IXML_VERSION>1.0</IXML_VERSION></BWFXML>"),
f.iXMLData());
f.setiXMLData(String());
f.save();
CPPUNIT_ASSERT(!f.hasiXMLData());
}
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(!f.hasiXMLData());
CPPUNIT_ASSERT(f.iXMLData().isEmpty());
}
// Check if file without iXML is same as original empty file
const ByteVector origData = PlainFile(TEST_FILE_PATH_C("empty.wav")).readAll();
const ByteVector fileData = PlainFile(filename.c_str()).readAll();
CPPUNIT_ASSERT(origData == fileData);
}
void testiXMLTagWithOtherTags()
{
ScopedFileCopy copy("empty", ".wav");
string filename = copy.fileName();
{
RIFF::WAV::File f(filename.c_str());
f.ID3v2Tag()->setTitle("ID3v2 Title");
f.setiXMLData("<BWFXML><SCENE>1</SCENE></BWFXML>");
f.setBEXTData(ByteVector("bext data"));
f.save();
}
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.hasID3v2Tag());
CPPUNIT_ASSERT(f.hasiXMLData());
CPPUNIT_ASSERT(f.hasBEXTData());
CPPUNIT_ASSERT_EQUAL(String("ID3v2 Title"), f.ID3v2Tag()->title());
CPPUNIT_ASSERT_EQUAL(
String("<BWFXML><SCENE>1</SCENE></BWFXML>"),
f.iXMLData());
CPPUNIT_ASSERT_EQUAL(ByteVector("bext data"), f.BEXTData());
f.setiXMLData(String());
f.setBEXTData(ByteVector());
f.strip();
CPPUNIT_ASSERT(f.save());
}
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(!f.hasID3v2Tag());
CPPUNIT_ASSERT(!f.hasiXMLData());
CPPUNIT_ASSERT(f.iXMLData().isEmpty());
CPPUNIT_ASSERT(!f.hasBEXTData());
CPPUNIT_ASSERT(f.BEXTData().isEmpty());
}
// Check if file without tags is same as original empty file
const ByteVector origData = PlainFile(TEST_FILE_PATH_C("empty.wav")).readAll();
const ByteVector fileData = PlainFile(filename.c_str()).readAll();
CPPUNIT_ASSERT(origData == fileData);
}
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestWAV);