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 }