Skip to content

Commit abadbb6

Browse files
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.
1 parent 49510e7 commit abadbb6

3 files changed

Lines changed: 262 additions & 0 deletions

File tree

taglib/riff/wav/wavfile.cpp

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ class RIFF::WAV::File::FilePrivate
5555

5656
bool hasID3v2 { false };
5757
bool hasInfo { false };
58+
bool hasiXML { false };
59+
bool hasBEXT { false };
60+
61+
String iXMLData;
62+
ByteVector bextData;
5863
};
5964

6065
////////////////////////////////////////////////////////////////////////////////
@@ -108,6 +113,26 @@ RIFF::Info::Tag *RIFF::WAV::File::InfoTag() const
108113
return d->tag.access<RIFF::Info::Tag>(InfoIndex, false);
109114
}
110115

116+
String RIFF::WAV::File::iXMLData() const
117+
{
118+
return d->iXMLData;
119+
}
120+
121+
void RIFF::WAV::File::setiXMLData(const String &data)
122+
{
123+
d->iXMLData = data;
124+
}
125+
126+
ByteVector RIFF::WAV::File::BEXTData() const
127+
{
128+
return d->bextData;
129+
}
130+
131+
void RIFF::WAV::File::setBEXTData(const ByteVector &data)
132+
{
133+
d->bextData = data;
134+
}
135+
111136
void RIFF::WAV::File::strip(TagTypes tags)
112137
{
113138
removeTagChunks(tags);
@@ -160,6 +185,26 @@ bool RIFF::WAV::File::save(TagTypes tags, StripTags strip, ID3v2::Version versio
160185
if(strip == StripOthers)
161186
File::strip(static_cast<TagTypes>(AllTags & ~tags));
162187

188+
if(!d->bextData.isEmpty()) {
189+
removeChunk("bext");
190+
setChunkData("bext", d->bextData);
191+
d->hasBEXT = true;
192+
}
193+
else if(d->hasBEXT) {
194+
removeChunk("bext");
195+
d->hasBEXT = false;
196+
}
197+
198+
if(!d->iXMLData.isEmpty()) {
199+
removeChunk("iXML");
200+
setChunkData("iXML", d->iXMLData.data(String::UTF8));
201+
d->hasiXML = true;
202+
}
203+
else if(d->hasiXML) {
204+
removeChunk("iXML");
205+
d->hasiXML = false;
206+
}
207+
163208
if(tags & ID3v2) {
164209
removeTagChunks(ID3v2);
165210

@@ -191,6 +236,16 @@ bool RIFF::WAV::File::hasInfoTag() const
191236
return d->hasInfo;
192237
}
193238

239+
bool RIFF::WAV::File::hasiXMLData() const
240+
{
241+
return d->hasiXML;
242+
}
243+
244+
bool RIFF::WAV::File::hasBEXTData() const
245+
{
246+
return d->hasBEXT;
247+
}
248+
194249
////////////////////////////////////////////////////////////////////////////////
195250
// private members
196251
////////////////////////////////////////////////////////////////////////////////
@@ -219,6 +274,14 @@ void RIFF::WAV::File::read(bool readProperties)
219274
}
220275
}
221276
}
277+
else if(name == "iXML") {
278+
d->hasiXML = true;
279+
d->iXMLData = String(chunkData(i));
280+
}
281+
else if(name == "bext") {
282+
d->hasBEXT = true;
283+
d->bextData = chunkData(i);
284+
}
222285
}
223286

224287
if(!d->tag[ID3v2Index])

