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

0001 /**
0002  * \file flacfile.cpp
0003  * Handling of FLAC files.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 04 Oct 2005
0008  *
0009  * Copyright (C) 2005-2024  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 "flacfile.hpp"
0028 
0029 #include "genres.h"
0030 #include "pictureframe.h"
0031 #include <FLAC++/metadata.h>
0032 #include <QFile>
0033 #include <QDir>
0034 #include <cstdio>
0035 #include <cmath>
0036 #include <QByteArray>
0037 
0038 namespace {
0039 
0040 #ifdef HAVE_FLAC_PICTURE
0041 /**
0042  * Get the picture block as a picture frame.
0043  *
0044  * @param frame frame to set
0045  * @param pic   picture block to get
0046  */
0047 void getPicture(Frame& frame, const FLAC::Metadata::Picture* pic)
0048 {
0049   QByteArray ba(reinterpret_cast<const char*>(pic->get_data()),
0050     pic->get_data_length());
0051   PictureFrame::ImageProperties imgProps(
0052         pic->get_width(), pic->get_height(), pic->get_depth(),
0053         pic->get_colors(), ba);
0054   PictureFrame::setFields(
0055     frame,
0056     Frame::TE_ISO8859_1, QLatin1String(""),
0057     QString::fromLatin1(pic->get_mime_type()),
0058     static_cast<PictureFrame::PictureType>(pic->get_type()),
0059     QString::fromUtf8(
0060       reinterpret_cast<const char*>(pic->get_description())),
0061     ba, &imgProps);
0062   frame.setExtendedType(Frame::ExtendedType(Frame::FT_Picture, QLatin1String("Picture")));
0063 }
0064 
0065 /**
0066  * Set the picture block with the picture frame.
0067  *
0068  * @param frame frame to get
0069  * @param pic picture block to set
0070  *
0071  * @return true if ok.
0072  */
0073 bool setPicture(const Frame& frame, FLAC::Metadata::Picture* pic)
0074 {
0075   Frame::TextEncoding enc;
0076   PictureFrame::PictureType pictureType = PictureFrame::PT_CoverFront;
0077   QString imgFormat, mimeType, description;
0078   QByteArray ba;
0079   PictureFrame::ImageProperties imgProps;
0080   PictureFrame::getFields(frame, enc, imgFormat, mimeType,
0081                           pictureType, description, ba, &imgProps);
0082   if (!imgProps.isValidForImage(ba)) {
0083     imgProps = PictureFrame::ImageProperties(ba);
0084   }
0085   pic->set_width(imgProps.width());
0086   pic->set_height(imgProps.height());
0087   pic->set_depth(imgProps.depth());
0088   pic->set_colors(imgProps.numColors());
0089   pic->set_mime_type(mimeType.toLatin1());
0090   pic->set_type(
0091     static_cast<FLAC__StreamMetadata_Picture_Type>(pictureType));
0092   pic->set_description(
0093     reinterpret_cast<const FLAC__byte*>(
0094       static_cast<const char*>(description.toUtf8())));
0095   const auto data = reinterpret_cast<const FLAC__byte*>(ba.data());
0096   int dataLength = ba.size();
0097   if (!data || dataLength <= 0) {
0098     // Avoid assertion crash in FLAC__metadata_object_picture_set_data().
0099     qWarning("FLAC picture data empty");
0100     return false;
0101   }
0102   pic->set_data(data, dataLength);
0103   if (pic->get_length() >= (1u << FLAC__STREAM_METADATA_LENGTH_LEN)) {
0104     // Avoid assertion crash in FLAC write_metadata_block_header_cb_().
0105     qWarning("FLAC picture is too large");
0106     return false;
0107   }
0108   return true;
0109 }
0110 #endif // HAVE_FLAC_PICTURE
0111 
0112 }
0113 
0114 /**
0115  * Constructor.
0116  *
0117  * @param idx index in file proxy model
0118  */
0119 FlacFile::FlacFile(const QPersistentModelIndex& idx) : OggFile(idx)
0120 {
0121 }
0122 
0123 /**
0124  * Destructor.
0125  */
0126 FlacFile::~FlacFile()
0127 {
0128   // Must not be inline because of forwared declared QScopedPointer.
0129 }
0130 
0131 /**
0132  * Get key of tagged file format.
0133  * @return "FlacMetadata".
0134  */
0135 QString FlacFile::taggedFileKey() const
0136 {
0137   return QLatin1String("FlacMetadata");
0138 }
0139 
0140 /**
0141  * Read tags from file.
0142  *
0143  * @param force true to force reading even if tags were already read.
0144  */
0145 void FlacFile::readTags(bool force)
0146 {
0147   bool priorIsTagInformationRead = isTagInformationRead();
0148   if (force || !m_fileRead) {
0149     m_comments.clear();
0150     markTagUnchanged(Frame::Tag_2);
0151     m_fileRead = true;
0152     QByteArray fnIn = QFile::encodeName(currentFilePath());
0153     readFileInfo(m_fileInfo, nullptr); // just to start invalid
0154     if (!m_chain) {
0155       m_chain.reset(new FLAC::Metadata::Chain);
0156     }
0157     if (m_chain && m_chain->is_valid()) {
0158       if (m_chain->read(fnIn)) {
0159 #ifdef HAVE_FLAC_PICTURE
0160         m_pictures.clear();
0161         int pictureNr = 0;
0162 #endif
0163         FLAC::Metadata::Iterator mdit;
0164         mdit.init(*m_chain);
0165         while (mdit.is_valid()) {
0166           if (::FLAC__MetadataType mdt = mdit.get_block_type();
0167               mdt == FLAC__METADATA_TYPE_STREAMINFO) {
0168             if (FLAC::Metadata::Prototype* proto = mdit.get_block()) {
0169               auto si =
0170                 dynamic_cast<FLAC::Metadata::StreamInfo*>(proto);
0171               readFileInfo(m_fileInfo, si);
0172               delete proto;
0173             }
0174           } else if (mdt == FLAC__METADATA_TYPE_VORBIS_COMMENT) {
0175             if (FLAC::Metadata::Prototype* proto = mdit.get_block()) {
0176               if (auto vc =
0177                     dynamic_cast<FLAC::Metadata::VorbisComment*>(proto);
0178                   vc && vc->is_valid()) {
0179                 unsigned numComments = vc->get_num_comments();
0180                 for (unsigned i = 0; i < numComments; ++i) {
0181                   if (FLAC::Metadata::VorbisComment::Entry entry =
0182                         vc->get_comment(i);
0183                       entry.is_valid()) {
0184                     QString name =
0185                       QString::fromUtf8(entry.get_field_name(),
0186                                         entry.get_field_name_length())
0187                         .trimmed().toUpper();
0188                     if (QString value =
0189                           QString::fromUtf8(entry.get_field_value(),
0190                             entry.get_field_value_length()).trimmed();
0191                         !value.isEmpty()) {
0192                       m_comments.push_back(
0193                         CommentField(name, value));
0194                     }
0195                   }
0196                 }
0197               }
0198               delete proto;
0199             }
0200           }
0201 #ifdef HAVE_FLAC_PICTURE
0202           else if (mdt == FLAC__METADATA_TYPE_PICTURE) {
0203             if (FLAC::Metadata::Prototype* proto = mdit.get_block()) {
0204               if (auto pic =
0205                   dynamic_cast<FLAC::Metadata::Picture*>(proto)) {
0206                 Frame frame(Frame::FT_Picture, QLatin1String(""),
0207                             QLatin1String(""), Frame::toNegativeIndex(pictureNr++));
0208                 getPicture(frame, pic);
0209                 m_pictures.push_back(frame);
0210               }
0211               delete proto;
0212             }
0213           }
0214 #endif
0215           if (!mdit.next()) {
0216             break;
0217           }
0218         }
0219       }
0220     }
0221   }
0222 
0223   if (force) {
0224     setFilename(currentFilename());
0225   }
0226 
0227   notifyModelDataChanged(priorIsTagInformationRead);
0228 }
0229 
0230 /**
0231  * Write tags to file and rename it if necessary.
0232  *
0233  * @param force   true to force writing even if file was not changed.
0234  * @param renamed will be set to true if the file was renamed,
0235  *                i.e. the file name is no longer valid, else *renamed
0236  *                is left unchanged
0237  * @param preserve true to preserve file time stamps
0238  *
0239  * @return true if ok, false if the file could not be written or renamed.
0240  */
0241 bool FlacFile::writeTags(bool force, bool* renamed, bool preserve)
0242 {
0243   if (isChanged() &&
0244     !QFileInfo(currentFilePath()).isWritable()) {
0245     revertChangedFilename();
0246     return false;
0247   }
0248 
0249   if (m_fileRead && (force || isTagChanged(Frame::Tag_2)) && m_chain && m_chain->is_valid()) {
0250     bool commentsSet = false;
0251 #ifdef HAVE_FLAC_PICTURE
0252     bool pictureSet = false;
0253     bool pictureRemoved = false;
0254     auto pictureIt = m_pictures.begin(); // clazy:exclude=detaching-member
0255 #endif
0256 
0257     if (FLAC::Metadata::Chain::Status status = m_chain->status();
0258         status == FLAC__METADATA_CHAIN_STATUS_NOT_A_FLAC_FILE ||
0259         status == FLAC__METADATA_CHAIN_STATUS_ERROR_OPENING_FILE) {
0260       // This check is done because of a crash in mdit.get_block_type() with an
0261       // empty file with flac extension. m_chain->status() will set the status
0262       // to FLAC__METADATA_CHAIN_STATUS_OK (!?), so we have to delete the
0263       // chain to avoid a crash with the next call to writeTags().
0264       m_chain.reset();
0265       return false;
0266     }
0267 
0268     m_chain->sort_padding();
0269     FLAC::Metadata::Iterator mdit;
0270     mdit.init(*m_chain);
0271     while (mdit.is_valid()) {
0272       if (::FLAC__MetadataType mdt = mdit.get_block_type();
0273           mdt == FLAC__METADATA_TYPE_VORBIS_COMMENT) {
0274         if (commentsSet) {
0275           mdit.delete_block(true);
0276         } else {
0277           if (FLAC::Metadata::Prototype* proto = mdit.get_block()) {
0278             if (auto vc =
0279                   dynamic_cast<FLAC::Metadata::VorbisComment*>(proto);
0280                 vc && vc->is_valid()) {
0281               setVorbisComment(vc);
0282               commentsSet = true;
0283             }
0284             delete proto;
0285           }
0286         }
0287       }
0288 #ifdef HAVE_FLAC_PICTURE
0289       else if (mdt == FLAC__METADATA_TYPE_PICTURE) {
0290         if (pictureIt != m_pictures.end()) {
0291           if (FLAC::Metadata::Prototype* proto = mdit.get_block()) {
0292             if (auto pic = dynamic_cast<FLAC::Metadata::Picture*>(proto)) {
0293               if (setPicture(*pictureIt++, pic)) {
0294                 pictureSet = true;
0295               } else {
0296                 mdit.delete_block(false);
0297                 pictureRemoved = true;
0298               }
0299             }
0300             delete proto;
0301           }
0302         } else {
0303           mdit.delete_block(false);
0304           pictureRemoved = true;
0305         }
0306       } else if (mdt == FLAC__METADATA_TYPE_PADDING) {
0307         if (pictureIt != m_pictures.end()) {
0308           if (auto pic = new FLAC::Metadata::Picture;
0309               setPicture(*pictureIt, pic) && mdit.set_block(pic)) {
0310             ++pictureIt;
0311             pictureSet = true;
0312           } else {
0313             delete pic;
0314           }
0315         } else if (pictureRemoved) {
0316           mdit.delete_block(false);
0317         }
0318       }
0319 #endif
0320       if (!mdit.next()) {
0321         if (!commentsSet) {
0322           auto vc = new FLAC::Metadata::VorbisComment;
0323           if (vc->is_valid()) {
0324             setVorbisComment(vc);
0325             if (mdit.insert_block_after(vc)) {
0326               commentsSet = true;
0327             }
0328           }
0329           if (!commentsSet) {
0330             delete vc;
0331           }
0332         }
0333 #ifdef HAVE_FLAC_PICTURE
0334         while (pictureIt != m_pictures.end()) {
0335           if (auto pic = new FLAC::Metadata::Picture;
0336               setPicture(*pictureIt, pic) && mdit.insert_block_after(pic)) {
0337             pictureSet = true;
0338           } else {
0339             delete pic;
0340           }
0341           ++pictureIt;
0342         }
0343 #endif
0344         break;
0345       }
0346     }
0347 #ifdef HAVE_FLAC_PICTURE
0348     if ((commentsSet || pictureSet) &&
0349         m_chain->write(!pictureRemoved, preserve)) {
0350       markTagUnchanged(Frame::Tag_2);
0351     }
0352 #else
0353     if (commentsSet &&
0354         m_chain->write(true, preserve)) {
0355       markTagUnchanged(Frame::Tag_2);
0356     }
0357 #endif
0358     else {
0359       return false;
0360     }
0361   }
0362   if (isFilenameChanged()) {
0363     if (!renameFile()) {
0364       return false;
0365     }
0366     markFilenameUnchanged();
0367     // link tags to new file name
0368     readTags(true);
0369     *renamed = true;
0370   }
0371   return true;
0372 }
0373 
0374 /**
0375  * Free resources allocated when calling readTags().
0376  *
0377  * @param force true to force clearing even if the tags are modified
0378  */
0379 void FlacFile::clearTags(bool force)
0380 {
0381   if (!m_fileRead || (isChanged() && !force))
0382     return;
0383 
0384   bool priorIsTagInformationRead = isTagInformationRead();
0385   if (m_chain) {
0386     m_chain.reset();
0387   }
0388 #ifdef HAVE_FLAC_PICTURE
0389   m_pictures.clear();
0390 #endif
0391   m_comments.clear();
0392   markTagUnchanged(Frame::Tag_2);
0393   m_fileRead = false;
0394   notifyModelDataChanged(priorIsTagInformationRead);
0395 }
0396 
0397 #ifdef HAVE_FLAC_PICTURE
0398 /**
0399  * Check if file has a tag.
0400  *
0401  * @param tagNr tag number
0402  * @return true if a tag is available.
0403  * @see isTagInformationRead()
0404  */
0405 bool FlacFile::hasTag(Frame::TagNumber tagNr) const
0406 {
0407   return tagNr == Frame::Tag_2 && (OggFile::hasTag(Frame::Tag_2) || !m_pictures.empty());
0408 }
0409 
0410 /**
0411  * Set a frame in the tags.
0412  *
0413  * @param tagNr tag number
0414  * @param frame frame to set
0415  *
0416  * @return true if ok.
0417  */
0418 bool FlacFile::setFrame(Frame::TagNumber tagNr, const Frame& frame)
0419 {
0420   if (tagNr == Frame::Tag_2) {
0421     if (Frame::ExtendedType extendedType = frame.getExtendedType();
0422         extendedType.getType() == Frame::FT_Picture) {
0423       if (int index = Frame::fromNegativeIndex(frame.getIndex());
0424           index >= 0 && index < m_pictures.size()) {
0425         if (auto it = m_pictures.begin() + index; it != m_pictures.end()) {
0426           Frame newFrame(frame);
0427           PictureFrame::setDescription(newFrame, frame.getValue());
0428           if (PictureFrame::areFieldsEqual(*it, newFrame)) {
0429             it->setValueChanged(false);
0430           } else {
0431             *it = newFrame;
0432             markTagChanged(Frame::Tag_2, extendedType);
0433           }
0434           return true;
0435         }
0436       }
0437     }
0438   }
0439   return OggFile::setFrame(tagNr, frame);
0440 }
0441 
0442 /**
0443  * Add a frame in the tags.
0444  *
0445  * @param tagNr tag number
0446  * @param frame frame to add
0447  *
0448  * @return true if ok.
0449  */
0450 bool FlacFile::addFrame(Frame::TagNumber tagNr, Frame& frame)
0451 {
0452   if (tagNr == Frame::Tag_2) {
0453     if (Frame::ExtendedType extendedType = frame.getExtendedType();
0454         extendedType.getType() == Frame::FT_Picture) {
0455       if (frame.getFieldList().empty()) {
0456         PictureFrame::setFields(
0457               frame, Frame::TE_ISO8859_1, QLatin1String("JPG"),
0458               QLatin1String("image/jpeg"), PictureFrame::PT_CoverFront,
0459               QLatin1String(""), QByteArray());
0460       }
0461       PictureFrame::setDescription(frame, frame.getValue());
0462       frame.setIndex(Frame::toNegativeIndex(m_pictures.size()));
0463       m_pictures.push_back(frame);
0464       markTagChanged(Frame::Tag_2, extendedType);
0465       return true;
0466     }
0467   }
0468   return OggFile::addFrame(tagNr, frame);
0469 }
0470 
0471 /**
0472  * Delete a frame from the tags.
0473  *
0474  * @param tagNr tag number
0475  * @param frame frame to delete.
0476  *
0477  * @return true if ok.
0478  */
0479 bool FlacFile::deleteFrame(Frame::TagNumber tagNr, const Frame& frame)
0480 {
0481   if (tagNr == Frame::Tag_2) {
0482     if (Frame::ExtendedType extendedType = frame.getExtendedType();
0483         extendedType.getType() == Frame::FT_Picture) {
0484       if (int index = Frame::fromNegativeIndex(frame.getIndex());
0485           index >= 0 && index < m_pictures.size()) {
0486         m_pictures.removeAt(index);
0487         markTagChanged(Frame::Tag_2, extendedType);
0488         return true;
0489       }
0490     }
0491   }
0492   return OggFile::deleteFrame(tagNr, frame);
0493 }
0494 
0495 /**
0496  * Remove frames.
0497  *
0498  * @param tagNr tag number
0499  * @param flt filter specifying which frames to remove
0500  */
0501 void FlacFile::deleteFrames(Frame::TagNumber tagNr, const FrameFilter& flt)
0502 {
0503   if (tagNr != Frame::Tag_2)
0504     return;
0505 
0506   if (flt.areAllEnabled() || flt.isEnabled(Frame::FT_Picture)) {
0507     m_pictures.clear();
0508     markTagChanged(Frame::Tag_2, Frame::ExtendedType(Frame::FT_Picture));
0509   }
0510   OggFile::deleteFrames(tagNr, flt);
0511 }
0512 
0513 /**
0514  * Get all frames in tag.
0515  *
0516  * @param tagNr tag number
0517  * @param frames frame collection to set.
0518  */
0519 void FlacFile::getAllFrames(Frame::TagNumber tagNr, FrameCollection& frames)
0520 {
0521   OggFile::getAllFrames(tagNr, frames);
0522   if (tagNr == Frame::Tag_2) {
0523     int i = 0;
0524     for (auto it = m_pictures.begin(); it != m_pictures.end(); ++it) { // clazy:exclude=detaching-member
0525       it->setIndex(Frame::toNegativeIndex(i++));
0526       frames.insert(*it);
0527     }
0528     updateMarkedState(tagNr, frames);
0529   }
0530 }
0531 #endif // HAVE_FLAC_PICTURE
0532 
0533 /**
0534  * Set the vorbis comment block with the comments.
0535  *
0536  * @param vc vorbis comment block to set
0537  */
0538 void FlacFile::setVorbisComment(FLAC::Metadata::VorbisComment* vc)
0539 {
0540   // first all existing comments are deleted
0541 #ifndef HAVE_NO_FLAC_STREAMMETADATA_OPERATOR
0542   // the C++ API is not complete
0543   const ::FLAC__StreamMetadata* fsmd = *vc;
0544   FLAC__metadata_object_vorbiscomment_resize_comments(
0545     const_cast<FLAC__StreamMetadata*>(fsmd), 0);
0546 #else
0547   const unsigned numComments = vc->get_num_comments();
0548   for (unsigned i = 0; i < numComments; ++i) {
0549     vc->delete_comment(0);
0550   }
0551 #endif
0552   // then our comments are appended
0553   auto it = m_comments.begin(); // clazy:exclude=detaching-member
0554   while (it != m_comments.end()) {
0555     QString name = fixUpTagKey(it->getName(), TT_Vorbis);
0556     if (QString value(it->getValue()); !value.isEmpty()) {
0557       // The number of bytes - not characters - has to be passed to the
0558       // Entry constructor, thus qstrlen is used.
0559       QByteArray valueCStr = value.toUtf8();
0560       vc->insert_comment(vc->get_num_comments(),
0561         FLAC::Metadata::VorbisComment::Entry(
0562           name.toLatin1().data(), valueCStr, qstrlen(valueCStr)));
0563       ++it;
0564     } else {
0565       it = m_comments.erase(it);
0566     }
0567   }
0568 }
0569 
0570 /**
0571  * Get technical detail information.
0572  *
0573  * @param info the detail information is returned here
0574  */
0575 void FlacFile::getDetailInfo(DetailInfo& info) const
0576 {
0577   if (m_fileRead && m_fileInfo.valid) {
0578     info.valid = true;
0579     info.format = QLatin1String("FLAC");
0580     info.bitrate = m_fileInfo.bitrate / 1000;
0581     info.sampleRate = m_fileInfo.sampleRate;
0582     info.channels = m_fileInfo.channels;
0583     info.duration = m_fileInfo.duration;
0584   } else {
0585     info.valid = false;
0586   }
0587 }
0588 
0589 /**
0590  * Get duration of file.
0591  *
0592  * @return duration in seconds,
0593  *         0 if unknown.
0594  */
0595 unsigned FlacFile::getDuration() const
0596 {
0597   if (m_fileRead && m_fileInfo.valid) {
0598     return m_fileInfo.duration;
0599   }
0600   return 0;
0601 }
0602 
0603 /**
0604  * Get file extension including the dot.
0605  *
0606  * @return file extension ".flac".
0607  */
0608 QString FlacFile::getFileExtension() const
0609 {
0610   return QLatin1String(".flac");
0611 }
0612 
0613 
0614 /**
0615  * Read information about a FLAC file.
0616  * @param info file info to fill
0617  * @param si stream info
0618  * @return true if ok.
0619  */
0620 bool FlacFile::readFileInfo(FileInfo& info,
0621                             const FLAC::Metadata::StreamInfo* si) const
0622 {
0623   if (si && si->is_valid()) {
0624     info.valid = true;
0625     info.channels = si->get_channels();
0626     info.sampleRate = si->get_sample_rate();
0627     info.duration = info.sampleRate != 0
0628         ? si->get_total_samples() / info.sampleRate : 0;
0629     info.bitrate = si->get_bits_per_sample() * info.sampleRate;
0630   } else {
0631     info.valid = false;
0632   }
0633   return info.valid;
0634 }