Add M4A to files pattern
[someplayer] / src / taglib / mp4 / mp4tag.cpp
1 /**************************************************************************
2     copyright            : (C) 2007 by Lukáš Lalinský
3     email                : lalinsky@gmail.com
4  **************************************************************************/
5
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.                     *
10  *                                                                         *
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.                       *
15  *                                                                         *
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  *
19  *   USA                                                                   *
20  *                                                                         *
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  ***************************************************************************/
25
26 #ifdef HAVE_CONFIG_H
27 #include <config.h>
28 #endif
29
30 #include <tdebug.h>
31 #include <tstring.h>
32 #include "mp4atom.h"
33 #include "mp4tag.h"
34 #include "id3v1genres.h"
35
36 using namespace TagLib;
37
38 class MP4::Tag::TagPrivate
39 {
40 public:
41   TagPrivate() : file(0), atoms(0) {}
42   ~TagPrivate() {}
43   TagLib::File *file;
44   Atoms *atoms;
45   ItemListMap items;
46 };
47
48 MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms)
49 {
50   d = new TagPrivate;
51   d->file = file;
52   d->atoms = atoms;
53
54   MP4::Atom *ilst = atoms->find("moov", "udta", "meta", "ilst");
55   if(!ilst) {
56     //debug("Atom moov.udta.meta.ilst not found.");
57     return;
58   }
59
60   for(unsigned int i = 0; i < ilst->children.size(); i++) {
61     MP4::Atom *atom = ilst->children[i];
62     file->seek(atom->offset + 8);
63     if(atom->name == "----") {
64       parseFreeForm(atom, file);
65     }
66     else if(atom->name == "trkn" || atom->name == "disk") {
67       parseIntPair(atom, file);
68     }
69     else if(atom->name == "cpil" || atom->name == "pgap" || atom->name == "pcst") {
70       parseBool(atom, file);
71     }
72     else if(atom->name == "tmpo") {
73       parseInt(atom, file);
74     }
75     else if(atom->name == "gnre") {
76       parseGnre(atom, file);
77     }
78     else if(atom->name == "covr") {
79       parseCovr(atom, file);
80     }
81     else {
82       parseText(atom, file);
83     }
84   }
85 }
86
87 MP4::Tag::~Tag()
88 {
89   delete d;
90 }
91
92 ByteVectorList
93 MP4::Tag::parseData(MP4::Atom *atom, TagLib::File *file, int expectedFlags, bool freeForm)
94 {
95   ByteVectorList result;
96   ByteVector data = file->readBlock(atom->length - 8);
97   int i = 0;
98   unsigned int pos = 0;
99   while(pos < data.size()) {
100     int length = data.mid(pos, 4).toUInt();
101     ByteVector name = data.mid(pos + 4, 4);
102     int flags = data.mid(pos + 8, 4).toUInt();
103     if(freeForm && i < 2) {
104       if(i == 0 && name != "mean") {
105         debug("MP4: Unexpected atom \"" + name + "\", expecting \"mean\"");
106         return result;
107       }
108       else if(i == 1 && name != "name") {
109         debug("MP4: Unexpected atom \"" + name + "\", expecting \"name\"");
110         return result;
111       }
112       result.append(data.mid(pos + 12, length - 12));
113     }
114     else {
115       if(name != "data") {
116         debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\"");
117         return result;
118       }
119       if(expectedFlags == -1 || flags == expectedFlags) {
120         result.append(data.mid(pos + 16, length - 16));
121       }
122     }
123     pos += length;
124     i++;
125   }
126   return result;
127 }
128
129 void
130 MP4::Tag::parseInt(MP4::Atom *atom, TagLib::File *file)
131 {
132   ByteVectorList data = parseData(atom, file);
133   if(data.size()) {
134     d->items.insert(atom->name, (int)data[0].toShort());
135   }
136 }
137
138 void
139 MP4::Tag::parseGnre(MP4::Atom *atom, TagLib::File *file)
140 {
141   ByteVectorList data = parseData(atom, file);
142   if(data.size()) {
143     int idx = (int)data[0].toShort();
144     if(!d->items.contains("\251gen") && idx > 0) {
145       d->items.insert("\251gen", StringList(ID3v1::genre(idx - 1)));
146     }
147   }
148 }
149
150 void
151 MP4::Tag::parseIntPair(MP4::Atom *atom, TagLib::File *file)
152 {
153   ByteVectorList data = parseData(atom, file);
154   if(data.size()) {
155     int a = data[0].mid(2, 2).toShort();
156     int b = data[0].mid(4, 2).toShort();
157     d->items.insert(atom->name, MP4::Item(a, b));
158   }
159 }
160
161 void
162 MP4::Tag::parseBool(MP4::Atom *atom, TagLib::File *file)
163 {
164   ByteVectorList data = parseData(atom, file);
165   if(data.size()) {
166     bool value = data[0].size() ? data[0][0] != '\0' : false;
167     d->items.insert(atom->name, value);
168   }
169 }
170
171 void
172 MP4::Tag::parseText(MP4::Atom *atom, TagLib::File *file, int expectedFlags)
173 {
174   ByteVectorList data = parseData(atom, file, expectedFlags);
175   if(data.size()) {
176     StringList value;
177     for(unsigned int i = 0; i < data.size(); i++) {
178       value.append(String(data[i], String::UTF8));
179     }
180     d->items.insert(atom->name, value);
181   }
182 }
183
184 void
185 MP4::Tag::parseFreeForm(MP4::Atom *atom, TagLib::File *file)
186 {
187   ByteVectorList data = parseData(atom, file, 1, true);
188   if(data.size() > 2) {
189     StringList value;
190     for(unsigned int i = 2; i < data.size(); i++) {
191       value.append(String(data[i], String::UTF8));
192     }
193     String name = "----:" + data[0] + ':' + data[1];
194     d->items.insert(name, value);
195   }
196 }
197
198 void
199 MP4::Tag::parseCovr(MP4::Atom *atom, TagLib::File *file)
200 {
201   MP4::CoverArtList value;
202   ByteVector data = file->readBlock(atom->length - 8);
203   unsigned int pos = 0;
204   while(pos < data.size()) {
205     int length = data.mid(pos, 4).toUInt();
206     ByteVector name = data.mid(pos + 4, 4);
207     int flags = data.mid(pos + 8, 4).toUInt();
208     if(name != "data") {
209       debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\"");
210       break;
211     }
212     if(flags == MP4::CoverArt::PNG || flags == MP4::CoverArt::JPEG) {
213       value.append(MP4::CoverArt(MP4::CoverArt::Format(flags),
214                                  data.mid(pos + 16, length - 16)));
215     }
216     pos += length;
217   }
218   if(value.size() > 0)
219     d->items.insert(atom->name, value);
220 }
221
222 ByteVector
223 MP4::Tag::padIlst(const ByteVector &data, int length)
224 {
225   if (length == -1) {
226     length = ((data.size() + 1023) & ~1023) - data.size();
227   }
228   return renderAtom("free", ByteVector(length, '\1'));
229 }
230
231 ByteVector
232 MP4::Tag::renderAtom(const ByteVector &name, const ByteVector &data)
233 {
234   return ByteVector::fromUInt(data.size() + 8) + name + data;
235 }
236
237 ByteVector
238 MP4::Tag::renderData(const ByteVector &name, int flags, const ByteVectorList &data)
239 {
240   ByteVector result;
241   for(unsigned int i = 0; i < data.size(); i++) {
242     result.append(renderAtom("data", ByteVector::fromUInt(flags) + ByteVector(4, '\0') + data[i]));
243   }
244   return renderAtom(name, result);
245 }
246
247 ByteVector
248 MP4::Tag::renderBool(const ByteVector &name, MP4::Item &item)
249 {
250   ByteVectorList data;
251   data.append(ByteVector(1, item.toBool() ? '\1' : '\0'));
252   return renderData(name, 0x15, data);
253 }
254
255 ByteVector
256 MP4::Tag::renderInt(const ByteVector &name, MP4::Item &item)
257 {
258   ByteVectorList data;
259   data.append(ByteVector::fromShort(item.toInt()));
260   return renderData(name, 0x15, data);
261 }
262
263 ByteVector
264 MP4::Tag::renderIntPair(const ByteVector &name, MP4::Item &item)
265 {
266   ByteVectorList data;
267   data.append(ByteVector(2, '\0') +
268               ByteVector::fromShort(item.toIntPair().first) +
269               ByteVector::fromShort(item.toIntPair().second) +
270               ByteVector(2, '\0'));
271   return renderData(name, 0x00, data);
272 }
273
274 ByteVector
275 MP4::Tag::renderIntPairNoTrailing(const ByteVector &name, MP4::Item &item)
276 {
277   ByteVectorList data;
278   data.append(ByteVector(2, '\0') +
279               ByteVector::fromShort(item.toIntPair().first) +
280               ByteVector::fromShort(item.toIntPair().second));
281   return renderData(name, 0x00, data);
282 }
283
284 ByteVector
285 MP4::Tag::renderText(const ByteVector &name, MP4::Item &item, int flags)
286 {
287   ByteVectorList data;
288   StringList value = item.toStringList();
289   for(unsigned int i = 0; i < value.size(); i++) {
290     data.append(value[i].data(String::UTF8));
291   }
292   return renderData(name, flags, data);
293 }
294
295 ByteVector
296 MP4::Tag::renderCovr(const ByteVector &name, MP4::Item &item)
297 {
298   ByteVector data;
299   MP4::CoverArtList value = item.toCoverArtList();
300   for(unsigned int i = 0; i < value.size(); i++) {
301     data.append(renderAtom("data", ByteVector::fromUInt(value[i].format()) +
302                                    ByteVector(4, '\0') + value[i].data()));
303   }
304   return renderAtom(name, data);
305 }
306
307 ByteVector
308 MP4::Tag::renderFreeForm(const String &name, MP4::Item &item)
309 {
310   StringList header = StringList::split(name, ":");
311   if (header.size() != 3) {
312     debug("MP4: Invalid free-form item name \"" + name + "\"");
313     return ByteVector::null;
314   }
315   ByteVector data;
316   data.append(renderAtom("mean", ByteVector::fromUInt(0) + header[1].data(String::UTF8)));
317   data.append(renderAtom("name", ByteVector::fromUInt(0) + header[2].data(String::UTF8)));
318   StringList value = item.toStringList();
319   for(unsigned int i = 0; i < value.size(); i++) {
320     data.append(renderAtom("data", ByteVector::fromUInt(1) + ByteVector(4, '\0') + value[i].data(String::UTF8)));
321   }
322   return renderAtom("----", data);
323 }
324
325 bool
326 MP4::Tag::save()
327 {
328   ByteVector data;
329   for(MP4::ItemListMap::Iterator i = d->items.begin(); i != d->items.end(); i++) {
330     const String name = i->first;
331     if(name.startsWith("----")) {
332       data.append(renderFreeForm(name, i->second));
333     }
334     else if(name == "trkn") {
335       data.append(renderIntPair(name.data(String::Latin1), i->second));
336     }
337     else if(name == "disk") {
338       data.append(renderIntPairNoTrailing(name.data(String::Latin1), i->second));
339     }
340     else if(name == "cpil" || name == "pgap" || name == "pcst") {
341       data.append(renderBool(name.data(String::Latin1), i->second));
342     }
343     else if(name == "tmpo") {
344       data.append(renderInt(name.data(String::Latin1), i->second));
345     }
346     else if(name == "covr") {
347       data.append(renderCovr(name.data(String::Latin1), i->second));
348     }
349     else if(name.size() == 4){
350       data.append(renderText(name.data(String::Latin1), i->second));
351     }
352     else {
353       debug("MP4: Unknown item name \"" + name + "\"");
354     }
355   }
356   data = renderAtom("ilst", data);
357
358   AtomList path = d->atoms->path("moov", "udta", "meta", "ilst");
359   if(path.size() == 4) {
360     saveExisting(data, path);
361   }
362   else {
363     saveNew(data);
364   }
365
366   return true;
367 }
368
369 void
370 MP4::Tag::updateParents(AtomList &path, long delta, int ignore)
371 {
372   for(unsigned int i = 0; i < path.size() - ignore; i++) {
373     d->file->seek(path[i]->offset);
374     long size = d->file->readBlock(4).toUInt();
375     // 64-bit
376     if (size == 1) {
377       d->file->seek(4, File::Current); // Skip name
378       long long longSize = d->file->readBlock(8).toLongLong();
379       // Seek the offset of the 64-bit size
380       d->file->seek(path[i]->offset + 8);
381       d->file->writeBlock(ByteVector::fromLongLong(longSize + delta));
382     }
383     // 32-bit
384     else {
385       d->file->seek(path[i]->offset);
386       d->file->writeBlock(ByteVector::fromUInt(size + delta));
387     }
388   }
389 }
390
391 void
392 MP4::Tag::updateOffsets(long delta, long offset)
393 {
394   MP4::Atom *moov = d->atoms->find("moov");
395   if(moov) {
396     MP4::AtomList stco = moov->findall("stco", true);
397     for(unsigned int i = 0; i < stco.size(); i++) {
398       MP4::Atom *atom = stco[i];
399       if(atom->offset > offset) {
400         atom->offset += delta;
401       }
402       d->file->seek(atom->offset + 12);
403       ByteVector data = d->file->readBlock(atom->length - 12);
404       unsigned int count = data.mid(0, 4).toUInt();
405       d->file->seek(atom->offset + 16);
406       int pos = 4;
407       while(count--) {
408         long o = data.mid(pos, 4).toUInt();
409         if(o > offset) {
410           o += delta;
411         }
412         d->file->writeBlock(ByteVector::fromUInt(o));
413         pos += 4;
414       }
415     }
416
417     MP4::AtomList co64 = moov->findall("co64", true);
418     for(unsigned int i = 0; i < co64.size(); i++) {
419       MP4::Atom *atom = co64[i];
420       if(atom->offset > offset) {
421         atom->offset += delta;
422       }
423       d->file->seek(atom->offset + 12);
424       ByteVector data = d->file->readBlock(atom->length - 12);
425       unsigned int count = data.mid(0, 4).toUInt();
426       d->file->seek(atom->offset + 16);
427       int pos = 4;
428       while(count--) {
429         long long o = data.mid(pos, 8).toLongLong();
430         if(o > offset) {
431           o += delta;
432         }
433         d->file->writeBlock(ByteVector::fromLongLong(o));
434         pos += 8;
435       }
436     }
437   }
438
439   MP4::Atom *moof = d->atoms->find("moof");
440   if(moof) {
441     MP4::AtomList tfhd = moof->findall("tfhd", true);
442     for(unsigned int i = 0; i < tfhd.size(); i++) {
443       MP4::Atom *atom = tfhd[i];
444       if(atom->offset > offset) {
445         atom->offset += delta;
446       }
447       d->file->seek(atom->offset + 9);
448       ByteVector data = d->file->readBlock(atom->offset - 9);
449       unsigned int flags = (ByteVector(1, '\0') + data.mid(0, 3)).toUInt();
450       if(flags & 1) {
451         long long o = data.mid(7, 8).toLongLong();
452         if(o > offset) {
453           o += delta;
454         }
455         d->file->seek(atom->offset + 16);
456         d->file->writeBlock(ByteVector::fromLongLong(o));
457       }
458     }
459   }
460 }
461
462 void
463 MP4::Tag::saveNew(ByteVector &data)
464 {
465   data = renderAtom("meta", TagLib::ByteVector(4, '\0') +
466                     renderAtom("hdlr", TagLib::ByteVector(8, '\0') + TagLib::ByteVector("mdirappl") + TagLib::ByteVector(9, '\0')) +
467                     data + padIlst(data));
468
469   AtomList path = d->atoms->path("moov", "udta");
470   if(path.size() != 2) {
471     path = d->atoms->path("moov");
472     data = renderAtom("udta", data);
473   }
474
475   long offset = path[path.size() - 1]->offset + 8;
476   d->file->insert(data, offset, 0);
477
478   updateParents(path, data.size());
479   updateOffsets(data.size(), offset);
480 }
481
482 void
483 MP4::Tag::saveExisting(ByteVector &data, AtomList &path)
484 {
485   MP4::Atom *ilst = path[path.size() - 1];
486   long offset = ilst->offset;
487   long length = ilst->length;
488
489   MP4::Atom *meta = path[path.size() - 2];
490   AtomList::Iterator index = meta->children.find(ilst);
491
492   // check if there is an atom before 'ilst', and possibly use it as padding
493   if(index != meta->children.begin()) {
494     AtomList::Iterator prevIndex = index;
495     prevIndex--;
496     MP4::Atom *prev = *prevIndex;
497     if(prev->name == "free") {
498       offset = prev->offset;
499       length += prev->length;
500     }
501   }
502   // check if there is an atom after 'ilst', and possibly use it as padding
503   AtomList::Iterator nextIndex = index;
504   nextIndex++;
505   if(nextIndex != meta->children.end()) {
506     MP4::Atom *next = *nextIndex;
507     if(next->name == "free") {
508       length += next->length;
509     }
510   }
511
512   long delta = data.size() - length;
513   if(delta > 0 || (delta < 0 && delta > -8)) {
514     data.append(padIlst(data));
515     delta = data.size() - length;
516   }
517   else if(delta < 0) {
518     data.append(padIlst(data, -delta - 8));
519     delta = 0;
520   }
521
522   d->file->insert(data, offset, length);
523
524   if(delta) {
525     updateParents(path, delta, 1);
526     updateOffsets(delta, offset);
527   }
528 }
529
530 String
531 MP4::Tag::title() const
532 {
533   if(d->items.contains("\251nam"))
534     return d->items["\251nam"].toStringList().toString(", ");
535   return String::null;
536 }
537
538 String
539 MP4::Tag::artist() const
540 {
541   if(d->items.contains("\251ART"))
542     return d->items["\251ART"].toStringList().toString(", ");
543   return String::null;
544 }
545
546 String
547 MP4::Tag::album() const
548 {
549   if(d->items.contains("\251alb"))
550     return d->items["\251alb"].toStringList().toString(", ");
551   return String::null;
552 }
553
554 String
555 MP4::Tag::comment() const
556 {
557   if(d->items.contains("\251cmt"))
558     return d->items["\251cmt"].toStringList().toString(", ");
559   return String::null;
560 }
561
562 String
563 MP4::Tag::genre() const
564 {
565   if(d->items.contains("\251gen"))
566     return d->items["\251gen"].toStringList().toString(", ");
567   return String::null;
568 }
569
570 unsigned int
571 MP4::Tag::year() const
572 {
573   if(d->items.contains("\251day"))
574     return d->items["\251day"].toStringList().toString().toInt();
575   return 0;
576 }
577
578 unsigned int
579 MP4::Tag::track() const
580 {
581   if(d->items.contains("trkn"))
582     return d->items["trkn"].toIntPair().first;
583   return 0;
584 }
585
586 void
587 MP4::Tag::setTitle(const String &value)
588 {
589   d->items["\251nam"] = StringList(value);
590 }
591
592 void
593 MP4::Tag::setArtist(const String &value)
594 {
595   d->items["\251ART"] = StringList(value);
596 }
597
598 void
599 MP4::Tag::setAlbum(const String &value)
600 {
601   d->items["\251alb"] = StringList(value);
602 }
603
604 void
605 MP4::Tag::setComment(const String &value)
606 {
607   d->items["\251cmt"] = StringList(value);
608 }
609
610 void
611 MP4::Tag::setGenre(const String &value)
612 {
613   d->items["\251gen"] = StringList(value);
614 }
615
616 void
617 MP4::Tag::setYear(uint value)
618 {
619   d->items["\251day"] = StringList(String::number(value));
620 }
621
622 void
623 MP4::Tag::setTrack(uint value)
624 {
625   d->items["trkn"] = MP4::Item(value, 0);
626 }
627
628 MP4::ItemListMap &
629 MP4::Tag::itemListMap()
630 {
631   return d->items;
632 }
633