taglib/riff/wav/wavfile.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,42 @@ namespace TagLib {
134134
*/
135135
Info::Tag *InfoTag() const;
136136

137+
/*!
138+
* Returns the raw iXML chunk data as a String.
139+
* Empty if no iXML chunk is present.
140+
*
141+
* \see setiXMLData()
142+
* \see hasiXMLData()
143+
*/
144+
String iXMLData() const;
145+
146+
/*!
147+
* Sets the iXML chunk data. Pass an empty string to remove the
148+
* iXML chunk on save.
149+
*
150+
* \see iXMLData()
151+
* \see hasiXMLData()
152+
*/
153+
void setiXMLData(const String &data);
154+
155+
/*!
156+
* Returns the raw BEXT (Broadcast Audio Extension) chunk data
157+
* as a ByteVector. Empty if no BEXT chunk is present.
158+
*
159+
* \see setBEXTData()
160+
* \see hasBEXTData()
161+
*/
162+
ByteVector BEXTData() const;
163+
164+
/*!
165+
* Sets the BEXT chunk data. Pass an empty ByteVector to remove
166+
* the BEXT chunk on save.
167+
*
168+
* \see BEXTData()
169+
* \see hasBEXTData()
170+
*/
171+
void setBEXTData(const ByteVector &data);
172+
137173
/*!
138174
* This will strip the tags that match the OR-ed together TagTypes from the
139175
* file. By default it strips all tags. It returns \c true if the tags are
@@ -191,6 +227,20 @@ namespace TagLib {
191227
*/
192228
bool hasInfoTag() const;
193229

230+
/*!
231+
* Returns whether or not the file on disk actually has an iXML chunk.
232+
*
233+
* \see iXMLTag
234+
*/
235+
bool hasiXMLData() const;
236+
237+
/*!
238+
* Returns whether or not the file on disk actually has a BEXT chunk.
239+
*
240+
* \see bextTag
241+
*/
242+
bool hasBEXTData() const;
243+
194244
/*!
195245
* Returns whether or not the given \a stream can be opened as a WAV
196246
* file.

tests/test_wav.cpp

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ class TestWAV : public CppUnit::TestFixture
6161
CPPUNIT_TEST(testWaveFormatExtensible);
6262
CPPUNIT_TEST(testInvalidChunk);
6363
CPPUNIT_TEST(testRIFFInfoProperties);
64+
CPPUNIT_TEST(testBEXTTag);
65+
CPPUNIT_TEST(testBEXTTagWithOtherTags);
66+
CPPUNIT_TEST(testiXMLTag);
67+
CPPUNIT_TEST(testiXMLTagWithOtherTags);
6468
CPPUNIT_TEST_SUITE_END();
6569

6670
public:
@@ -482,6 +486,151 @@ class TestWAV : public CppUnit::TestFixture
482486
}
483487
}
484488

489+
void testBEXTTag()
490+
{
491+
ScopedFileCopy copy("empty", ".wav");
492+
string filename = copy.fileName();
493+
494+
{
495+
RIFF::WAV::File f(filename.c_str());
496+
CPPUNIT_ASSERT(f.isValid());
497+
CPPUNIT_ASSERT(!f.hasBEXTData());
498+
CPPUNIT_ASSERT(f.BEXTData().isEmpty());
499+
500+
f.setBEXTData(ByteVector("test bext data"));
501+
f.save();
502+
CPPUNIT_ASSERT(f.hasBEXTData());
503+
}
504+
{
505+
RIFF::WAV::File f(filename.c_str());
506+
CPPUNIT_ASSERT(f.isValid());
507+
CPPUNIT_ASSERT(f.hasBEXTData());
508+
CPPUNIT_ASSERT_EQUAL(ByteVector("test bext data"), f.BEXTData());
509+
510+
f.setBEXTData(ByteVector());
511+
f.save();
512+
CPPUNIT_ASSERT(!f.hasBEXTData());
513+
}
514+
{
515+
RIFF::WAV::File f(filename.c_str());
516+
CPPUNIT_ASSERT(f.isValid());
517+
CPPUNIT_ASSERT(!f.hasBEXTData());
518+
CPPUNIT_ASSERT(f.BEXTData().isEmpty());
519+
}
520+
521+
// Check if file without BEXT is same as original empty file
522+
const ByteVector origData = PlainFile(TEST_FILE_PATH_C("empty.wav")).readAll();
523+
const ByteVector fileData = PlainFile(filename.c_str()).readAll();
524+
CPPUNIT_ASSERT(origData == fileData);
525+
}
526+
527+
void testBEXTTagWithOtherTags()
528+
{
529+
ScopedFileCopy copy("empty", ".wav");
530+
string filename = copy.fileName();
531+
532+
{
533+
RIFF::WAV::File f(filename.c_str());
534+
f.ID3v2Tag()->setTitle("ID3v2 Title");
535+
f.InfoTag()->setTitle("INFO Title");
536+
f.setBEXTData(ByteVector("bext payload"));
537+
f.save();
538+
}
539+
{
540+
RIFF::WAV::File f(filename.c_str());
541+
CPPUNIT_ASSERT(f.hasID3v2Tag());
542+
CPPUNIT_ASSERT(f.hasInfoTag());
543+
CPPUNIT_ASSERT(f.hasBEXTData());
544+
CPPUNIT_ASSERT_EQUAL(String("ID3v2 Title"), f.ID3v2Tag()->title());
545+
CPPUNIT_ASSERT_EQUAL(String("INFO Title"), f.InfoTag()->title());
546+
CPPUNIT_ASSERT_EQUAL(ByteVector("bext payload"), f.BEXTData());
547+
}
548+
}
549+
550+
void testiXMLTag()
551+
{
552+
ScopedFileCopy copy("empty", ".wav");
553+
string filename = copy.fileName();
554+
555+
{
556+
RIFF::WAV::File f(filename.c_str());
557+
CPPUNIT_ASSERT(f.isValid());
558+
CPPUNIT_ASSERT(!f.hasiXMLData());
559+
CPPUNIT_ASSERT(f.iXMLData().isEmpty());
560+
561+
f.setiXMLData("<BWFXML><IXML_VERSION>1.0</IXML_VERSION></BWFXML>");
562+
f.save();
563+
CPPUNIT_ASSERT(f.hasiXMLData());
564+
}
565+
{
566+
RIFF::WAV::File f(filename.c_str());
567+
CPPUNIT_ASSERT(f.isValid());
568+
CPPUNIT_ASSERT(f.hasiXMLData());
569+
CPPUNIT_ASSERT_EQUAL(
570+
String("<BWFXML><IXML_VERSION>1.0</IXML_VERSION></BWFXML>"),
571+
f.iXMLData());
572+
573+
f.setiXMLData(String());
574+
f.save();
575+
CPPUNIT_ASSERT(!f.hasiXMLData());
576+
}
577+
{
578+
RIFF::WAV::File f(filename.c_str());
579+
CPPUNIT_ASSERT(f.isValid());
580+
CPPUNIT_ASSERT(!f.hasiXMLData());
581+
CPPUNIT_ASSERT(f.iXMLData().isEmpty());
582+
}
583+
584+
// Check if file without iXML is same as original empty file
585+
const ByteVector origData = PlainFile(TEST_FILE_PATH_C("empty.wav")).readAll();
586+
const ByteVector fileData = PlainFile(filename.c_str()).readAll();
587+
CPPUNIT_ASSERT(origData == fileData);
588+
}
589+
590+
void testiXMLTagWithOtherTags()
591+
{
592+
ScopedFileCopy copy("empty", ".wav");
593+
string filename = copy.fileName();
594+
595+
{
596+
RIFF::WAV::File f(filename.c_str());
597+
f.ID3v2Tag()->setTitle("ID3v2 Title");
598+
f.setiXMLData("<BWFXML><SCENE>1</SCENE></BWFXML>");
599+
f.setBEXTData(ByteVector("bext data"));
600+
f.save();
601+
}
602+
{
603+
RIFF::WAV::File f(filename.c_str());
604+
CPPUNIT_ASSERT(f.hasID3v2Tag());
605+
CPPUNIT_ASSERT(f.hasiXMLData());
606+
CPPUNIT_ASSERT(f.hasBEXTData());
607+
CPPUNIT_ASSERT_EQUAL(String("ID3v2 Title"), f.ID3v2Tag()->title());
608+
CPPUNIT_ASSERT_EQUAL(
609+
String("<BWFXML><SCENE>1</SCENE></BWFXML>"),
610+
f.iXMLData());
611+
CPPUNIT_ASSERT_EQUAL(ByteVector("bext data"), f.BEXTData());
612+
613+
f.setiXMLData(String());
614+
f.setBEXTData(ByteVector());
615+
f.strip();
616+
CPPUNIT_ASSERT(f.save());
617+
}
618+
{
619+
RIFF::WAV::File f(filename.c_str());
620+
CPPUNIT_ASSERT(f.isValid());
621+
CPPUNIT_ASSERT(!f.hasID3v2Tag());
622+
CPPUNIT_ASSERT(!f.hasiXMLData());
623+
CPPUNIT_ASSERT(f.iXMLData().isEmpty());
624+
CPPUNIT_ASSERT(!f.hasBEXTData());
625+
CPPUNIT_ASSERT(f.BEXTData().isEmpty());
626+
}
627+
628+
// Check if file without tags is same as original empty file
629+
const ByteVector origData = PlainFile(TEST_FILE_PATH_C("empty.wav")).readAll();
630+
const ByteVector fileData = PlainFile(filename.c_str()).readAll();
631+
CPPUNIT_ASSERT(origData == fileData);
632+
}
633+
485634
};
486635

487636
CPPUNIT_TEST_SUITE_REGISTRATION(TestWAV);

0 commit comments

Comments
 (0)