diff --git a/download_testfiles.sh b/download_testfiles.sh index f7f8dc9..42da360 100755 --- a/download_testfiles.sh +++ b/download_testfiles.sh @@ -43,7 +43,7 @@ download() { skipping fi popd - + # extraction mkdir "$destdir" pushd "$destdir" @@ -59,12 +59,12 @@ download() { popd else skipping - fi + fi } download_rsync() { title="$1" cmd="$2" - + inform "Downloading \"$title\" ..." if [[ ! -d $destdir ]]; then # actual download @@ -75,7 +75,7 @@ download_rsync() { fi else skipping - fi + fi } # enter testfiles directory @@ -95,3 +95,8 @@ download \ download_rsync \ 'MTX Test Data' \ 'rsync -rv --links --delete belgarath.bunkus.org::mtx-test-data mtx-test-data' + +# convert FLAC files for FLAC tests with ffmpeg +mkdir -p flac +[[ ! -f flac/test.flac ]] && ffmpeg -i mtx-test-data/alac/othertest-itunes.m4a -c:a flac flac/test.flac # raw FLAC stream +[[ ! -f flac/test.ogg ]] && ffmpeg -i flac/test.flac -c:a copy flac/test.ogg # FLAC in Ogg diff --git a/flac/flacstream.cpp b/flac/flacstream.cpp index 3f200b8..7cc1fb9 100644 --- a/flac/flacstream.cpp +++ b/flac/flacstream.cpp @@ -51,6 +51,20 @@ VorbisComment *FlacStream::createVorbisComment() return m_vorbisComment.get(); } +/*! + * \brief Removes the assigned Vorbis comment if one is assigned; does nothing otherwise. + * \returns Returns whether there were a Vorbis comment assigned. + */ +bool FlacStream::removeVorbisComment() +{ + if(m_vorbisComment) { + m_vorbisComment.reset(); + return true; + } else { + return false; + } +} + void FlacStream::internalParseHeader() { static const string context("parsing raw FLAC header"); diff --git a/flac/flacstream.h b/flac/flacstream.h index 0857d2b..3303360 100644 --- a/flac/flacstream.h +++ b/flac/flacstream.h @@ -21,6 +21,7 @@ public: TrackType type() const; VorbisComment *vorbisComment() const; VorbisComment *createVorbisComment(); + bool removeVorbisComment(); uint32 paddingSize() const; uint32 streamOffset() const; diff --git a/mediafileinfo.cpp b/mediafileinfo.cpp index 904af49..7e91fc5 100644 --- a/mediafileinfo.cpp +++ b/mediafileinfo.cpp @@ -512,16 +512,16 @@ void MediaFileInfo::parseEverything() * \param requiredTargets Specifies the required targets. Targets are ignored if not supported by the container. * \return Returns an indication whether appropriate tags could be created for the file. * \remarks - * - The ID3 related arguments are only practiced when the file format is MP3 or when the file format - * is unknown and \a treatUnknownFilesAsMp3Files is true. These arguments are ignored when creating - * tags for other known file formats such as MP4. - * - Tags might be removed as well. For example the existing ID3v1 tag of an MP3 file will be removed - * if \a id3v1Usage is set to TagUsage::Never. - * - The method might do nothing if the file already has appropriate tags. - * - This is only a convenience method. The task can be done by manually using the methods createId3v1Tag(), - * createId3v2Tag(), removeId3v1Tag() ... as well. - * - Some tag information might be discarded. For example when an ID3v2 tag needs to be removed (\a id3v2usage is set to TagUsage::Never) - * and an ID3v1 tag will be created instead not all fields can be transfered. + * - The ID3 related arguments are only practiced when the file format is MP3 or when the file format + * is unknown and \a treatUnknownFilesAsMp3Files is true. These arguments are ignored when creating + * tags for other known file formats such as MP4. + * - Tags might be removed as well. For example the existing ID3v1 tag of an MP3 file will be removed + * if \a id3v1Usage is set to TagUsage::Never. + * - The method might do nothing if the file already has appropriate tags. + * - This is only a convenience method. The task can be done by manually using the methods createId3v1Tag(), + * createId3v2Tag(), removeId3v1Tag() ... as well. + * - Some tag information might be discarded. For example when an ID3v2 tag needs to be removed (\a id3v2usage is set to TagUsage::Never) + * and an ID3v1 tag will be created instead not all fields can be transfered. */ bool MediaFileInfo::createAppropriateTags(bool treatUnknownFilesAsMp3Files, TagUsage id3v1usage, TagUsage id3v2usage, bool mergeMultipleSuccessiveId3v2Tags, bool keepExistingId3v2version, uint32 id3v2version, const std::vector &requiredTargets) { @@ -911,7 +911,6 @@ bool MediaFileInfo::removeId3v2Tag(Id3v2Tag *tag) * To apply the removal and other changings call the applyChanges() method. * * \returns Returns whether there where ID3v2 tags assigned which could be removed. - * * \sa applyChanges() */ bool MediaFileInfo::removeAllId3v2Tags() @@ -934,10 +933,8 @@ bool MediaFileInfo::removeAllId3v2Tags() * To apply the created tag and other changings call the applyChanges() method. * * \returns Returns the first ID3v2 tag of the current file. - * * \remarks The MediaFileInfo keeps the ownership over the created tag. It will be * destroyed when the MediaFileInfo is invalidated. - * * \sa applyChanges() */ Id3v2Tag *MediaFileInfo::createId3v2Tag() @@ -988,6 +985,9 @@ void MediaFileInfo::removeAllTags() if(m_container) { m_container->removeAllTags(); } + if(m_singleTrack && m_containerFormat == ContainerFormat::Flac) { + static_cast(m_singleTrack.get())->removeVorbisComment(); + } m_id3v1Tag.reset(); m_id3v2Tags.clear(); } @@ -1347,6 +1347,69 @@ bool MediaFileInfo::id3v2ToId3v1() } } +/*! + * \brief Creates a Vorbis comment for the current file. + * + * This method does nothing if the tags/tracks of the current file haven't been parsed using + * the parseTags() and parseTracks() methods. + * + * If the file has already a Vorbis comment no new tag will be created. + * + * To apply the created tag and other changings call the applyChanges() method. + * + * \returns Returns the Vorbis comment or nullptr if creation is not possible. + * + * \sa applyChanges() + */ +VorbisComment *MediaFileInfo::createVorbisComment() +{ + switch(m_containerFormat) { + case ContainerFormat::Ogg: + if(m_container) { + return static_cast(m_container.get())->createTag(TagTarget()); + } + break; + case ContainerFormat::Flac: + if(m_singleTrack) { + return static_cast(m_singleTrack.get())->createVorbisComment(); + } + break; + default: + ; + } + return nullptr; +} + +/*! + * \brief Removes all assigned Vorbis comment from the current file. + * + * To apply the removal and other changings call the applyChanges() method. + * + * \returns Returns whether there was an Vorbis comment assigned which could be removed. + * + * \sa applyChanges() + */ +bool MediaFileInfo::removeVorbisComment() +{ + switch(m_containerFormat) { + case ContainerFormat::Ogg: + if(m_container) { + bool hadTags = static_cast(m_container.get())->tagCount(); + static_cast(m_container.get())->removeAllTags(); + return hadTags; + } + break; + case ContainerFormat::Flac: + if(m_singleTrack) { + return static_cast(m_singleTrack.get())->removeVorbisComment(); + } + break; + default: + ; + } + return false; +} + /*! * \brief Stores all tags assigned to the current file in the specified vector. * diff --git a/mediafileinfo.h b/mediafileinfo.h index 0dcb52c..3edf1f1 100644 --- a/mediafileinfo.h +++ b/mediafileinfo.h @@ -131,6 +131,8 @@ public: void mergeId3v2Tags(); bool id3v1ToId3v2(); bool id3v2ToId3v1(); + VorbisComment *createVorbisComment(); + bool removeVorbisComment(); // methods to get/wipe notifications bool haveRelatedObjectsNotifications() const; diff --git a/ogg/oggcontainer.cpp b/ogg/oggcontainer.cpp index 8c8541d..e17b9d2 100644 --- a/ogg/oggcontainer.cpp +++ b/ogg/oggcontainer.cpp @@ -419,7 +419,9 @@ void OggContainer::internalMakeFile() // check whether there is a new comment to be inserted into the current page if(m_iterator.currentPageIndex() == currentParams->lastPageIndex && currentParams->firstSegmentIndex == static_cast(-1)) { - makeVorbisCommentSegment(buffer, copyHelper, newSegmentSizes, currentComment, currentParams); + if(!currentParams->removed) { + makeVorbisCommentSegment(buffer, copyHelper, newSegmentSizes, currentComment, currentParams); + } // proceed with next comment if(++tagIterator != tagEnd) { currentParams = &(currentComment = tagIterator->get())->oggParams(); diff --git a/tests/overall.cpp b/tests/overall.cpp index 0c77bf2..ea45067 100644 --- a/tests/overall.cpp +++ b/tests/overall.cpp @@ -45,6 +45,8 @@ class OverallTests : public TestFixture CPPUNIT_TEST(testMp3Making); CPPUNIT_TEST(testOggParsing); CPPUNIT_TEST(testOggMaking); + CPPUNIT_TEST(testFlacParsing); + CPPUNIT_TEST(testFlacMaking); CPPUNIT_TEST(testMkvParsing); CPPUNIT_TEST(testMkvMaking); CPPUNIT_TEST_SUITE_END(); @@ -83,6 +85,9 @@ public: void checkOggTestfile2(); void checkOggTestMetaData(); + void checkFlacTestfile1(); + void checkFlacTestfile2(); + void setMkvTestMetaData(); void setMp4TestMetaData(); void setMp3TestMetaData(); @@ -97,6 +102,8 @@ public: void testMp3Making(); void testOggParsing(); void testOggMaking(); + void testFlacParsing(); + void testFlacMaking(); private: MediaFileInfo m_fileInfo; @@ -967,6 +974,7 @@ void OverallTests::checkOggTestfile1() switch(m_tagStatus) { case TagStatus::Original: CPPUNIT_ASSERT(tags.size() == 1); + CPPUNIT_ASSERT(tags.front()->value(KnownField::Encoder).toString() == "ffmpeg2theora 0.13"); // Theora tags are currently not supported and hence only the Vorbis comment is // taken into account here break; @@ -1003,6 +1011,7 @@ void OverallTests::checkOggTestfile2() switch(m_tagStatus) { case TagStatus::Original: CPPUNIT_ASSERT(tags.size() == 1); + CPPUNIT_ASSERT(tags.front()->value(KnownField::Encoder).toString() == "opusenc from opus-tools 0.1.6"); break; case TagStatus::TestMetaDataPresent: checkOggTestMetaData(); @@ -1034,6 +1043,76 @@ void OverallTests::checkOggTestMetaData() m_preservedMetaData.pop(); } +/*! + * \brief Checks "flac/test.flac" (converted from "mtx-test-data/alac/othertest-itunes.m4a" via ffmpeg). + * \remarks Raw FLAC stream. + */ +void OverallTests::checkFlacTestfile1() +{ + CPPUNIT_ASSERT(m_fileInfo.containerFormat() == ContainerFormat::Flac); + const auto tracks = m_fileInfo.tracks(); + CPPUNIT_ASSERT(tracks.size() == 1); + for(const auto &track : tracks) { + CPPUNIT_ASSERT(track->mediaType() == MediaType::Audio); + CPPUNIT_ASSERT(track->format() == GeneralMediaFormat::Flac); + CPPUNIT_ASSERT(track->channelCount() == 2); + CPPUNIT_ASSERT(track->samplingFrequency() == 44100); + CPPUNIT_ASSERT(track->bitsPerSample() == 16); + } + const auto tags = m_fileInfo.tags(); + switch(m_tagStatus) { + case TagStatus::Original: + // ffmpeg is able to set some tags from the original file (mtx-test-data/alac/othertest-itunes.m4a) + CPPUNIT_ASSERT(tags.size() == 1); + CPPUNIT_ASSERT(tags.front()->value(KnownField::Title).toString() == "Sad Song"); + CPPUNIT_ASSERT(tags.front()->value(KnownField::Artist).toString() == "Oasis"); + CPPUNIT_ASSERT(tags.front()->value(KnownField::Album).toString() == "Don't Go Away (Apple Lossless)"); + CPPUNIT_ASSERT(tags.front()->value(KnownField::Genre).toString() == "Alternative & Punk"); + CPPUNIT_ASSERT(tags.front()->value(KnownField::Encoder).toString() == "Lavf57.25.100"); + CPPUNIT_ASSERT(tags.front()->value(KnownField::Year).toString() == "1998"); + CPPUNIT_ASSERT(tags.front()->value(KnownField::Comment).isEmpty()); + //CPPUNIT_ASSERT(tags.front()->value(KnownField::Cover).dataSize() == 0x58f3); + //CPPUNIT_ASSERT(BE::toUInt64(tags.front()->value(KnownField::Cover).dataPointer()) == 0xFFD8FFE000104A46); + CPPUNIT_ASSERT(tags.front()->value(KnownField::TrackPosition).toPositionInSet() == PositionInSet(3, 4)); + CPPUNIT_ASSERT(tags.front()->value(KnownField::DiskPosition).toPositionInSet() == PositionInSet(1, 1)); + break; + case TagStatus::TestMetaDataPresent: + checkOggTestMetaData(); + break; + case TagStatus::Removed: + CPPUNIT_ASSERT(tags.size() == 0); + } +} + +/*! + * \brief Checks "flac/test.ogg" (converted from "flac/test.flac" via ffmpeg). + * \remarks FLAC in Ogg. + */ +void OverallTests::checkFlacTestfile2() +{ + CPPUNIT_ASSERT(m_fileInfo.containerFormat() == ContainerFormat::Ogg); + const auto tracks = m_fileInfo.tracks(); + CPPUNIT_ASSERT(tracks.size() == 1); + for(const auto &track : tracks) { + CPPUNIT_ASSERT(track->mediaType() == MediaType::Audio); + CPPUNIT_ASSERT(track->format() == GeneralMediaFormat::Flac); + CPPUNIT_ASSERT(track->channelCount() == 2); + CPPUNIT_ASSERT(track->samplingFrequency() == 44100); + CPPUNIT_ASSERT(track->bitsPerSample() == 16); + } + const auto tags = m_fileInfo.tags(); + switch(m_tagStatus) { + case TagStatus::Original: + CPPUNIT_ASSERT(tags.size() == 1); + break; + case TagStatus::TestMetaDataPresent: + checkOggTestMetaData(); + break; + case TagStatus::Removed: + CPPUNIT_ASSERT(tags.size() == 0); + } +} + /*! * \brief Creates a tag targeting the first track with some test meta data. */ @@ -1115,7 +1194,7 @@ void OverallTests::setMp3TestMetaData() void OverallTests::setOggTestMetaData() { // ensure a tag exists - Tag *tag = m_fileInfo.container()->createTag(); + VorbisComment *tag = m_fileInfo.createVorbisComment(); // assign test meta data tag->setValue(KnownField::Title, m_testTitle); @@ -1373,6 +1452,7 @@ void OverallTests::testMp3Making() /*! * \brief Tests the Ogg parser via MediaFileInfo. + * \remarks FLAC in Ogg is tested in testFlacParsing(). */ void OverallTests::testOggParsing() { @@ -1385,7 +1465,9 @@ void OverallTests::testOggParsing() /*! * \brief Tests the Ogg maker via MediaFileInfo. - * \remarks Relies on the parser to check results. + * \remarks + * - Relies on the parser to check results. + * - FLAC in Ogg is tested in testFlacMaking(). */ void OverallTests::testOggMaking() { @@ -1414,3 +1496,46 @@ void OverallTests::testOggMaking() makeFile(TestUtilities::workingCopyPath("mtx-test-data/opus/v-opus.ogg"), modifyRoutine, &OverallTests::checkOggTestfile2); } } + +/*! + * \brief Tests the FLAC parser via MediaFileInfo. + */ +void OverallTests::testFlacParsing() +{ + cerr << endl << "FLAC parser" << endl; + m_fileInfo.setForceFullParse(false); + m_tagStatus = TagStatus::Original; + parseFile(TestUtilities::testFilePath("flac/test.flac"), &OverallTests::checkFlacTestfile1); + parseFile(TestUtilities::testFilePath("flac/test.ogg"), &OverallTests::checkFlacTestfile2); +} + +/*! + * \brief Tests the FLAC maker via MediaFileInfo. + * \remarks Relies on the parser to check results. + */ +void OverallTests::testFlacMaking() +{ + // full parse is required to determine padding + m_fileInfo.setForceFullParse(true); + + // do the test under different conditions + for(m_mode = 0; m_mode != 0x2; ++m_mode) { + // TODO: setup test conditions + + // print test conditions + list testConditions; + if(m_mode & 0x1) { + testConditions.emplace_back("removing tag"); + } else { + testConditions.emplace_back("modifying tag"); + } + cerr << endl << "FLAC maker - testmode " << m_mode << ": " << joinStrings(testConditions, ", ") << endl; + + // do actual tests + bool remove = m_mode & 0x1; + m_tagStatus = remove ? TagStatus::Removed : TagStatus::TestMetaDataPresent; + void (OverallTests::*modifyRoutine)(void) = remove ? &OverallTests::removeAllTags : &OverallTests::setOggTestMetaData; + makeFile(TestUtilities::workingCopyPath("flac/test.flac"), modifyRoutine, &OverallTests::checkFlacTestfile1); + makeFile(TestUtilities::workingCopyPath("flac/test.ogg"), modifyRoutine, &OverallTests::checkFlacTestfile2); + } +}