1 /**************************************************************************
2 copyright : (C) 2007 by Lukáš Lalinský
3 email : lalinsky@gmail.com
4 **************************************************************************/
6 /***************************************************************************
7 * This library is free software; you can redistribute it and/or modify *
8 * it under the terms of the GNU Lesser General Public License version *
9 * 2.1 as published by the Free Software Foundation. *
11 * This library is distributed in the hope that it will be useful, but *
12 * WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
14 * Lesser General Public License for more details. *
16 * You should have received a copy of the GNU Lesser General Public *
17 * License along with this library; if not, write to the Free Software *
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
21 * Alternatively, this file is available under the Mozilla Public *
22 * License Version 1.1. You may obtain a copy of the License at *
23 * http://www.mozilla.org/MPL/ *
24 ***************************************************************************/
36 #include "id3v1genres.h"
38 using namespace TagLib;
40 class MP4::Tag::TagPrivate
43 TagPrivate() : file(0), atoms(0) {}
50 MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms)
56 MP4::Atom *ilst = atoms->find("moov", "udta", "meta", "ilst");
58 //debug("Atom moov.udta.meta.ilst not found.");
62 for(unsigned int i = 0; i < ilst->children.size(); i++) {
63 MP4::Atom *atom = ilst->children[i];
64 file->seek(atom->offset + 8);
65 if(atom->name == "----") {
66 parseFreeForm(atom, file);
68 else if(atom->name == "trkn" || atom->name == "disk") {
69 parseIntPair(atom, file);
71 else if(atom->name == "cpil" || atom->name == "pgap" || atom->name == "pcst") {
72 parseBool(atom, file);
74 else if(atom->name == "tmpo") {
77 else if(atom->name == "gnre") {
78 parseGnre(atom, file);
80 else if(atom->name == "covr") {
81 parseCovr(atom, file);
84 parseText(atom, file);
95 MP4::Tag::parseData(MP4::Atom *atom, TagLib::File *file, int expectedFlags, bool freeForm)
97 ByteVectorList result;
98 ByteVector data = file->readBlock(atom->length - 8);
100 unsigned int pos = 0;
101 while(pos < data.size()) {
102 int length = data.mid(pos, 4).toUInt();
103 ByteVector name = data.mid(pos + 4, 4);
104 int flags = data.mid(pos + 8, 4).toUInt();
105 if(freeForm && i < 2) {
106 if(i == 0 && name != "mean") {
107 debug("MP4: Unexpected atom \"" + name + "\", expecting \"mean\"");
110 else if(i == 1 && name != "name") {
111 debug("MP4: Unexpected atom \"" + name + "\", expecting \"name\"");
114 result.append(data.mid(pos + 12, length - 12));
118 debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\"");
121 if(expectedFlags == -1 || flags == expectedFlags) {
122 result.append(data.mid(pos + 16, length - 16));
132 MP4::Tag::parseInt(MP4::Atom *atom, TagLib::File *file)
134 ByteVectorList data = parseData(atom, file);
136 d->items.insert(atom->name, (int)data[0].toShort());
141 MP4::Tag::parseGnre(MP4::Atom *atom, TagLib::File *file)
143 ByteVectorList data = parseData(atom, file);
145 int idx = (int)data[0].toShort();
146 if(!d->items.contains("\251gen") && idx > 0) {
147 d->items.insert("\251gen", StringList(ID3v1::genre(idx - 1)));
153 MP4::Tag::parseIntPair(MP4::Atom *atom, TagLib::File *file)
155 ByteVectorList data = parseData(atom, file);
157 int a = data[0].mid(2, 2).toShort();
158 int b = data[0].mid(4, 2).toShort();
159 d->items.insert(atom->name, MP4::Item(a, b));
164 MP4::Tag::parseBool(MP4::Atom *atom, TagLib::File *file)
166 ByteVectorList data = parseData(atom, file);
168 bool value = data[0].size() ? data[0][0] != '\0' : false;
169 d->items.insert(atom->name, value);
174 MP4::Tag::parseText(MP4::Atom *atom, TagLib::File *file, int expectedFlags)
176 ByteVectorList data = parseData(atom, file, expectedFlags);
179 for(unsigned int i = 0; i < data.size(); i++) {
180 value.append(String(data[i], String::UTF8));
182 d->items.insert(atom->name, value);
187 MP4::Tag::parseFreeForm(MP4::Atom *atom, TagLib::File *file)
189 ByteVectorList data = parseData(atom, file, 1, true);
190 if(data.size() > 2) {
192 for(unsigned int i = 2; i < data.size(); i++) {
193 value.append(String(data[i], String::UTF8));
195 String name = "----:" + data[0] + ':' + data[1];
196 d->items.insert(name, value);
201 MP4::Tag::parseCovr(MP4::Atom *atom, TagLib::File *file)
203 MP4::CoverArtList value;
204 ByteVector data = file->readBlock(atom->length - 8);
205 unsigned int pos = 0;
206 while(pos < data.size()) {
207 int length = data.mid(pos, 4).toUInt();
208 ByteVector name = data.mid(pos + 4, 4);
209 int flags = data.mid(pos + 8, 4).toUInt();
211 debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\"");
214 if(flags == MP4::CoverArt::PNG || flags == MP4::CoverArt::JPEG) {
215 value.append(MP4::CoverArt(MP4::CoverArt::Format(flags),
216 data.mid(pos + 16, length - 16)));
221 d->items.insert(atom->name, value);
225 MP4::Tag::padIlst(const ByteVector &data, int length)
228 length = ((data.size() + 1023) & ~1023) - data.size();
230 return renderAtom("free", ByteVector(length, '\1'));
234 MP4::Tag::renderAtom(const ByteVector &name, const ByteVector &data)
236 return ByteVector::fromUInt(data.size() + 8) + name + data;
240 MP4::Tag::renderData(const ByteVector &name, int flags, const ByteVectorList &data)
243 for(unsigned int i = 0; i < data.size(); i++) {
244 result.append(renderAtom("data", ByteVector::fromUInt(flags) + ByteVector(4, '\0') + data[i]));
246 return renderAtom(name, result);
250 MP4::Tag::renderBool(const ByteVector &name, MP4::Item &item)
253 data.append(ByteVector(1, item.toBool() ? '\1' : '\0'));
254 return renderData(name, 0x15, data);
258 MP4::Tag::renderInt(const ByteVector &name, MP4::Item &item)
261 data.append(ByteVector::fromShort(item.toInt()));
262 return renderData(name, 0x15, data);
266 MP4::Tag::renderIntPair(const ByteVector &name, MP4::Item &item)
269 data.append(ByteVector(2, '\0') +
270 ByteVector::fromShort(item.toIntPair().first) +
271 ByteVector::fromShort(item.toIntPair().second) +
272 ByteVector(2, '\0'));
273 return renderData(name, 0x00, data);
277 MP4::Tag::renderIntPairNoTrailing(const ByteVector &name, MP4::Item &item)
280 data.append(ByteVector(2, '\0') +
281 ByteVector::fromShort(item.toIntPair().first) +
282 ByteVector::fromShort(item.toIntPair().second));
283 return renderData(name, 0x00, data);
287 MP4::Tag::renderText(const ByteVector &name, MP4::Item &item, int flags)
290 StringList value = item.toStringList();
291 for(unsigned int i = 0; i < value.size(); i++) {
292 data.append(value[i].data(String::UTF8));
294 return renderData(name, flags, data);
298 MP4::Tag::renderCovr(const ByteVector &name, MP4::Item &item)
301 MP4::CoverArtList value = item.toCoverArtList();
302 for(unsigned int i = 0; i < value.size(); i++) {
303 data.append(renderAtom("data", ByteVector::fromUInt(value[i].format()) +
304 ByteVector(4, '\0') + value[i].data()));
306 return renderAtom(name, data);
310 MP4::Tag::renderFreeForm(const String &name, MP4::Item &item)
312 StringList header = StringList::split(name, ":");
313 if (header.size() != 3) {
314 debug("MP4: Invalid free-form item name \"" + name + "\"");
315 return ByteVector::null;
318 data.append(renderAtom("mean", ByteVector::fromUInt(0) + header[1].data(String::UTF8)));
319 data.append(renderAtom("name", ByteVector::fromUInt(0) + header[2].data(String::UTF8)));
320 StringList value = item.toStringList();
321 for(unsigned int i = 0; i < value.size(); i++) {
322 data.append(renderAtom("data", ByteVector::fromUInt(1) + ByteVector(4, '\0') + value[i].data(String::UTF8)));
324 return renderAtom("----", data);
331 for(MP4::ItemListMap::Iterator i = d->items.begin(); i != d->items.end(); i++) {
332 const String name = i->first;
333 if(name.startsWith("----")) {
334 data.append(renderFreeForm(name, i->second));
336 else if(name == "trkn") {
337 data.append(renderIntPair(name.data(String::Latin1), i->second));
339 else if(name == "disk") {
340 data.append(renderIntPairNoTrailing(name.data(String::Latin1), i->second));
342 else if(name == "cpil" || name == "pgap" || name == "pcst") {
343 data.append(renderBool(name.data(String::Latin1), i->second));
345 else if(name == "tmpo") {
346 data.append(renderInt(name.data(String::Latin1), i->second));
348 else if(name == "covr") {
349 data.append(renderCovr(name.data(String::Latin1), i->second));
351 else if(name.size() == 4){
352 data.append(renderText(name.data(String::Latin1), i->second));
355 debug("MP4: Unknown item name \"" + name + "\"");
358 data = renderAtom("ilst", data);
360 AtomList path = d->atoms->path("moov", "udta", "meta", "ilst");
361 if(path.size() == 4) {
362 saveExisting(data, path);
372 MP4::Tag::updateParents(AtomList &path, long delta, int ignore)
374 for(unsigned int i = 0; i < path.size() - ignore; i++) {
375 d->file->seek(path[i]->offset);
376 long size = d->file->readBlock(4).toUInt();
379 d->file->seek(4, File::Current); // Skip name
380 long long longSize = d->file->readBlock(8).toLongLong();
381 // Seek the offset of the 64-bit size
382 d->file->seek(path[i]->offset + 8);
383 d->file->writeBlock(ByteVector::fromLongLong(longSize + delta));
387 d->file->seek(path[i]->offset);
388 d->file->writeBlock(ByteVector::fromUInt(size + delta));
394 MP4::Tag::updateOffsets(long delta, long offset)
396 MP4::Atom *moov = d->atoms->find("moov");
398 MP4::AtomList stco = moov->findall("stco", true);
399 for(unsigned int i = 0; i < stco.size(); i++) {
400 MP4::Atom *atom = stco[i];
401 if(atom->offset > offset) {
402 atom->offset += delta;
404 d->file->seek(atom->offset + 12);
405 ByteVector data = d->file->readBlock(atom->length - 12);
406 unsigned int count = data.mid(0, 4).toUInt();
407 d->file->seek(atom->offset + 16);
410 long o = data.mid(pos, 4).toUInt();
414 d->file->writeBlock(ByteVector::fromUInt(o));
419 MP4::AtomList co64 = moov->findall("co64", true);
420 for(unsigned int i = 0; i < co64.size(); i++) {
421 MP4::Atom *atom = co64[i];
422 if(atom->offset > offset) {
423 atom->offset += delta;
425 d->file->seek(atom->offset + 12);
426 ByteVector data = d->file->readBlock(atom->length - 12);
427 unsigned int count = data.mid(0, 4).toUInt();
428 d->file->seek(atom->offset + 16);
431 long long o = data.mid(pos, 8).toLongLong();
435 d->file->writeBlock(ByteVector::fromLongLong(o));
441 MP4::Atom *moof = d->atoms->find("moof");
443 MP4::AtomList tfhd = moof->findall("tfhd", true);
444 for(unsigned int i = 0; i < tfhd.size(); i++) {
445 MP4::Atom *atom = tfhd[i];
446 if(atom->offset > offset) {
447 atom->offset += delta;
449 d->file->seek(atom->offset + 9);
450 ByteVector data = d->file->readBlock(atom->offset - 9);
451 unsigned int flags = (ByteVector(1, '\0') + data.mid(0, 3)).toUInt();
453 long long o = data.mid(7, 8).toLongLong();
457 d->file->seek(atom->offset + 16);
458 d->file->writeBlock(ByteVector::fromLongLong(o));
465 MP4::Tag::saveNew(ByteVector &data)
467 data = renderAtom("meta", TagLib::ByteVector(4, '\0') +
468 renderAtom("hdlr", TagLib::ByteVector(8, '\0') + TagLib::ByteVector("mdirappl") + TagLib::ByteVector(9, '\0')) +
469 data + padIlst(data));
471 AtomList path = d->atoms->path("moov", "udta");
472 if(path.size() != 2) {
473 path = d->atoms->path("moov");
474 data = renderAtom("udta", data);
477 long offset = path[path.size() - 1]->offset + 8;
478 d->file->insert(data, offset, 0);
480 updateParents(path, data.size());
481 updateOffsets(data.size(), offset);
485 MP4::Tag::saveExisting(ByteVector &data, AtomList &path)
487 MP4::Atom *ilst = path[path.size() - 1];
488 long offset = ilst->offset;
489 long length = ilst->length;
491 MP4::Atom *meta = path[path.size() - 2];
492 AtomList::Iterator index = meta->children.find(ilst);
494 // check if there is an atom before 'ilst', and possibly use it as padding
495 if(index != meta->children.begin()) {
496 AtomList::Iterator prevIndex = index;
498 MP4::Atom *prev = *prevIndex;
499 if(prev->name == "free") {
500 offset = prev->offset;
501 length += prev->length;
504 // check if there is an atom after 'ilst', and possibly use it as padding
505 AtomList::Iterator nextIndex = index;
507 if(nextIndex != meta->children.end()) {
508 MP4::Atom *next = *nextIndex;
509 if(next->name == "free") {
510 length += next->length;
514 long delta = data.size() - length;
515 if(delta > 0 || (delta < 0 && delta > -8)) {
516 data.append(padIlst(data));
517 delta = data.size() - length;
520 data.append(padIlst(data, -delta - 8));
524 d->file->insert(data, offset, length);
527 updateParents(path, delta, 1);
528 updateOffsets(delta, offset);
533 MP4::Tag::title() const
535 if(d->items.contains("\251nam"))
536 return d->items["\251nam"].toStringList().toString(", ");
541 MP4::Tag::artist() const
543 if(d->items.contains("\251ART"))
544 return d->items["\251ART"].toStringList().toString(", ");
549 MP4::Tag::album() const
551 if(d->items.contains("\251alb"))
552 return d->items["\251alb"].toStringList().toString(", ");
557 MP4::Tag::comment() const
559 if(d->items.contains("\251cmt"))
560 return d->items["\251cmt"].toStringList().toString(", ");
565 MP4::Tag::genre() const
567 if(d->items.contains("\251gen"))
568 return d->items["\251gen"].toStringList().toString(", ");
573 MP4::Tag::year() const
575 if(d->items.contains("\251day"))
576 return d->items["\251day"].toStringList().toString().toInt();
581 MP4::Tag::track() const
583 if(d->items.contains("trkn"))
584 return d->items["trkn"].toIntPair().first;
589 MP4::Tag::setTitle(const String &value)
591 d->items["\251nam"] = StringList(value);
595 MP4::Tag::setArtist(const String &value)
597 d->items["\251ART"] = StringList(value);
601 MP4::Tag::setAlbum(const String &value)
603 d->items["\251alb"] = StringList(value);
607 MP4::Tag::setComment(const String &value)
609 d->items["\251cmt"] = StringList(value);
613 MP4::Tag::setGenre(const String &value)
615 d->items["\251gen"] = StringList(value);
619 MP4::Tag::setYear(uint value)
621 d->items["\251day"] = StringList(String::number(value));
625 MP4::Tag::setTrack(uint value)
627 d->items["trkn"] = MP4::Item(value, 0);
631 MP4::Tag::itemListMap()