File indexing completed on 2024-05-12 04:55:40

0001 /**
0002  * \file m4afile.cpp
0003  * Handling of MPEG-4 audio files.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 25 Oct 2007
0008  *
0009  * Copyright (C) 2007-2023  Urs Fleisch
0010  *
0011  * This file is part of Kid3.
0012  *
0013  * Kid3 is free software; you can redistribute it and/or modify
0014  * it under the terms of the GNU General Public License as published by
0015  * the Free Software Foundation; either version 2 of the License, or
0016  * (at your option) any later version.
0017  *
0018  * Kid3 is distributed in the hope that it will be useful,
0019  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0020  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0021  * GNU General Public License for more details.
0022  *
0023  * You should have received a copy of the GNU General Public License
0024  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0025  */
0026 
0027 #include "m4afile.h"
0028 #include "mp4v2config.h"
0029 
0030 #include <QFile>
0031 #include <QDir>
0032 #include <QByteArray>
0033 #include <stdio.h>
0034 #ifdef HAVE_MP4V2_MP4V2_H
0035 #include <mp4v2/mp4v2.h>
0036 #else
0037 #include <mp4.h>
0038 #endif
0039 #include <cstdlib>
0040 #include <cstring>
0041 #include "genres.h"
0042 #include "pictureframe.h"
0043 
0044 /** MPEG4IP version as 16-bit hex number with major and minor version. */
0045 #if defined MP4V2_PROJECT_version_major && defined MP4V2_PROJECT_version_minor
0046 #define MPEG4IP_MAJOR_MINOR_VERSION ((MP4V2_PROJECT_version_major << 8) | \
0047   MP4V2_PROJECT_version_minor)
0048 #elif defined MPEG4IP_MAJOR_VERSION && defined MPEG4IP_MINOR_VERSION
0049 #define MPEG4IP_MAJOR_MINOR_VERSION ((MPEG4IP_MAJOR_VERSION << 8) | \
0050   MPEG4IP_MINOR_VERSION)
0051 #else
0052 #define MPEG4IP_MAJOR_MINOR_VERSION 0x0009
0053 #endif
0054 
0055 #if MPEG4IP_MAJOR_MINOR_VERSION < 0x0200
0056 /** Set content ID. */
0057 #define MP4TagsSetContentID MP4TagsSetCNID
0058 /** Set artist ID. */
0059 #define MP4TagsSetArtistID MP4TagsSetATID
0060 /** Set playlist ID. */
0061 #define MP4TagsSetPlaylistID MP4TagsSetPLID
0062 /** Set genre ID. */
0063 #define MP4TagsSetGenreID MP4TagsSetGEID
0064 #endif
0065 
0066 namespace {
0067 
0068 /** Mapping between frame types and field names. */
0069 const struct {
0070   const char* name;
0071   Frame::Type type;
0072 } nameTypes[] = {
0073   { "\251nam", Frame::FT_Title },
0074   { "\251ART", Frame::FT_Artist },
0075   { "\251wrt", Frame::FT_Composer },
0076   { "\251alb", Frame::FT_Album },
0077   { "\251day", Frame::FT_Date },
0078   { "\251enc", Frame::FT_EncodedBy },
0079   { "\251cmt", Frame::FT_Comment },
0080   { "\251gen", Frame::FT_Genre },
0081   { "trkn", Frame::FT_Track },
0082   { "disk", Frame::FT_Disc },
0083   { "gnre", Frame::FT_Genre },
0084   { "cpil", Frame::FT_Compilation },
0085   { "tmpo", Frame::FT_Bpm },
0086 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0105
0087   { "\251grp", Frame::FT_Grouping },
0088 #endif
0089 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106
0090   { "aART", Frame::FT_AlbumArtist },
0091   { "pgap", Frame::FT_Other },
0092 #endif
0093 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
0094   { "cprt", Frame::FT_Copyright },
0095   { "\251lyr", Frame::FT_Lyrics },
0096   { "tvsh", Frame::FT_Other },
0097   { "tvnn", Frame::FT_Other },
0098   { "tven", Frame::FT_Other },
0099   { "tvsn", Frame::FT_Other },
0100   { "tves", Frame::FT_Other },
0101   { "desc", Frame::FT_Description },
0102   { "ldes", Frame::FT_Other },
0103   { "sonm", Frame::FT_SortName },
0104   { "soar", Frame::FT_SortArtist },
0105   { "soaa", Frame::FT_SortAlbumArtist },
0106   { "soal", Frame::FT_SortAlbum },
0107   { "soco", Frame::FT_SortComposer },
0108   { "sosn", Frame::FT_Other },
0109   { "\251too", Frame::FT_EncoderSettings },
0110   { "\251wrk", Frame::FT_Work },
0111   { "purd", Frame::FT_Other },
0112   { "pcst", Frame::FT_Other },
0113   { "keyw", Frame::FT_Other },
0114   { "catg", Frame::FT_Other },
0115   { "hdvd", Frame::FT_Other },
0116   { "stik", Frame::FT_Other },
0117   { "rtng", Frame::FT_Other },
0118   { "apID", Frame::FT_Other },
0119   { "akID", Frame::FT_Other },
0120   { "sfID", Frame::FT_Other },
0121   { "cnID", Frame::FT_Other },
0122   { "atID", Frame::FT_Other },
0123   { "plID", Frame::FT_Other },
0124   { "geID", Frame::FT_Other },
0125   { "purl", Frame::FT_Other },
0126   { "egid", Frame::FT_Other },
0127   { "cmID", Frame::FT_Other },
0128   { "xid ", Frame::FT_Other },
0129 #endif
0130   { "covr", Frame::FT_Picture }
0131 },
0132 freeFormNameTypes[] = {
0133 #if !(MPEG4IP_MAJOR_MINOR_VERSION >= 0x0105)
0134   { "GROUPING", Frame::FT_Grouping },
0135 #endif
0136 #if !(MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106)
0137   { "ALBUMARTIST", Frame::FT_AlbumArtist },
0138 #endif
0139   { "ARRANGER", Frame::FT_Arranger },
0140   { "AUTHOR", Frame::FT_Author },
0141   { "CATALOGNUMBER", Frame::FT_CatalogNumber },
0142   { "CONDUCTOR", Frame::FT_Conductor },
0143   { "ENCODINGTIME", Frame::FT_EncodingTime },
0144   { "INITIALKEY", Frame::FT_InitialKey },
0145 #if !(MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109)
0146   { "COPYRIGHT", Frame::FT_Copyright },
0147 #endif
0148   { "ISRC", Frame::FT_Isrc },
0149   { "LANGUAGE", Frame::FT_Language },
0150   { "LYRICIST", Frame::FT_Lyricist },
0151 #if !(MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109)
0152   { "LYRICS", Frame::FT_Lyrics },
0153 #endif
0154   { "MOOD", Frame::FT_Mood },
0155   { "SOURCEMEDIA", Frame::FT_Media },
0156   { "ORIGINALALBUM", Frame::FT_OriginalAlbum },
0157   { "ORIGINALARTIST", Frame::FT_OriginalArtist },
0158   { "ORIGINALDATE", Frame::FT_OriginalDate },
0159   { "PERFORMER", Frame::FT_Performer },
0160   { "PUBLISHER", Frame::FT_Publisher },
0161   { "RELEASECOUNTRY", Frame::FT_ReleaseCountry },
0162   { "REMIXER", Frame::FT_Remixer },
0163   { "SUBTITLE", Frame::FT_Subtitle },
0164   { "WEBSITE", Frame::FT_Website },
0165   { "WWWAUDIOFILE", Frame::FT_WWWAudioFile },
0166   { "WWWAUDIOSOURCE", Frame::FT_WWWAudioSource },
0167   { "RELEASEDATE", Frame::FT_ReleaseDate },
0168   { "rate", Frame::FT_Rating }
0169 };
0170 
0171 /**
0172  * Get the predefined field name for a type.
0173  *
0174  * @param type frame type
0175  *
0176  * @return field name, QString::null if not defined.
0177  */
0178 QString getNameForType(Frame::Type type)
0179 {
0180   static QMap<Frame::Type, QString> typeNameMap;
0181   if (typeNameMap.empty()) {
0182     // first time initialization
0183     for (const auto& nameType : nameTypes) {
0184       if (nameType.type != Frame::FT_Other) {
0185         typeNameMap.insert(nameType.type, QString::fromLatin1(nameType.name));
0186       }
0187     }
0188     for (const auto& freeFormNameType : freeFormNameTypes) {
0189       typeNameMap.insert(freeFormNameType.type,
0190                          QString::fromLatin1(freeFormNameType.name));
0191     }
0192   }
0193   if (type != Frame::FT_Other) {
0194     auto it = typeNameMap.constFind(type);
0195     if (it != typeNameMap.constEnd()) {
0196       return *it;
0197     } else {
0198       auto customFrameName = Frame::getNameForCustomFrame(type);
0199       if (!customFrameName.isEmpty()) {
0200         return QString::fromLatin1(customFrameName);
0201       }
0202     }
0203   }
0204   return QString();
0205 }
0206 
0207 /**
0208  * Get the type for a predefined field name.
0209  *
0210  * @param name           field name
0211  * @param onlyPredefined if true, FT_Unknown is returned for fields which
0212  *                       are not predefined, else FT_Other
0213  *
0214  * @return type, FT_Unknown or FT_Other if not predefined field.
0215  */
0216 Frame::Type getTypeForName(const QString& name, bool onlyPredefined = false)
0217 {
0218   if (name.length() == 4) {
0219     static QMap<QString, Frame::Type> nameTypeMap;
0220     if (nameTypeMap.empty()) {
0221       // first time initialization
0222       for (const auto& nameType : nameTypes) {
0223         nameTypeMap.insert(QString::fromLatin1(nameType.name), nameType.type);
0224       }
0225     }
0226     auto it = nameTypeMap.constFind(name);
0227     if (it != nameTypeMap.constEnd()) {
0228       Frame::Type type = *it;
0229       if (type == Frame::FT_Other) {
0230         type = Frame::getTypeFromCustomFrameName(name.toLatin1());
0231       }
0232       return type;
0233     }
0234   }
0235   if (!onlyPredefined) {
0236     static QMap<QString, Frame::Type> freeFormNameTypeMap;
0237     if (freeFormNameTypeMap.empty()) {
0238       // first time initialization
0239       for (const auto& freeFormNameType : freeFormNameTypes) {
0240         freeFormNameTypeMap.insert(QString::fromLatin1(freeFormNameType.name),
0241                                    freeFormNameType.type);
0242       }
0243     }
0244     auto it = freeFormNameTypeMap.constFind(name);
0245     if (it != freeFormNameTypeMap.constEnd()) {
0246       return *it;
0247     }
0248     return Frame::getTypeFromCustomFrameName(name.toLatin1());
0249   }
0250   return Frame::FT_UnknownFrame;
0251 }
0252 
0253 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
0254 #elif defined HAVE_MP4V2_MP4GETMETADATABYINDEX_CHARPP_ARG
0255 #else
0256 /**
0257  * Check if a name is a free form field.
0258  *
0259  * @param hFile handle
0260  * @param name  field name
0261  *
0262  * @return true if a free form field.
0263  */
0264 bool isFreeFormMetadata(MP4FileHandle hFile, const char* name)
0265 {
0266   bool result = false;
0267   if (getTypeForName(name, true) == Frame::FT_UnknownFrame) {
0268     uint8_t* pValue = 0;
0269     uint32_t valueSize = 0;
0270     result = MP4GetMetadataFreeForm(hFile, const_cast<char*>(name),
0271                                     &pValue, &valueSize);
0272     if (pValue && valueSize > 0) {
0273       free(pValue);
0274     }
0275   }
0276   return result;
0277 }
0278 #endif
0279 
0280 /**
0281  * Get a byte array for a value.
0282  *
0283  * @param name  field name
0284  * @param value field value
0285  * @param size  size of value in bytes
0286  *
0287  * @return byte array with string representation.
0288  */
0289 QByteArray getValueByteArray(const char* name,
0290                              const uint8_t* value, uint32_t size)
0291 {
0292   QByteArray str;
0293   if (name[0] == '\251') {
0294     str = QByteArray(reinterpret_cast<const char*>(value), size);
0295   } else if (std::strcmp(name, "trkn") == 0) {
0296     if (size >= 6) {
0297       unsigned track = value[3] + (value[2] << 8);
0298       unsigned totalTracks = value[5] + (value[4] << 8);
0299       str.setNum(track);
0300       if (totalTracks > 0) {
0301         str += '/';
0302         str += QByteArray().setNum(totalTracks);
0303       }
0304     }
0305   } else if (std::strcmp(name, "disk") == 0) {
0306     if (size >= 6) {
0307       unsigned disk = value[3] + (value[2] << 8);
0308       unsigned totalDisks = value[5] + (value[4] << 8);
0309       str.setNum(disk);
0310       if (totalDisks > 0) {
0311         str += '/';
0312         str += QByteArray().setNum(totalDisks);
0313       }
0314     }
0315   } else if (std::strcmp(name, "gnre") == 0) {
0316     if (size >= 2) {
0317       unsigned genreNum = value[1] + (value[0] << 8);
0318       if (genreNum > 0) {
0319         str = Genres::getName(genreNum - 1);
0320       }
0321     }
0322   } else if (std::strcmp(name, "cpil") == 0) {
0323     if (size >= 1) {
0324       str.setNum(value[0]);
0325     }
0326   } else if (std::strcmp(name, "tmpo") == 0) {
0327     if (size >= 2) {
0328       unsigned bpm = value[1] + (value[0] << 8);
0329       if (bpm > 0) {
0330         str.setNum(bpm);
0331       }
0332     }
0333   } else if (std::strcmp(name, "covr") == 0) {
0334     QByteArray ba;
0335     ba = QByteArray(reinterpret_cast<const char*>(value), size);
0336     return ba;
0337 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106
0338   } else if (std::strcmp(name, "pgap") == 0) {
0339     if (size >= 1) {
0340       str.setNum(value[0]);
0341     }
0342 #endif
0343 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
0344   } else if (std::strcmp(name, "tvsn") == 0 || std::strcmp(name, "tves") == 0 ||
0345              std::strcmp(name, "sfID") == 0 || std::strcmp(name, "cnID") == 0 ||
0346              std::strcmp(name, "atID") == 0 || std::strcmp(name, "geID") == 0 ||
0347              std::strcmp(name, "cmID") == 0) {
0348     if (size >= 4) {
0349       uint val = value[3] + (value[2] << 8) +
0350         (value[1] << 16) + (value[0] << 24);
0351       if (val > 0) {
0352         str.setNum(val);
0353       }
0354     }
0355   } else if (std::strcmp(name, "pcst") == 0 || std::strcmp(name, "hdvd") == 0 ||
0356              std::strcmp(name, "stik") == 0 || std::strcmp(name, "rtng") == 0 ||
0357              std::strcmp(name, "akID") == 0) {
0358     if (size >= 1) {
0359       str.setNum(value[0]);
0360     }
0361   } else if (std::strcmp(name, "plID") == 0) {
0362     if (size >= 8) {
0363       qulonglong val =
0364           static_cast<qulonglong>(value[7]) +
0365           (static_cast<qulonglong>(value[6]) << 8) +
0366           (static_cast<qulonglong>(value[5]) << 16) +
0367           (static_cast<qulonglong>(value[4]) << 24) +
0368           (static_cast<qulonglong>(value[3]) << 32) +
0369           (static_cast<qulonglong>(value[2]) << 40) +
0370           (static_cast<qulonglong>(value[1]) << 48) +
0371           (static_cast<qulonglong>(value[0]) << 56);
0372       if (val > 0) {
0373         str.setNum(val);
0374       }
0375     }
0376 #endif
0377   } else {
0378     str = QByteArray(reinterpret_cast<const char*>(value), size);
0379   }
0380   return str;
0381 }
0382 
0383 /**
0384  * Set a SYLT frame with data from MP4 chapters.
0385  * @param frame frame to set
0386  * @param data list with time stamps and chapter titles
0387  */
0388 void setMp4ChaptersFields(Frame& frame,
0389                           const QVariantList& data = QVariantList())
0390 {
0391   frame.setExtendedType(Frame::ExtendedType(Frame::FT_Other,
0392                                             QLatin1String("Chapters")));
0393   frame.setValue(QString());
0394 
0395   Frame::Field field;
0396   Frame::FieldList& fields = frame.fieldList();
0397   fields.clear();
0398 
0399   field.m_id = Frame::ID_TimestampFormat;
0400   field.m_value = 2; // milliseconds
0401   fields.append(field);
0402 
0403   field.m_id = Frame::ID_ContentType;
0404   field.m_value = 0; // other
0405   fields.append(field);
0406 
0407   field.m_id = Frame::ID_Description;
0408   field.m_value = QString();
0409   fields.append(field);
0410 
0411   field.m_id = Frame::ID_Data;
0412   field.m_value = data;
0413   fields.append(field);
0414 }
0415 
0416 /**
0417  * Set a SYLT frame from MP4 chapters.
0418  * @param chapterList MP4 chapters
0419  * @param chapterCount number of elements in chapterList
0420  * @param frame the SYLT frame is returned here
0421  */
0422 void mp4ChaptersToFrame(const MP4Chapter_t* chapterList, uint32_t chapterCount,
0423                         Frame& frame)
0424 {
0425   QVariantList data;
0426   quint32 time = 0;
0427   for (uint32_t i = 0; i < chapterCount; ++i) {
0428     MP4Chapter_t chapter = chapterList[i];
0429     data.append(time);
0430     data.append(QString::fromUtf8(chapter.title));
0431     time += chapter.duration;
0432   }
0433   data.append(time);
0434   data.append(QString());
0435   setMp4ChaptersFields(frame, data);
0436 }
0437 
0438 /**
0439  * Set MP4 chapters from a SYLT frame.
0440  * @param frame SYLT frame
0441  * @param chapterList the chapters are returned here and must be freed using
0442  * delete[] afterwards
0443  * @param chapterCount the number of elements in @a chapterList is returned here
0444  */
0445 void frameToMp4Chapters(const Frame& frame,
0446                         MP4Chapter_t*& chapterList, uint32_t& chapterCount)
0447 {
0448   QVariantList data = Frame::getField(frame, Frame::ID_Data).toList();
0449   int dataLen = data.size();
0450   if (dataLen >= 2) {
0451     quint32 lastTime = data.at(dataLen - 2).toUInt();
0452     QString lastTitle = data.at(dataLen - 1).toString();
0453     if (!lastTitle.trimmed().isEmpty()) {
0454       data.append(lastTime);
0455       data.append(QString());
0456       dataLen += 2;
0457     }
0458   }
0459   if (dataLen > 2 && (dataLen & 1) == 0) {
0460     chapterCount = (dataLen - 2) / 2;
0461     chapterList = new MP4Chapter_t[chapterCount];
0462     quint32 lastTime = 0;
0463     uint32_t i = 0;
0464     QListIterator<QVariant> it(data);
0465     while (it.hasNext()) {
0466       quint32 time = it.next().toUInt();
0467       if (!it.hasNext())
0468         break;
0469 
0470       QByteArray chapterTitle = it.next().toString().trimmed().toUtf8();
0471       if (i < chapterCount) {
0472         MP4Chapter_t* mp4Chapter = &chapterList[i];
0473         qstrncpy(mp4Chapter->title, chapterTitle.constData(),
0474                  sizeof(mp4Chapter->title) - 1);
0475         mp4Chapter->title[sizeof(mp4Chapter->title) - 1] = '\0';
0476       }
0477       if (i > 0 && i <= chapterCount) {
0478         chapterList[i - 1].duration = time - lastTime;
0479       }
0480       lastTime = time;
0481       ++i;
0482     }
0483   } else {
0484     chapterCount = 0;
0485     chapterList = nullptr;
0486   }
0487 }
0488 
0489 /**
0490  * Check if two chapters frames are equal.
0491  * @param f1 first chapters frame
0492  * @param f2 second chapters frame
0493  * @return true if equal.
0494  */
0495 bool areMp4ChaptersFieldsEqual(const Frame& f1, const Frame& f2)
0496 {
0497   return Frame::getField(f1, Frame::ID_Data) == Frame::getField(f2, Frame::ID_Data);
0498 }
0499 
0500 }
0501 
0502 /**
0503  * Constructor.
0504  *
0505  * @param idx index in tagged file system model
0506  */
0507 M4aFile::M4aFile(const QPersistentModelIndex& idx)
0508   : TaggedFile(idx), m_fileRead(false)
0509 {
0510 }
0511 
0512 /**
0513  * Get key of tagged file format.
0514  * @return "Mp4v2Metadata".
0515  */
0516 QString M4aFile::taggedFileKey() const
0517 {
0518   return QLatin1String("Mp4v2Metadata");
0519 }
0520 
0521 /**
0522  * Read tags from file.
0523  *
0524  * @param force true to force reading even if tags were already read.
0525  */
0526 void M4aFile::readTags(bool force)
0527 {
0528   bool priorIsTagInformationRead = isTagInformationRead();
0529   if (force || !m_fileRead) {
0530     m_metadata.clear();
0531     m_extraFrames.clear();
0532     markTagUnchanged(Frame::Tag_2);
0533     m_fileRead = true;
0534     QByteArray fnIn =
0535 #ifdef Q_OS_WIN32
0536         currentFilePath().toUtf8();
0537 #else
0538         QFile::encodeName(currentFilePath());
0539 #endif
0540 
0541     MP4FileHandle handle = MP4Read(fnIn);
0542     if (handle != MP4_INVALID_FILE_HANDLE) {
0543       m_fileInfo.read(handle);
0544 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
0545     MP4ItmfItemList* list = MP4ItmfGetItems(handle);
0546     if (list) {
0547       for (uint32_t i = 0; i < list->size; ++i) {
0548         MP4ItmfItem& item = list->elements[i];
0549         const char* key = nullptr;
0550         if (memcmp(item.code, "----", 4) == 0) {
0551           // free form tagfield
0552           if (item.name) {
0553             key = item.name;
0554           }
0555         } else {
0556           key = item.code;
0557         }
0558         if (key) {
0559           if (std::strcmp(key, "covr") == 0) {
0560             if (item.dataList.size > 0) {
0561               int i;
0562               MP4ItmfData* element;
0563               for (i = 0, element = item.dataList.elements;
0564                    i < static_cast<int>(item.dataList.size);
0565                    ++i, ++element) {
0566                 QString mimeType, imgFormat;
0567                 switch (element->typeCode) {
0568                 case MP4_ITMF_BT_PNG:
0569                   mimeType = QLatin1String("image/png");
0570                   imgFormat = QLatin1String("PNG");
0571                   break;
0572                 case MP4_ITMF_BT_BMP:
0573                   mimeType = QLatin1String("image/bmp");
0574                   imgFormat = QLatin1String("BMP");
0575                   break;
0576                 case MP4_ITMF_BT_GIF:
0577                   mimeType = QLatin1String("image/gif");
0578                   imgFormat = QLatin1String("GIF");
0579                   break;
0580                 case MP4_ITMF_BT_JPEG:
0581                 default:
0582                   mimeType = QLatin1String("image/jpeg");
0583                   imgFormat = QLatin1String("JPG");
0584                 }
0585                 PictureFrame frame(
0586                       getValueByteArray(key, element->value, element->valueSize),
0587                       QLatin1String(""), PictureFrame::PT_CoverFront, mimeType,
0588                       Frame::TE_ISO8859_1, imgFormat);
0589                 frame.setIndex(Frame::toNegativeIndex(i));
0590                 frame.setExtendedType(Frame::ExtendedType(Frame::FT_Picture,
0591                                                           QLatin1String(key)));
0592                 m_extraFrames.append(frame);
0593               }
0594             }
0595           } else {
0596             QByteArray ba;
0597             if (item.dataList.size > 0 &&
0598                 item.dataList.elements[0].value &&
0599                 item.dataList.elements[0].valueSize > 0) {
0600               ba = getValueByteArray(key, item.dataList.elements[0].value,
0601                   item.dataList.elements[0].valueSize);
0602             }
0603             m_metadata[QString::fromLatin1(key)] = ba;
0604           }
0605         }
0606       }
0607       MP4ItmfItemListFree(list);
0608     }
0609 
0610     MP4Chapter_t* chapterList = nullptr;
0611     uint32_t chapterCount = 0;
0612     MP4GetChapters(handle, &chapterList, &chapterCount, MP4ChapterTypeQt);
0613     if (chapterList) {
0614       Frame frame;
0615       mp4ChaptersToFrame(chapterList, chapterCount, frame);
0616       frame.setIndex(Frame::toNegativeIndex(m_extraFrames.size()));
0617       m_extraFrames.append(frame);
0618       MP4Free(chapterList);
0619     }
0620 #elif defined HAVE_MP4V2_MP4GETMETADATABYINDEX_CHARPP_ARG
0621       static char notFreeFormStr[] = "NOFF";
0622       static char freeFormStr[] = "----";
0623       char* ppName;
0624       uint8_t* ppValue = 0;
0625       uint32_t pValueSize = 0;
0626       uint32_t index = 0;
0627       unsigned numEmptyEntries = 0;
0628       for (index = 0; index < 64; ++index) {
0629         ppName = notFreeFormStr;
0630         bool ok = MP4GetMetadataByIndex(handle, index,
0631                                         &ppName, &ppValue, &pValueSize);
0632         if (ok && ppName && memcmp(ppName, "----", 4) == 0) {
0633           // free form tagfield
0634           free(ppName);
0635           free(ppValue);
0636           ppName = freeFormStr;
0637           ppValue = 0;
0638           pValueSize = 0;
0639           ok = MP4GetMetadataByIndex(handle, index,
0640                                      &ppName, &ppValue, &pValueSize);
0641         }
0642         if (ok) {
0643           numEmptyEntries = 0;
0644           if (ppName) {
0645             QString key(ppName);
0646             QByteArray ba;
0647             if (ppValue && pValueSize > 0) {
0648               ba = getValueByteArray(ppName, ppValue, pValueSize);
0649             }
0650             m_metadata[key] = ba;
0651             free(ppName);
0652           }
0653           free(ppValue);
0654           ppName = 0;
0655           ppValue = 0;
0656           pValueSize = 0;
0657         } else {
0658           // There are iTunes files with invalid fields in between,
0659           // so we stop after 3 invalid indices.
0660           if (++numEmptyEntries >= 3) {
0661             break;
0662           }
0663         }
0664       }
0665 #else
0666       const char* ppName = 0;
0667       uint8_t* ppValue = 0;
0668       uint32_t pValueSize = 0;
0669       uint32_t index = 0;
0670       unsigned numEmptyEntries = 0;
0671       for (index = 0; index < 64; ++index) {
0672         if (MP4GetMetadataByIndex(handle, index,
0673                                   &ppName, &ppValue, &pValueSize)) {
0674           numEmptyEntries = 0;
0675           if (ppName) {
0676             QString key(ppName);
0677             QByteArray ba;
0678             if (ppValue && pValueSize > 0) {
0679               ba = getValueByteArray(ppName, ppValue, pValueSize);
0680             }
0681             m_metadata[key] = ba;
0682 
0683             // If the field is free form, there are two memory leaks in mp4v2.
0684             // The first is not accessible, the second can be freed.
0685             if (isFreeFormMetadata(handle, ppName)) {
0686               free(const_cast<char*>(ppName));
0687             }
0688           }
0689           free(ppValue);
0690           ppName = 0;
0691           ppValue = 0;
0692           pValueSize = 0;
0693         } else {
0694           // There are iTunes files with invalid fields in between,
0695           // so we stop after 3 invalid indices.
0696           if (++numEmptyEntries >= 3) {
0697             break;
0698           }
0699         }
0700       }
0701 #endif
0702       MP4Close(handle
0703 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0200
0704                , MP4_CLOSE_DO_NOT_COMPUTE_BITRATE
0705 #endif
0706                );
0707     }
0708   }
0709 
0710   if (force) {
0711     setFilename(currentFilename());
0712   }
0713 
0714   notifyModelDataChanged(priorIsTagInformationRead);
0715 }
0716 
0717 /**
0718  * Write tags to file and rename it if necessary.
0719  *
0720  * @param force   true to force writing even if file was not changed.
0721  * @param renamed will be set to true if the file was renamed,
0722  *                i.e. the file name is no longer valid, else *renamed
0723  *                is left unchanged
0724  * @param preserve true to preserve file time stamps
0725  *
0726  * @return true if ok, false if the file could not be written or renamed.
0727  */
0728 bool M4aFile::writeTags(bool force, bool* renamed, bool preserve)
0729 {
0730   bool ok = true;
0731   QString fnStr(currentFilePath());
0732   if (isChanged() && !QFileInfo(fnStr).isWritable()) {
0733     revertChangedFilename();
0734     return false;
0735   }
0736 
0737   if (m_fileRead && (force || isTagChanged(Frame::Tag_2))) {
0738     QByteArray fn =
0739 #ifdef Q_OS_WIN32
0740         fnStr.toUtf8();
0741 #else
0742         QFile::encodeName(fnStr);
0743 #endif
0744 
0745     // store time stamp if it has to be preserved
0746     quint64 actime = 0, modtime = 0;
0747     if (preserve) {
0748       getFileTimeStamps(fnStr, actime, modtime);
0749     }
0750 
0751     MP4FileHandle handle = MP4Modify(fn);
0752     if (handle != MP4_INVALID_FILE_HANDLE) {
0753 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
0754       MP4ItmfItemList* list = MP4ItmfGetItems(handle);
0755       if (list) {
0756         for (uint32_t i = 0; i < list->size; ++i) {
0757           MP4ItmfRemoveItem(handle, &list->elements[i]);
0758         }
0759         MP4ItmfItemListFree(list);
0760       }
0761       const MP4Tags* tags = MP4TagsAlloc();
0762 #else
0763       // return code is not checked because it will fail if no metadata exists
0764       MP4MetadataDelete(handle);
0765 #endif
0766 
0767       for (auto it = m_metadata.constBegin(); it != m_metadata.constEnd(); ++it) {
0768         const QByteArray& value = *it;
0769         if (!value.isEmpty()) {
0770           const QString& name = it.key();
0771           const QByteArray& str = value;
0772 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
0773           // clazy:excludeall=qlatin1string-non-ascii
0774           if (name == QLatin1String("\251nam")) {
0775             MP4TagsSetName(tags, str);
0776           } else if (name == QLatin1String("\251ART")) {
0777             MP4TagsSetArtist(tags, str);
0778           } else if (name == QLatin1String("\251wrt")) {
0779             MP4TagsSetComposer(tags, str);
0780           } else if (name == QLatin1String("\251cmt")) {
0781             MP4TagsSetComments(tags, str);
0782           } else if (name == QLatin1String("\251too")) {
0783             MP4TagsSetEncodingTool(tags, str);
0784           } else if (name == QLatin1String("\251day")) {
0785             MP4TagsSetReleaseDate(tags, str);
0786           } else if (name == QLatin1String("\251alb")) {
0787             MP4TagsSetAlbum(tags, str);
0788           } else if (name == QLatin1String("trkn")) {
0789             MP4TagTrack indexTotal;
0790             int slashPos = str.indexOf('/');
0791             if (slashPos != -1) {
0792               indexTotal.total = str.mid(slashPos + 1).toUShort();
0793               indexTotal.index = str.mid(0, slashPos).toUShort();
0794             } else {
0795               indexTotal.total = 0;
0796               indexTotal.index = str.toUShort();
0797             }
0798             MP4TagsSetTrack(tags, &indexTotal);
0799           } else if (name == QLatin1String("disk")) {
0800             MP4TagDisk indexTotal;
0801             int slashPos = str.indexOf('/');
0802             if (slashPos != -1) {
0803               indexTotal.total = str.mid(slashPos + 1).toUShort();
0804               indexTotal.index = str.mid(0, slashPos).toUShort();
0805             } else {
0806               indexTotal.total = 0;
0807               indexTotal.index = str.toUShort();
0808             }
0809             MP4TagsSetDisk(tags, &indexTotal);
0810           } else if (name == QLatin1String("\251gen") || name == QLatin1String("gnre")) {
0811             MP4TagsSetGenre(tags, str);
0812           } else if (name == QLatin1String("tmpo")) {
0813             uint16_t tempo = str.toUShort();
0814             MP4TagsSetTempo(tags, &tempo);
0815           } else if (name == QLatin1String("cpil")) {
0816             uint8_t cpl = str.toUShort();
0817             MP4TagsSetCompilation(tags, &cpl);
0818           } else if (name == QLatin1String("\251grp")) {
0819             MP4TagsSetGrouping(tags, str);
0820           } else if (name == QLatin1String("aART")) {
0821             MP4TagsSetAlbumArtist(tags, str);
0822           } else if (name == QLatin1String("pgap")) {
0823             uint8_t pgap = str.toUShort();
0824             MP4TagsSetGapless(tags, &pgap);
0825           } else if (name == QLatin1String("tvsh")) {
0826             MP4TagsSetTVShow(tags, str);
0827           } else if (name == QLatin1String("tvnn")) {
0828             MP4TagsSetTVNetwork(tags, str);
0829           } else if (name == QLatin1String("tven")) {
0830             MP4TagsSetTVEpisodeID(tags, str);
0831           } else if (name == QLatin1String("tvsn")) {
0832             uint32_t val = str.toULong();
0833             MP4TagsSetTVSeason(tags, &val);
0834           } else if (name == QLatin1String("tves")) {
0835             uint32_t val = str.toULong();
0836             MP4TagsSetTVEpisode(tags, &val);
0837           } else if (name == QLatin1String("desc")) {
0838             MP4TagsSetDescription(tags, str);
0839           } else if (name == QLatin1String("ldes")) {
0840             MP4TagsSetLongDescription(tags, str);
0841           } else if (name == QLatin1String("\251lyr")) {
0842             MP4TagsSetLyrics(tags, str);
0843           } else if (name == QLatin1String("sonm")) {
0844             MP4TagsSetSortName(tags, str);
0845           } else if (name == QLatin1String("soar")) {
0846             MP4TagsSetSortArtist(tags, str);
0847           } else if (name == QLatin1String("soaa")) {
0848             MP4TagsSetSortAlbumArtist(tags, str);
0849           } else if (name == QLatin1String("soal")) {
0850             MP4TagsSetSortAlbum(tags, str);
0851           } else if (name == QLatin1String("soco")) {
0852             MP4TagsSetSortComposer(tags, str);
0853           } else if (name == QLatin1String("sosn")) {
0854             MP4TagsSetSortTVShow(tags, str);
0855           } else if (name == QLatin1String("cprt")) {
0856             MP4TagsSetCopyright(tags, str);
0857           } else if (name == QLatin1String("\251enc")) {
0858             MP4TagsSetEncodedBy(tags, str);
0859           } else if (name == QLatin1String("purd")) {
0860             MP4TagsSetPurchaseDate(tags, str);
0861           } else if (name == QLatin1String("pcst")) {
0862             uint8_t val = str.toUShort();
0863             MP4TagsSetPodcast(tags, &val);
0864           } else if (name == QLatin1String("keyw")) {
0865             MP4TagsSetKeywords(tags, str);
0866           } else if (name == QLatin1String("catg")) {
0867             MP4TagsSetCategory(tags, str);
0868           } else if (name == QLatin1String("hdvd")) {
0869             uint8_t val = str.toUShort();
0870             MP4TagsSetHDVideo(tags, &val);
0871           } else if (name == QLatin1String("stik")) {
0872             uint8_t val = str.toUShort();
0873             MP4TagsSetMediaType(tags, &val);
0874           } else if (name == QLatin1String("rtng")) {
0875             uint8_t val = str.toUShort();
0876             MP4TagsSetContentRating(tags, &val);
0877           } else if (name == QLatin1String("apID")) {
0878             MP4TagsSetITunesAccount(tags, str);
0879           } else if (name == QLatin1String("akID")) {
0880             uint8_t val = str.toUShort();
0881             MP4TagsSetITunesAccountType(tags, &val);
0882           } else if (name == QLatin1String("sfID")) {
0883             uint32_t val = str.toULong();
0884             MP4TagsSetITunesCountry(tags, &val);
0885           } else if (name == QLatin1String("cnID")) {
0886             uint32_t val = str.toULong();
0887             MP4TagsSetContentID(tags, &val);
0888           } else if (name == QLatin1String("atID")) {
0889             uint32_t val = str.toULong();
0890             MP4TagsSetArtistID(tags, &val);
0891           } else if (name == QLatin1String("plID")) {
0892             uint64_t val = str.toULongLong();
0893             MP4TagsSetPlaylistID(tags, &val);
0894           } else if (name == QLatin1String("geID")) {
0895             uint32_t val = str.toULong();
0896             MP4TagsSetGenreID(tags, &val);
0897           } else if (name == QLatin1String("cmID")) {
0898             uint32_t val = str.toULong();
0899             MP4TagsSetComposerID(tags, &val);
0900           } else if (name == QLatin1String("xid ")) {
0901             MP4TagsSetXID(tags, str);
0902           } else {
0903             MP4ItmfItem* item;
0904             if (name.length() == 4 &&
0905                 (name.at(0).toLatin1() == '\251' ||
0906                  (name.at(0) >= QLatin1Char('a') &&
0907                   name.at(0) <= QLatin1Char('z')))) {
0908               item = MP4ItmfItemAlloc(name.toLatin1().constData(), 1);
0909             } else {
0910               item = MP4ItmfItemAlloc("----", 1);
0911               item->mean = strdup("com.apple.iTunes");
0912               item->name = strdup(name.toUtf8().data());
0913             }
0914 
0915             MP4ItmfData& data = item->dataList.elements[0];
0916             data.typeCode = MP4_ITMF_BT_UTF8;
0917             data.valueSize = value.size();
0918             data.value = reinterpret_cast<uint8_t*>(malloc(data.valueSize));
0919             memcpy(data.value, value.data(), data.valueSize);
0920 
0921             MP4ItmfAddItem(handle, item);
0922             MP4ItmfItemFree(item);
0923           }
0924 #else
0925           bool setOk;
0926           if (name == QLatin1String("\251nam")) {
0927             setOk = MP4SetMetadataName(handle, str);
0928           } else if (name == QLatin1String("\251ART")) {
0929             setOk = MP4SetMetadataArtist(handle, str);
0930           } else if (name == QLatin1String("\251wrt")) {
0931             setOk = MP4SetMetadataWriter(handle, str);
0932           } else if (name == QLatin1String("\251cmt")) {
0933             setOk = MP4SetMetadataComment(handle, str);
0934           } else if (name == QLatin1String("\251too")) {
0935             setOk = MP4SetMetadataTool(handle, str);
0936           } else if (name == QLatin1String("\251day")) {
0937             unsigned short year = str.toUShort();
0938             if (year > 0) {
0939               if (year < 1000) year += 2000;
0940               else if (year > 9999) year = 9999;
0941               setOk = MP4SetMetadataYear(handle, QByteArray().setNum(year));
0942               if (setOk) {
0943                 if (year >= 0) {
0944                   setTextField(QLatin1String("\251day"),
0945                                year != 0 ? QString::number(year)
0946                                          : QLatin1String(""), Frame::FT_Date);
0947                 }
0948               }
0949             } else {
0950               setOk = true;
0951             }
0952           } else if (name == QLatin1String("\251alb")) {
0953             setOk = MP4SetMetadataAlbum(handle, str);
0954           } else if (name == QLatin1String("trkn")) {
0955             uint16_t track = 0, totalTracks = 0;
0956             int slashPos = str.indexOf('/');
0957             if (slashPos != -1) {
0958               totalTracks = str.mid(slashPos + 1).toUShort();
0959               track = str.mid(0, slashPos).toUShort();
0960             } else {
0961               track = str.toUShort();
0962             }
0963             setOk = MP4SetMetadataTrack(handle, track, totalTracks);
0964           } else if (name == QLatin1String("disk")) {
0965             uint16_t disk = 0, totalDisks = 0;
0966             int slashPos = str.indexOf('/');
0967             if (slashPos != -1) {
0968               totalDisks = str.mid(slashPos + 1).toUShort();
0969               disk = str.mid(0, slashPos).toUShort();
0970             } else {
0971               disk = str.toUShort();
0972             }
0973             setOk = MP4SetMetadataDisk(handle, disk, totalDisks);
0974           } else if (name == QLatin1String("\251gen") || name == QLatin1String("gnre")) {
0975             setOk = MP4SetMetadataGenre(handle, str);
0976           } else if (name == QLatin1String("tmpo")) {
0977             uint16_t tempo = str.toUShort();
0978             setOk = MP4SetMetadataTempo(handle, tempo);
0979           } else if (name == QLatin1String("cpil")) {
0980             uint8_t cpl = str.toUShort();
0981             setOk = MP4SetMetadataCompilation(handle, cpl);
0982 // While this works on Debian Etch with libmp4v2-dev 1.5.0.1-0.3 from
0983 // www.debian-multimedia.org, linking on OpenSUSE 10.3 with
0984 // libmp4v2-devel-1.5.0.1-6 from packman.links2linux.de fails with
0985 // undefined reference to MP4SetMetadataGrouping. To avoid this,
0986 // in the line below, 0x105 is replaced by 0x106.
0987 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106
0988           } else if (name == QLatin1String("\251grp")) {
0989             setOk = MP4SetMetadataGrouping(handle, str);
0990 #endif
0991 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106
0992           } else if (name == QLatin1String("aART")) {
0993             setOk = MP4SetMetadataAlbumArtist(handle, str);
0994           } else if (name == QLatin1String("pgap")) {
0995             uint8_t pgap = str.toUShort();
0996             setOk = MP4SetMetadataPartOfGaplessAlbum(handle, pgap);
0997 #endif
0998           } else {
0999             setOk = MP4SetMetadataFreeForm(
1000               handle, const_cast<char*>(name.toUtf8().data()),
1001               reinterpret_cast<uint8_t*>(const_cast<char*>(value.data())),
1002               value.size());
1003           }
1004           if (!setOk) {
1005             qDebug("MP4SetMetadata %s failed", name.toLatin1().data());
1006             ok = false;
1007           }
1008 #endif
1009         }
1010       }
1011 
1012       bool hasChapters = false;
1013       const auto frames = m_extraFrames;
1014       for (const Frame& frame : frames) {
1015         if (frame.getType() == Frame::FT_Other &&
1016             frame.getName() == QLatin1String("Chapters")) {
1017           uint32_t chapterCount = 0;
1018           MP4Chapter_t* chapterList = nullptr;
1019           frameToMp4Chapters(frame, chapterList, chapterCount);
1020           MP4SetChapters(handle, chapterList, chapterCount, MP4ChapterTypeQt);
1021           hasChapters = true;
1022           delete [] chapterList;
1023         } else {
1024           QByteArray ba;
1025           if (PictureFrame::getData(frame, ba)) {
1026 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
1027             MP4TagArtwork artwork;
1028             artwork.data = ba.data();
1029             artwork.size = static_cast<uint32_t>(ba.size());
1030             artwork.type = MP4_ART_JPEG;
1031             QString mimeType;
1032             if (PictureFrame::getMimeType(frame, mimeType)) {
1033               if (mimeType == QLatin1String("image/png")) {
1034                 artwork.type = MP4_ART_PNG;
1035               } else if (mimeType == QLatin1String("image/bmp")) {
1036                 artwork.type = MP4_ART_BMP;
1037               } else if (mimeType == QLatin1String("image/gif")) {
1038                 artwork.type = MP4_ART_GIF;
1039               }
1040             }
1041             MP4TagsAddArtwork(tags, &artwork);
1042 #else
1043             MP4SetMetadataCoverArt(handle, reinterpret_cast<uint8_t*>(ba.data()),
1044                                    ba.size());
1045 #endif
1046           }
1047         }
1048       }
1049       if (!hasChapters) {
1050         MP4DeleteChapters(handle);
1051       }
1052 
1053 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
1054       MP4TagsStore(tags, handle);
1055       MP4TagsFree(tags);
1056 #endif
1057 
1058       MP4Close(handle
1059 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0200
1060                , MP4_CLOSE_DO_NOT_COMPUTE_BITRATE
1061 #endif
1062                );
1063       if (ok) {
1064         // without this, old tags stay in the file marked as free
1065         MP4Optimize(fn);
1066         markTagUnchanged(Frame::Tag_2);
1067       }
1068 
1069       // restore time stamp
1070       if (actime || modtime) {
1071         setFileTimeStamps(fnStr, actime, modtime);
1072       }
1073     } else {
1074       qDebug("MP4Modify failed");
1075       ok = false;
1076     }
1077   }
1078 
1079   if (isFilenameChanged()) {
1080     if (!renameFile()) {
1081       return false;
1082     }
1083     markFilenameUnchanged();
1084     // link tags to new file name
1085     readTags(true);
1086     *renamed = true;
1087   }
1088   return ok;
1089 }
1090 
1091 /**
1092  * Free resources allocated when calling readTags().
1093  *
1094  * @param force true to force clearing even if the tags are modified
1095  */
1096 void M4aFile::clearTags(bool force)
1097 {
1098   if (!m_fileRead || (isChanged() && !force))
1099     return;
1100 
1101   bool priorIsTagInformationRead = isTagInformationRead();
1102   m_metadata.clear();
1103   m_extraFrames.clear();
1104   markTagUnchanged(Frame::Tag_2);
1105   m_fileRead = false;
1106   notifyModelDataChanged(priorIsTagInformationRead);
1107 }
1108 
1109 /**
1110  * Remove frames.
1111  *
1112  * @param tagNr tag number
1113  * @param flt filter specifying which frames to remove
1114  */
1115 void M4aFile::deleteFrames(Frame::TagNumber tagNr, const FrameFilter& flt)
1116 {
1117   if (tagNr != Frame::Tag_2)
1118     return;
1119 
1120   if (flt.areAllEnabled()) {
1121     m_metadata.clear();
1122     m_extraFrames.clear();
1123     markTagChanged(Frame::Tag_2, Frame::ExtendedType());
1124   } else {
1125     bool changed = false;
1126     for (auto it = m_metadata.begin(); it != m_metadata.end();) { // clazy:exclude=detaching-member
1127       QString name(it.key());
1128       Frame::Type type = getTypeForName(name);
1129       if (flt.isEnabled(type, name)) {
1130         it = m_metadata.erase(it);
1131         changed = true;
1132       } else {
1133         ++it;
1134       }
1135     }
1136     bool pictureEnabled = flt.isEnabled(Frame::FT_Picture);
1137     bool chaptersEnabled = flt.isEnabled(Frame::FT_Other,
1138                                          QLatin1String("Chapters"));
1139     if ((pictureEnabled || chaptersEnabled) && !m_extraFrames.isEmpty()) {
1140       for (auto it = m_extraFrames.begin(); it != m_extraFrames.end();) {
1141         Frame::Type type = it->getType();
1142         if ((pictureEnabled && type == Frame::FT_Picture) ||
1143             (chaptersEnabled && type == Frame::FT_Other &&
1144              it->getName() == QLatin1String("Chapters"))) {
1145           it = m_extraFrames.erase(it);
1146           changed = true;
1147         } else {
1148           ++it;
1149         }
1150       }
1151     }
1152     if (changed) {
1153       markTagChanged(Frame::Tag_2, Frame::ExtendedType());
1154     }
1155   }
1156 }
1157 
1158 /**
1159  * Get metadata field as string.
1160  *
1161  * @param name field name
1162  *
1163  * @return value as string, "" if not found,
1164  *         QString::null if the tags have not been read yet.
1165  */
1166 QString M4aFile::getTextField(const QString& name) const
1167 {
1168   if (m_fileRead) {
1169     auto it = m_metadata.constFind(name);
1170     if (it != m_metadata.constEnd()) {
1171       return QString::fromUtf8((*it).data(), (*it).size());
1172     }
1173     return QLatin1String("");
1174   }
1175   return QString();
1176 }
1177 
1178 /**
1179  * Set text field.
1180  * If value is null if the tags have not been read yet, nothing is changed.
1181  * If value is different from the current value, tag 2 is marked as changed.
1182  *
1183  * @param name name
1184  * @param value value, "" to remove, QString::null to do nothing
1185  * @param type frame type
1186  */
1187 void M4aFile::setTextField(const QString& name, const QString& value,
1188                            const Frame::ExtendedType& type)
1189 {
1190   if (m_fileRead && !value.isNull()) {
1191     QByteArray str = value.toUtf8();
1192     auto it = m_metadata.find(name); // clazy:exclude=detaching-member
1193     if (it != m_metadata.end()) {
1194       if (QString::fromUtf8((*it).data(), (*it).size()) != value) {
1195         *it = str;
1196         markTagChanged(Frame::Tag_2, type);
1197       }
1198     } else {
1199       m_metadata.insert(name, str);
1200       markTagChanged(Frame::Tag_2, type);
1201     }
1202   }
1203 }
1204 
1205 /**
1206  * Check if tag information has already been read.
1207  *
1208  * @return true if information is available,
1209  *         false if the tags have not been read yet, in which case
1210  *         hasTag() does not return meaningful information.
1211  */
1212 bool M4aFile::isTagInformationRead() const
1213 {
1214   return m_fileRead;
1215 }
1216 
1217 /**
1218  * Check if file has a tag.
1219  *
1220  * @param tagNr tag number
1221  * @return true if a V2 tag is available.
1222  * @see isTagInformationRead()
1223  */
1224 bool M4aFile::hasTag(Frame::TagNumber tagNr) const
1225 {
1226   return tagNr == Frame::Tag_2 && !m_metadata.empty();
1227 }
1228 
1229 /**
1230  * Get file extension including the dot.
1231  *
1232  * @return file extension ".m4a".
1233  */
1234 QString M4aFile::getFileExtension() const
1235 {
1236   return QLatin1String(".m4a");
1237 }
1238 
1239 /**
1240  * Get technical detail information.
1241  *
1242  * @param info the detail information is returned here
1243  */
1244 void M4aFile::getDetailInfo(DetailInfo& info) const
1245 {
1246   if (m_fileRead && m_fileInfo.valid) {
1247     info.valid = true;
1248     info.format = QLatin1String("MP4");
1249     info.bitrate = m_fileInfo.bitrate;
1250     info.sampleRate = m_fileInfo.sampleRate;
1251     info.channels = m_fileInfo.channels;
1252     info.duration = m_fileInfo.duration;
1253   } else {
1254     info.valid = false;
1255   }
1256 }
1257 
1258 /**
1259  * Get duration of file.
1260  *
1261  * @return duration in seconds,
1262  *         0 if unknown.
1263  */
1264 unsigned M4aFile::getDuration() const
1265 {
1266   if (m_fileRead && m_fileInfo.valid) {
1267     return m_fileInfo.duration;
1268   }
1269   return 0;
1270 }
1271 
1272 /**
1273  * Get the format of tag.
1274  *
1275  * @param tagNr tag number
1276  * @return "Vorbis".
1277  */
1278 QString M4aFile::getTagFormat(Frame::TagNumber tagNr) const
1279 {
1280   return hasTag(tagNr) ? QLatin1String("MP4") : QString();
1281 }
1282 
1283 /**
1284  * Get a specific frame from the tags.
1285  *
1286  * @param tagNr tag number
1287  * @param type  frame type
1288  * @param frame the frame is returned here
1289  *
1290  * @return true if ok.
1291  */
1292 bool M4aFile::getFrame(Frame::TagNumber tagNr, Frame::Type type, Frame& frame) const
1293 {
1294   if (type < Frame::FT_FirstFrame || type > Frame::FT_LastV1Frame ||
1295       tagNr > 1)
1296     return false;
1297 
1298   if (tagNr == Frame::Tag_1) {
1299     frame.setValue(QString());
1300   } else {
1301     if (type == Frame::FT_Genre) {
1302       QString str(getTextField(QLatin1String("\251gen")));
1303       frame.setValue(str.isEmpty() ? getTextField(QLatin1String("gnre")) : str);
1304     } else {
1305       frame.setValue(getTextField(getNameForType(type)));
1306     }
1307   }
1308   frame.setType(type);
1309   return true;
1310 }
1311 
1312 /**
1313  * Set a frame in the tags.
1314  *
1315  * @param tagNr tag number
1316  * @param frame frame to set
1317  *
1318  * @return true if ok.
1319  */
1320 bool M4aFile::setFrame(Frame::TagNumber tagNr, const Frame& frame)
1321 {
1322   if (tagNr == Frame::Tag_2) {
1323     if ((frame.getType() == Frame::FT_Picture) ||
1324         (frame.getType() == Frame::FT_Other &&
1325                 frame.getName() == QLatin1String("Chapters"))) {
1326       int idx = Frame::fromNegativeIndex(frame.getIndex());
1327       if (idx >= 0 && idx < m_extraFrames.size()) {
1328         Frame newFrame(frame);
1329         if ((frame.getType() == Frame::FT_Picture &&
1330              PictureFrame::areFieldsEqual(m_extraFrames[idx], newFrame)) ||
1331             (frame.getType() == Frame::FT_Other &&
1332              frame.getName() == QLatin1String("Chapters") &&
1333              areMp4ChaptersFieldsEqual(m_extraFrames[idx], newFrame))) {
1334           m_extraFrames[idx].setValueChanged(false);
1335         } else {
1336           m_extraFrames[idx] = newFrame;
1337           markTagChanged(tagNr, frame.getExtendedType());
1338         }
1339         return true;
1340       } else {
1341         return false;
1342       }
1343     }
1344     QString name = fixUpTagKey(frame.getInternalName(), TT_Mp4);
1345     auto it = m_metadata.find(name); // clazy:exclude=detaching-member
1346     if (it != m_metadata.end()) {
1347       if (frame.getType() != Frame::FT_Picture) {
1348         QByteArray str = frame.getValue().toUtf8();
1349         if (*it != str) {
1350           *it = str;
1351           markTagChanged(Frame::Tag_2, frame.getExtendedType());
1352         }
1353       } else {
1354         if (PictureFrame::getData(frame, *it)) {
1355           markTagChanged(Frame::Tag_2,
1356                          Frame::ExtendedType(Frame::FT_Picture, name));
1357         }
1358       }
1359       return true;
1360     }
1361   }
1362 
1363   // Try the basic method
1364   Frame::Type type = frame.getType();
1365   if (type < Frame::FT_FirstFrame || type > Frame::FT_LastV1Frame ||
1366       tagNr > 1)
1367     return false;
1368 
1369   if (tagNr == Frame::Tag_2) {
1370     if (type == Frame::FT_Genre) {
1371       QString str = frame.getValue();
1372       QString oldStr(getTextField(QLatin1String("\251gen")));
1373       if (oldStr.isEmpty()) {
1374         oldStr = getTextField(QLatin1String("gnre"));
1375       }
1376       if (str != oldStr) {
1377         int genreNum = Genres::getNumber(str);
1378         if (genreNum != 255) {
1379           const QString genreName(QLatin1String("gnre"));
1380           setTextField(genreName, str,
1381                        Frame::ExtendedType(Frame::FT_Genre, genreName));
1382           m_metadata.remove(QLatin1String("\251gen"));
1383         } else {
1384           const QString genreName(QLatin1String("\251gen"));
1385           setTextField(genreName, str,
1386                        Frame::ExtendedType(Frame::FT_Genre, genreName));
1387           m_metadata.remove(QLatin1String("gnre"));
1388         }
1389       }
1390     } else if (type == Frame::FT_Track) {
1391       int numTracks;
1392       int num = splitNumberAndTotal(frame.getValue(), &numTracks);
1393       if (num >= 0) {
1394         QString str;
1395         if (num != 0) {
1396           str.setNum(num);
1397           if (numTracks == 0)
1398             numTracks = getTotalNumberOfTracksIfEnabled();
1399           if (numTracks > 0) {
1400             str += QLatin1Char('/');
1401             str += QString::number(numTracks);
1402           }
1403         } else {
1404           str = QLatin1String("");
1405         }
1406         const QString trackName(QLatin1String("trkn"));
1407         setTextField(trackName, str,
1408                      Frame::ExtendedType(Frame::FT_Track, trackName));
1409       }
1410     } else {
1411       const QString fieldName = getNameForType(type);
1412       setTextField(fieldName, frame.getValue(),
1413                    Frame::ExtendedType(type, fieldName));
1414     }
1415   }
1416   return true;
1417 }
1418 
1419 /**
1420  * Add a frame in the tags.
1421  *
1422  * @param tagNr tag number
1423  * @param frame frame to add
1424  *
1425  * @return true if ok.
1426  */
1427 bool M4aFile::addFrame(Frame::TagNumber tagNr, Frame& frame)
1428 {
1429   if (tagNr == Frame::Tag_2) {
1430     Frame::ExtendedType extendedType = frame.getExtendedType();
1431     Frame::Type type = extendedType.getType();
1432     if (type == Frame::FT_Picture) {
1433       if (frame.getFieldList().empty()) {
1434         PictureFrame::setFields(frame);
1435       }
1436       frame.setIndex(Frame::toNegativeIndex(m_extraFrames.size()));
1437       m_extraFrames.append(frame);
1438       markTagChanged(tagNr, extendedType);
1439       return true;
1440     }
1441     if (type == Frame::FT_Other &&
1442         frame.getName() == QLatin1String("Chapters")) {
1443       if (frame.getFieldList().empty()) {
1444         setMp4ChaptersFields(frame);
1445       }
1446       frame.setIndex(Frame::toNegativeIndex(m_extraFrames.size()));
1447       m_extraFrames.append(frame);
1448       markTagChanged(Frame::Tag_2, extendedType);
1449       return true;;
1450     }
1451     QString name;
1452     if (type != Frame::FT_Other) {
1453       name = getNameForType(type);
1454       if (!name.isEmpty()) {
1455         extendedType = Frame::ExtendedType(type, name);
1456         frame.setExtendedType(extendedType);
1457       }
1458     }
1459     name = fixUpTagKey(frame.getInternalName(), TT_Mp4);
1460     m_metadata[name] = frame.getValue().toUtf8();
1461     markTagChanged(Frame::Tag_2, extendedType);
1462     return true;
1463   }
1464   return false;
1465 }
1466 
1467 /**
1468  * Delete a frame in the tags.
1469  *
1470  * @param tagNr tag number
1471  * @param frame frame to delete.
1472  *
1473  * @return true if ok.
1474  */
1475 bool M4aFile::deleteFrame(Frame::TagNumber tagNr, const Frame& frame)
1476 {
1477   if (tagNr == Frame::Tag_2) {
1478     if ((frame.getType() == Frame::FT_Picture) ||
1479         (frame.getType() == Frame::FT_Other &&
1480                 frame.getName() == QLatin1String("Chapters"))) {
1481       int idx = Frame::fromNegativeIndex(frame.getIndex());
1482       if (idx >= 0 && idx < m_extraFrames.size()) {
1483         m_extraFrames.removeAt(idx);
1484         while (idx < m_extraFrames.size()) {
1485           m_extraFrames[idx].setIndex(Frame::toNegativeIndex(idx));
1486           ++idx;
1487         }
1488         markTagChanged(tagNr, frame.getExtendedType());
1489         return true;
1490       }
1491     }
1492     QString name(frame.getInternalName());
1493     auto it = m_metadata.find(name); // clazy:exclude=detaching-member
1494     if (it != m_metadata.end()) {
1495       m_metadata.erase(it);
1496       markTagChanged(Frame::Tag_2, frame.getExtendedType());
1497       return true;
1498     }
1499   }
1500 
1501   // Try the superclass method
1502   return TaggedFile::deleteFrame(tagNr, frame);
1503 }
1504 
1505 /**
1506  * Get all frames in tag.
1507  *
1508  * @param tagNr tag number
1509  * @param frames frame collection to set.
1510  */
1511 void M4aFile::getAllFrames(Frame::TagNumber tagNr, FrameCollection& frames)
1512 {
1513   if (tagNr == Frame::Tag_2) {
1514     frames.clear();
1515     QString name;
1516     QString value;
1517     int i = 0;
1518     for (auto it = m_metadata.constBegin(); it != m_metadata.constEnd(); ++it) {
1519       name = it.key();
1520       Frame::Type type = getTypeForName(name);
1521       value = QString::fromUtf8((*it).data(), (*it).size());
1522       frames.insert(Frame(type, value, name, i++));
1523     }
1524     for (auto it = m_extraFrames.constBegin(); it != m_extraFrames.constEnd(); ++it) {
1525       frames.insert(*it);
1526     }
1527     frames.addMissingStandardFrames();
1528     return;
1529   }
1530 
1531   TaggedFile::getAllFrames(tagNr, frames);
1532 }
1533 
1534 /**
1535  * Get a list of frame IDs which can be added.
1536  * @param tagNr tag number
1537  * @return list with frame IDs.
1538  */
1539 QStringList M4aFile::getFrameIds(Frame::TagNumber tagNr) const
1540 {
1541   if (tagNr != Frame::Tag_2)
1542     return QStringList();
1543 
1544   static const Frame::Type types[] = {
1545     Frame::FT_Title,
1546     Frame::FT_Artist,
1547     Frame::FT_Album,
1548     Frame::FT_Comment,
1549     Frame::FT_Compilation,
1550     Frame::FT_Date,
1551     Frame::FT_Track,
1552     Frame::FT_Genre,
1553 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106
1554     Frame::FT_AlbumArtist,
1555 #endif
1556     Frame::FT_Bpm,
1557     Frame::FT_Composer,
1558 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
1559     Frame::FT_Copyright,
1560 #endif
1561     Frame::FT_Description,
1562     Frame::FT_Disc,
1563     Frame::FT_EncodedBy,
1564 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
1565     Frame::FT_EncoderSettings,
1566 #endif
1567 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0105
1568     Frame::FT_Grouping,
1569 #endif
1570 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
1571     Frame::FT_Lyrics,
1572 #endif
1573     Frame::FT_Picture,
1574     Frame::FT_Rating
1575 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
1576     , Frame::FT_SortAlbum,
1577     Frame::FT_SortAlbumArtist,
1578     Frame::FT_SortArtist,
1579     Frame::FT_SortComposer,
1580     Frame::FT_SortName
1581 #endif
1582   };
1583 
1584   QStringList lst;
1585   for (auto type : types) {
1586     lst.append(Frame::ExtendedType(type, QLatin1String("")). // clazy:exclude=reserve-candidates
1587                getName());
1588   }
1589 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106
1590   lst << QLatin1String("pgap");
1591 #endif
1592 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
1593   lst << QLatin1String("akID") << QLatin1String("apID") << QLatin1String("atID") << QLatin1String("catg") << QLatin1String("cnID") <<
1594     QLatin1String("geID") << QLatin1String("hdvd") << QLatin1String("keyw") << QLatin1String("ldes") << QLatin1String("pcst") <<
1595     QLatin1String("plID") << QLatin1String("purd") << QLatin1String("rtng") << QLatin1String("sfID") <<
1596     QLatin1String("sosn") << QLatin1String("stik") << QLatin1String("tven") <<
1597     QLatin1String("tves") << QLatin1String("tvnn") << QLatin1String("tvsh") << QLatin1String("tvsn") <<
1598     QLatin1String("purl") << QLatin1String("egid") << QLatin1String("cmID") << QLatin1String("xid ");
1599 #endif
1600   lst << QLatin1String("Chapters");
1601   return lst;
1602 }
1603 
1604 
1605 /**
1606  * Read information about an MPEG-4 file.
1607  * @param fn file name
1608  * @return true if ok.
1609  */
1610 bool M4aFile::FileInfo::read(MP4FileHandle handle)
1611 {
1612   valid = false;
1613   uint32_t numTracks = MP4GetNumberOfTracks(handle);
1614   for (uint32_t i = 0; i < numTracks; ++i) {
1615     MP4TrackId trackId = MP4FindTrackId(handle, i);
1616     const char* trackType = MP4GetTrackType(handle, trackId);
1617     if (std::strcmp(trackType, MP4_AUDIO_TRACK_TYPE) == 0) {
1618       valid = true;
1619       bitrate = (MP4GetTrackBitRate(handle, trackId) + 500) / 1000;
1620       sampleRate = MP4GetTrackTimeScale(handle, trackId);
1621       duration = MP4ConvertFromTrackDuration(
1622         handle, trackId,
1623         MP4GetTrackDuration(handle, trackId), MP4_MSECS_TIME_SCALE) / 1000;
1624 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
1625       channels = MP4GetTrackAudioChannels(handle, trackId);
1626 #else
1627       channels = 2;
1628 #endif
1629       break;
1630     }
1631   }
1632   return valid;
1633 }