diff --git a/CMakeLists.txt b/CMakeLists.txt index 692528f..e677330 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,7 +112,6 @@ set(SRC_FILES tagvalue.cpp vorbis/vorbiscomment.cpp vorbis/vorbiscommentfield.cpp - vorbis/vorbiscommentids.cpp vorbis/vorbisidentificationheader.cpp wav/waveaudiostream.cpp id3/id3genres.cpp diff --git a/README.md b/README.md index c777f24..cb6c24f 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,6 @@ The tagparser library depends on c++utilities and is built in the same way. It also depends on zlib. ## TODO -- Support more tag formats (EXIF, PDF metadata, ...). +- Support more formats (EXIF, PDF metadata, Theora, ...). +- Allow adding tags to specific streams when dealing with OGG. - Do tests with Matroska files which have multiple segments. diff --git a/genericcontainer.h b/genericcontainer.h index 1635067..b04c644 100644 --- a/genericcontainer.h +++ b/genericcontainer.h @@ -3,6 +3,8 @@ #include "./abstractcontainer.h" +#include + #include #include #include @@ -202,7 +204,7 @@ inline const std::vector > &GenericContainer > &GenericContainer > &GenericContainer -inline TagType *GenericContainer::createTag(const TagTarget &target) +TagType *GenericContainer::createTag(const TagTarget &target) { if(!target.isEmpty()) { for(auto &tag : m_tags) { @@ -254,7 +256,7 @@ inline TagType *GenericContainer: } else if(!m_tags.empty()) { return m_tags.front().get(); } - m_tags.emplace_back(new TagType()); + m_tags.emplace_back(std::make_unique()); auto &tag = m_tags.back(); tag->setTarget(target); return tag.get(); @@ -308,7 +310,7 @@ bool GenericContainer::removeTrac bool removed = false; if(areTracksParsed() && supportsTrackModifications() && !m_tracks.empty()) { for(auto i = m_tracks.end() - 1, begin = m_tracks.begin(); ; --i) { - if(static_cast(i->get()) == static_cast(track)) { + if(static_cast(i->get()) == track) { i->release(); m_tracks.erase(i); removed = true; diff --git a/ogg/oggcontainer.cpp b/ogg/oggcontainer.cpp index 39b6d57..71fb110 100644 --- a/ogg/oggcontainer.cpp +++ b/ogg/oggcontainer.cpp @@ -30,10 +30,126 @@ OggContainer::~OggContainer() void OggContainer::reset() { - m_commentTable.clear(); m_iterator.reset(); } +/*! + * \brief Creates a new tag. + * \sa AbstractContainer::createTag() + * \remarks Tracks must be parsed before because tags are stored on track level! + */ +VorbisComment *OggContainer::createTag(const TagTarget &target) +{ + if(!target.isEmpty()) { + // targets are not supported here, so the specified target should be empty + // -> just be consistent with generic implementation here + for(auto &tag : m_tags) { + if(tag->target() == target && !tag->oggParams().removed) { + return tag.get(); + } + } + for(auto &tag : m_tags) { + if(tag->target() == target) { + tag->oggParams().removed = false; + return tag.get(); + } + } + } else if(VorbisComment *comment = tag(0)) { + comment->oggParams().removed = false; + return comment; + } else if(!m_tags.empty()) { + m_tags.front()->oggParams().removed = false; + return m_tags.front().get(); + } + + // a new tag needs to be created + // -> determine an appropriate track for the tag + // -> just use the first Vorbis/Opus track + // -> TODO: provide interface for specifying a specific track + for(const auto &track : m_tracks) { + switch(track->format().general) { + case GeneralMediaFormat::Vorbis: + case GeneralMediaFormat::Opus: + // check whether start page has a valid value + if(track->startPage() < m_iterator.pages().size()) { + ariseComment(track->startPage(), static_cast(-1), track->format().general); + m_tags.back()->setTarget(target); // also for consistency + return m_tags.back().get(); + } else { + // TODO: error handling? + } + default: + ; + } + } + return nullptr; +} + +VorbisComment *OggContainer::tag(size_t index) +{ + size_t i = 0; + for(const auto &tag : m_tags) { + if(!tag->oggParams().removed) { + if(index == i) { + return tag.get(); + } + ++i; + } + } + return nullptr; +} + +size_t OggContainer::tagCount() const +{ + size_t count = 0; + for(const auto &tag : m_tags) { + if(!tag->oggParams().removed) { + ++count; + } + } + return count; +} + +/*! + * \brief Actually just flags the specified \a tag as removed and clears all assigned fields. + * + * This specialization is neccessary because completeley removing the tag whould also + * remove the OGG parameter which are needed when appying the changes. + * + * \remarks Seems like common players aren't able to play Vorbis when no comment is present. + * So do NOT use this method to remove tags from Vorbis, just call removeAllFields() on \a tag. + * \sa AbstractContainer::removeTag() + */ +bool OggContainer::removeTag(Tag *tag) +{ + for(auto &existingTag : m_tags) { + if(static_cast(existingTag.get()) == tag) { + existingTag->removeAllFields(); + existingTag->oggParams().removed = true; + return true; + } + } + return false; +} + +/*! + * \brief Actually just flags all tags as removed and clears all assigned fields. + * + * This specialization is neccessary because completeley removing the tag whould also + * remove the OGG parameter which are needed when appying the changes. + * + * \remarks Seems like common players aren't able to play Vorbis when no comment is present. + * So do NOT use this method to remove tags from Vorbis, just call removeAllFields() on all tags. + * \sa AbstractContainer::removeAllTags() + */ +void OggContainer::removeAllTags() +{ + for(auto &existingTag : m_tags) { + existingTag->removeAllFields(); + existingTag->oggParams().removed = true; + } +} + void OggContainer::internalParseHeader() { static const string context("parsing OGG bitstream header"); @@ -68,60 +184,80 @@ void OggContainer::internalParseHeader() } catch(const TruncatedDataException &) { // thrown when page exceeds max size addNotification(NotificationType::Critical, "The OGG file is truncated.", context); - throw; } catch(const InvalidDataException &) { // thrown when first 4 byte do not match capture pattern addNotification(NotificationType::Critical, "Capture pattern \"OggS\" at " + ConversionUtilities::numberToString(m_iterator.currentSegmentOffset()) + " expected.", context); - throw; } } void OggContainer::internalParseTags() { - parseTracks(); // tracks needs to be parsed because tags are stored at stream level - for(VorbisCommentInfo &vorbisCommentInfo : m_commentTable) { - m_iterator.setPageIndex(vorbisCommentInfo.firstPageIndex); - m_iterator.setSegmentIndex(vorbisCommentInfo.firstSegmentIndex); - switch(vorbisCommentInfo.streamFormat) { + // tracks needs to be parsed before because tags are stored at stream level + parseTracks(); + for(auto &comment : m_tags) { + OggParameter ¶ms = comment->oggParams(); + m_iterator.setPageIndex(params.firstPageIndex); + m_iterator.setSegmentIndex(params.firstSegmentIndex); + switch(params.streamFormat) { case GeneralMediaFormat::Vorbis: - m_tags[vorbisCommentInfo.tagIndex]->parse(m_iterator); + comment->parse(m_iterator); break; case GeneralMediaFormat::Opus: - m_iterator.seekForward(8); // skip header (has already been detected by OggStream) - m_tags[vorbisCommentInfo.tagIndex]->parse(m_iterator, true); + // skip header (has already been detected by OggStream) + m_iterator.seekForward(8); + comment->parse(m_iterator, true); break; default: addNotification(NotificationType::Critical, "Stream format not supported.", "parsing tags from OGG streams"); } - vorbisCommentInfo.lastPageIndex = m_iterator.currentPageIndex(); - vorbisCommentInfo.lastSegmentIndex = m_iterator.currentSegmentIndex(); + params.lastPageIndex = m_iterator.currentPageIndex(); + params.lastSegmentIndex = m_iterator.currentSegmentIndex(); } } -void OggContainer::ariseComment(vector::size_type pageIndex, vector::size_type segmentIndex, GeneralMediaFormat mediaFormat) +void OggContainer::ariseComment(std::size_t pageIndex, std::size_t segmentIndex, GeneralMediaFormat mediaFormat) { - m_commentTable.emplace_back(pageIndex, segmentIndex, m_tags.size(), mediaFormat); m_tags.emplace_back(make_unique()); + m_tags.back()->oggParams().set(pageIndex, segmentIndex, mediaFormat); } void OggContainer::internalParseTracks() { - if(!areTracksParsed()) { - parseHeader(); - static const string context("parsing OGG stream"); - for(auto &stream : m_tracks) { - try { // try to parse header - stream->parseHeader(); - if(stream->duration() > m_duration) { - m_duration = stream->duration(); - } - } catch(const Failure &) { - addNotification(NotificationType::Critical, "Unable to parse stream at " + ConversionUtilities::numberToString(stream->startOffset()) + ".", context); + static const string context("parsing OGG stream"); + for(auto &stream : m_tracks) { + try { // try to parse header + stream->parseHeader(); + if(stream->duration() > m_duration) { + m_duration = stream->duration(); } + } catch(const Failure &) { + addNotification(NotificationType::Critical, "Unable to parse stream at " + ConversionUtilities::numberToString(stream->startOffset()) + ".", context); } } } +/*! + * \brief Writes the specified \a comment with the given \a params to the specified \a buffer and + * adds the number of bytes written to \a newSegmentSizes. + */ +void makeVorbisCommentSegment(stringstream &buffer, CopyHelper<65307> ©Helper, vector &newSegmentSizes, VorbisComment *comment, OggParameter *params) +{ + auto offset = buffer.tellp(); + switch(params->streamFormat) { + case GeneralMediaFormat::Vorbis: + comment->make(buffer); + break; + case GeneralMediaFormat::Opus: + ConversionUtilities::BE::getBytes(0x4F70757354616773u, copyHelper.buffer()); + buffer.write(copyHelper.buffer(), 8); + comment->make(buffer, true); + break; + default: + ; + } + newSegmentSizes.push_back(buffer.tellp() - offset); +} + void OggContainer::internalMakeFile() { const string context("making OGG file"); @@ -134,18 +270,32 @@ void OggContainer::internalMakeFile() BackupHelper::createBackupFile(fileInfo().path(), backupPath, backupStream); // recreate original file fileInfo().stream().open(fileInfo().path(), ios_base::out | ios_base::binary | ios_base::trunc); - CopyHelper<65307> copy; - auto commentTableIterator = m_commentTable.cbegin(), commentTableEnd = m_commentTable.cend(); + + // prepare iterating comments + VorbisComment *currentComment; + OggParameter *currentParams; + auto tagIterator = m_tags.cbegin(), tagEnd = m_tags.cend(); + if(tagIterator != tagEnd) { + currentParams = &(currentComment = tagIterator->get())->oggParams(); + } else { + currentComment = nullptr; + currentParams = nullptr; + } + + // define misc variables + CopyHelper<65307> copyHelper; vector updatedPageOffsets; - uint32 pageSequenceNumber = 0; + unordered_map pageSequenceNumberBySerialNo; + + // iterate through all pages of the original file for(m_iterator.setStream(backupStream), m_iterator.removeFilter(), m_iterator.reset(); m_iterator; m_iterator.nextPage()) { - const auto ¤tPage = m_iterator.currentPage(); - auto pageSize = currentPage.totalSize(); + const OggPage ¤tPage = m_iterator.currentPage(); + const auto pageSize = currentPage.totalSize(); + uint32 &pageSequenceNumber = pageSequenceNumberBySerialNo[currentPage.streamSerialNumber()]; // check whether the Vorbis Comment is present in this Ogg page - // -> then the page needs to be rewritten - if(commentTableIterator != commentTableEnd - && m_iterator.currentPageIndex() >= commentTableIterator->firstPageIndex - && m_iterator.currentPageIndex() <= commentTableIterator->lastPageIndex + if(currentComment + && m_iterator.currentPageIndex() >= currentParams->firstPageIndex + && m_iterator.currentPageIndex() <= currentParams->lastPageIndex && !currentPage.segmentSizes().empty()) { // page needs to be rewritten (not just copied) // -> write segments to a buffer first @@ -157,40 +307,48 @@ void OggContainer::internalMakeFile() for(const auto segmentSize : currentPage.segmentSizes()) { if(segmentSize) { // check whether this segment contains the Vorbis Comment - if((m_iterator.currentPageIndex() > commentTableIterator->firstPageIndex || segmentIndex >= commentTableIterator->firstSegmentIndex) - && (m_iterator.currentPageIndex() < commentTableIterator->lastPageIndex || segmentIndex <= commentTableIterator->lastSegmentIndex)) { - // prevent making the comment twice if it spreads over multiple pages - if(m_iterator.currentPageIndex() == commentTableIterator->firstPageIndex) { - // make Vorbis Comment segment - auto offset = buffer.tellp(); - switch(commentTableIterator->streamFormat) { - case GeneralMediaFormat::Vorbis: - m_tags[commentTableIterator->tagIndex]->make(buffer); - break; - case GeneralMediaFormat::Opus: - ConversionUtilities::BE::getBytes(0x4F70757354616773u, copy.buffer()); - buffer.write(copy.buffer(), 8); - m_tags[commentTableIterator->tagIndex]->make(buffer, true); - break; - default: - ; - } - newSegmentSizes.push_back(buffer.tellp() - offset); + if((m_iterator.currentPageIndex() >= currentParams->firstPageIndex && segmentIndex >= currentParams->firstSegmentIndex) + && (m_iterator.currentPageIndex() <= currentParams->lastPageIndex && segmentIndex <= currentParams->lastSegmentIndex)) { + // prevent making the comment twice if it spreads over multiple pages/segments + if(!currentParams->removed + && ((m_iterator.currentPageIndex() == currentParams->firstPageIndex + && m_iterator.currentSegmentIndex() == currentParams->firstSegmentIndex))) { + makeVorbisCommentSegment(buffer, copyHelper, newSegmentSizes, currentComment, currentParams); } - if(m_iterator.currentPageIndex() > commentTableIterator->lastPageIndex - || (m_iterator.currentPageIndex() == commentTableIterator->lastPageIndex && segmentIndex > commentTableIterator->lastSegmentIndex)) { - ++commentTableIterator; + + // proceed with next comment? + if(m_iterator.currentPageIndex() > currentParams->lastPageIndex + || (m_iterator.currentPageIndex() == currentParams->lastPageIndex && segmentIndex > currentParams->lastSegmentIndex)) { + if(++tagIterator != tagEnd) { + currentParams = &(currentComment = tagIterator->get())->oggParams(); + } else { + currentComment = nullptr; + currentParams = nullptr; + } } } else { // copy other segments unchanged backupStream.seekg(segmentOffset); - copy.copy(backupStream, buffer, segmentSize); + copyHelper.copy(backupStream, buffer, segmentSize); newSegmentSizes.push_back(segmentSize); + + // 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); + // proceed with next comment + if(++tagIterator != tagEnd) { + currentParams = &(currentComment = tagIterator->get())->oggParams(); + } else { + currentComment = nullptr; + currentParams = nullptr; + } + } } segmentOffset += segmentSize; } ++segmentIndex; } + // write buffered data to actual stream auto newSegmentSizesIterator = newSegmentSizes.cbegin(), newSegmentSizesEnd = newSegmentSizes.cend(); bool continuePreviousSegment = false; @@ -201,7 +359,7 @@ void OggContainer::internalMakeFile() // write header backupStream.seekg(currentPage.startOffset()); updatedPageOffsets.push_back(stream().tellp()); // memorize offset to update checksum later - copy.copy(backupStream, stream(), 27); // just copy header from original file + copyHelper.copy(backupStream, stream(), 27); // just copy header from original file // set continue flag stream().seekp(-22, ios_base::cur); stream().put(currentPage.headerTypeFlag() & (continuePreviousSegment ? 0xFF : 0xFE)); @@ -214,21 +372,21 @@ void OggContainer::internalMakeFile() // write segment sizes as long as there are segment sizes to be written and // the max number of segment sizes (255) is not exceeded uint32 currentSize = 0; - while(bytesLeft > 0 && segmentSizesWritten < 0xFF) { + while(bytesLeft && segmentSizesWritten < 0xFF) { while(bytesLeft >= 0xFF && segmentSizesWritten < 0xFF) { stream().put(0xFF); currentSize += 0xFF; bytesLeft -= 0xFF; ++segmentSizesWritten; } - if(bytesLeft > 0 && segmentSizesWritten < 0xFF) { + if(bytesLeft && segmentSizesWritten < 0xFF) { // bytes left is here < 0xFF stream().put(bytesLeft); currentSize += bytesLeft; bytesLeft = 0; ++segmentSizesWritten; } - if(bytesLeft == 0) { + if(!bytesLeft) { // sizes for the segment have been written // -> continue with next segment if(++newSegmentSizesIterator != newSegmentSizesEnd) { @@ -237,10 +395,12 @@ void OggContainer::internalMakeFile() } } } + // there are no bytes left in the current segment; remove continue flag - if(bytesLeft == 0) { + if(!bytesLeft) { continuePreviousSegment = false; } + // page is full or all segment data has been covered // -> write segment table size (segmentSizesWritten) and segment data // -> seek back and write updated page segment number @@ -248,43 +408,51 @@ void OggContainer::internalMakeFile() stream().put(segmentSizesWritten); stream().seekp(segmentSizesWritten, ios_base::cur); // -> write actual page data - copy.copy(buffer, stream(), currentSize); + copyHelper.copy(buffer, stream(), currentSize); ++pageSequenceNumber; } } + } else { if(pageSequenceNumber != m_iterator.currentPageIndex()) { // just update page sequence number backupStream.seekg(currentPage.startOffset()); updatedPageOffsets.push_back(stream().tellp()); // memorize offset to update checksum later - copy.copy(backupStream, stream(), 27); + copyHelper.copy(backupStream, stream(), 27); stream().seekp(-9, ios_base::cur); writer().writeUInt32LE(pageSequenceNumber); stream().seekp(5, ios_base::cur); - copy.copy(backupStream, stream(), pageSize - 27); + copyHelper.copy(backupStream, stream(), pageSize - 27); } else { // copy page unchanged backupStream.seekg(currentPage.startOffset()); - copy.copy(backupStream, stream(), pageSize); + copyHelper.copy(backupStream, stream(), pageSize); } ++pageSequenceNumber; } } + + fileInfo().reportSizeChanged(stream().tellp()); + // close backups stream; reopen new file as readable stream backupStream.close(); fileInfo().close(); - fileInfo().open(); + fileInfo().stream().open(fileInfo().path(), ios_base::in | ios_base::out | ios_base::binary); + // update checksums of modified pages for(auto offset : updatedPageOffsets) { OggPage::updateChecksum(fileInfo().stream(), offset); } + // clear iterator - m_iterator = OggIterator(fileInfo().stream(), startOffset(), fileInfo().size()); + m_iterator.clear(fileInfo().stream(), startOffset(), fileInfo().size()); + } catch(const OperationAbortedException &) { addNotification(NotificationType::Information, "Rewriting file to apply new tag information has been aborted.", context); BackupHelper::restoreOriginalFileFromBackupFile(fileInfo().path(), backupPath, fileInfo().stream(), backupStream); m_iterator.setStream(fileInfo().stream()); throw; + } catch(const ios_base::failure &ex) { addNotification(NotificationType::Critical, "IO error occured when rewriting file to apply new tag information.\n" + string(ex.what()), context); BackupHelper::restoreOriginalFileFromBackupFile(fileInfo().path(), backupPath, fileInfo().stream(), backupStream); diff --git a/ogg/oggcontainer.h b/ogg/oggcontainer.h index 4c473a8..434694f 100644 --- a/ogg/oggcontainer.h +++ b/ogg/oggcontainer.h @@ -16,27 +16,6 @@ namespace Media { class MediaFileInfo; -struct LIB_EXPORT VorbisCommentInfo -{ - VorbisCommentInfo(std::vector::size_type firstPageIndex, std::vector::size_type firstSegmentIndex, std::vector::size_type tagIndex, GeneralMediaFormat streamFormat = GeneralMediaFormat::Vorbis); - - std::vector::size_type firstPageIndex; - std::vector::size_type firstSegmentIndex; - std::vector::size_type lastPageIndex; - std::vector::size_type lastSegmentIndex; - std::vector >::size_type tagIndex; - GeneralMediaFormat streamFormat; -}; - -inline VorbisCommentInfo::VorbisCommentInfo(std::vector::size_type firstPageIndex, std::vector::size_type firstSegmentIndex, std::vector::size_type tagIndex, GeneralMediaFormat mediaFormat) : - firstPageIndex(firstPageIndex), - firstSegmentIndex(firstSegmentIndex), - lastPageIndex(0), - lastSegmentIndex(0), - tagIndex(tagIndex), - streamFormat(mediaFormat) -{} - class LIB_EXPORT OggContainer : public GenericContainer { friend class OggStream; @@ -49,6 +28,12 @@ public: void setChecksumValidationEnabled(bool enabled); void reset(); + VorbisComment *createTag(const TagTarget &target); + VorbisComment *tag(std::size_t index); + std::size_t tagCount() const; + bool removeTag(Tag *tag); + void removeAllTags(); + protected: void internalParseHeader(); void internalParseTags(); @@ -56,15 +41,10 @@ protected: void internalMakeFile(); private: - void ariseComment(std::vector::size_type pageIndex, std::vector::size_type segmentIndex, GeneralMediaFormat mediaFormat = GeneralMediaFormat::Vorbis); + void ariseComment(std::size_t pageIndex, std::size_t segmentIndex, GeneralMediaFormat mediaFormat = GeneralMediaFormat::Vorbis); std::unordered_map >::size_type> m_streamsBySerialNo; - /*! - * \brief Consists of first page index, first segment index, last page index, last segment index and tag index (in this order). - */ - std::list m_commentTable; - OggIterator m_iterator; bool m_validateChecksums; }; diff --git a/ogg/oggiterator.cpp b/ogg/oggiterator.cpp index cd32e7c..12a3dc0 100644 --- a/ogg/oggiterator.cpp +++ b/ogg/oggiterator.cpp @@ -2,6 +2,10 @@ #include "../exceptions.h" +#include + +using namespace std; + namespace Media { /*! @@ -17,10 +21,23 @@ namespace Media { * The internal buffer of OGG pages might be accessed using the pages() method. */ +/*! + * \brief Sets the stream and related parameters and clears all available pages. + * \remarks Invalidates the iterator. Use reset() to continue iteration. + */ +void OggIterator::clear(istream &stream, uint64 startOffset, uint64 streamSize) +{ + m_stream = &stream; + m_startOffset = startOffset; + m_streamSize = streamSize; + m_pages.clear(); +} + /*! * \brief Resets the iterator to point at the first segment of the first page (matching the filter if set). * - * Fetched pages (directly accessable through the page() method) remain after resetting the iterator. + * Fetched pages (directly accessable through the page() method) remain after resetting the iterator. Use + * the clear method to clear all pages. */ void OggIterator::reset() { @@ -36,70 +53,66 @@ void OggIterator::reset() } /*! - * \brief Increases the current position by one page if the iterator is valid; does nothing otherwise. + * \brief Increases the current position by one page. + * \remarks The iterator must be valid. The iterator might be invalidated. */ void OggIterator::nextPage() { - if(*this) { - while(++m_page < m_pages.size() || fetchNextPage()) { - const OggPage &page = m_pages[m_page]; - if(!page.segmentSizes().empty() && matchesFilter(page)) { - // page is not empty and matches ID filter if set - m_segment = m_bytesRead = 0; - m_offset = page.startOffset() + page.headerSize(); - return; - } + while(++m_page < m_pages.size() || fetchNextPage()) { + const OggPage &page = m_pages[m_page]; + if(!page.segmentSizes().empty() && matchesFilter(page)) { + // page is not empty and matches ID filter if set + m_segment = m_bytesRead = 0; + m_offset = page.startOffset() + page.headerSize(); + return; } - // no next page available -> iterator is in invalid state } + // no next page available -> iterator is in invalid state } /*! - * \brief Increases the current position by one segment if the iterator is valid; does nothing otherwise. + * \brief Increases the current position by one segment. + * \remarks The iterator must be valid. The iterator might be invalidated. */ void OggIterator::nextSegment() { - if(*this) { - const OggPage &page = m_pages[m_page]; - if(m_segment + 1 < page.segmentSizes().size() && matchesFilter(page)) { - // current page has next segment - m_bytesRead = 0; - m_offset += page.segmentSizes()[m_segment++]; - } else { - // next (matching) page has next segment - nextPage(); - } + const OggPage &page = m_pages[m_page]; + if(matchesFilter(page) && ++m_segment < page.segmentSizes().size()) { + // current page has next segment + m_bytesRead = 0; + m_offset += page.segmentSizes()[m_segment - 1]; + } else { + // next (matching) page has next segment + nextPage(); } } /*! - * \brief Decreases the current position by one page if the iterator is valid; does nothing otherwise. + * \brief Decreases the current position by one page. + * \remarks The iterator must be valid. The iterator might be invalidated. */ void OggIterator::previousPage() { - if(*this) { - while(m_page > 0) { - const OggPage &page = m_pages[--m_page]; - if(matchesFilter(page)) { - m_offset = page.dataOffset(m_segment = page.segmentSizes().size() - 1); - return; - } + while(m_page) { + const OggPage &page = m_pages[--m_page]; + if(matchesFilter(page)) { + m_offset = page.dataOffset(m_segment = page.segmentSizes().size() - 1); + return; } } } /*! - * \brief Decreases the current position by one segment if the iterator is valid; does nothing otherwise. + * \brief Decreases the current position by one segment. + * \remarks The iterator must be valid. The iterator might be invalidated. */ void OggIterator::previousSegment() { - if(*this) { - const OggPage &page = m_pages[m_page]; - if(m_segment > 0 && matchesFilter(page)) { - m_offset -= page.segmentSizes()[m_segment--]; - } else { - previousPage(); - } + const OggPage &page = m_pages[m_page]; + if(m_segment && matchesFilter(page)) { + m_offset -= page.segmentSizes()[m_segment--]; + } else { + previousPage(); } } @@ -130,7 +143,10 @@ void OggIterator::read(char *buffer, size_t count) count -= available; } } - throw TruncatedDataException(); + if(count) { + // still bytes to read but no more available + throw TruncatedDataException(); + } } /*! @@ -173,9 +189,7 @@ bool OggIterator::fetchNextPage() if(m_page == m_pages.size()) { // can only fetch the next page if the current page is the last page m_offset = m_pages.empty() ? m_startOffset : m_pages.back().startOffset() + m_pages.back().totalSize(); if(m_offset < m_streamSize) { - OggPage page; - page.parseHeader(*m_stream, m_offset, static_cast(m_streamSize - m_offset)); - m_pages.push_back(page); + m_pages.emplace_back(*m_stream, m_offset, static_cast(m_streamSize - m_offset)); return true; } } diff --git a/ogg/oggiterator.h b/ogg/oggiterator.h index 1c814c7..2547ab9 100644 --- a/ogg/oggiterator.h +++ b/ogg/oggiterator.h @@ -13,6 +13,7 @@ class LIB_EXPORT OggIterator public: OggIterator(std::istream &stream, uint64 startOffset, uint64 streamSize); + void clear(std::istream &stream, uint64 startOffset, uint64 streamSize); std::istream &stream(); void setStream(std::istream &stream); uint64 startOffset() const; diff --git a/ogg/oggpage.cpp b/ogg/oggpage.cpp index 1431ed1..7854a9a 100644 --- a/ogg/oggpage.cpp +++ b/ogg/oggpage.cpp @@ -77,7 +77,7 @@ uint32 OggPage::computeChecksum(istream &stream, uint64 startOffset) stream.seekg(startOffset); uint32 crc = 0x0; byte value, segmentTableSize = 0, segmentTableIndex = 0; - for(uint32 i = 0, segmentLength = 27; i < segmentLength; ++i) { + for(uint32 i = 0, segmentLength = 27; i != segmentLength; ++i) { switch(i) { case 22: // bytes 22, 23, 24, 25 hold denoted checksum and must be set to zero diff --git a/ogg/oggpage.h b/ogg/oggpage.h index 8a5d62b..0439016 100644 --- a/ogg/oggpage.h +++ b/ogg/oggpage.h @@ -14,6 +14,7 @@ class LIB_EXPORT OggPage { public: OggPage(); + OggPage(std::istream &stream, uint64 startOffset, int32 maxSize); void parseHeader(std::istream &stream, uint64 startOffset, int32 maxSize); static uint32 computeChecksum(std::istream &stream, uint64 startOffset); @@ -62,6 +63,16 @@ inline OggPage::OggPage() : m_segmentCount(0) {} +/*! + * \brief Constructs a new OggPage and instantly parses the header read from the specified \a stream + * at the specified \a startOffset. + */ +inline OggPage::OggPage(std::istream &stream, uint64 startOffset, int32 maxSize) : + OggPage() +{ + parseHeader(stream, startOffset, maxSize); +} + /*! * \brief Returns the start offset of the page. * diff --git a/ogg/oggstream.cpp b/ogg/oggstream.cpp index 1510e7d..0e55f87 100644 --- a/ogg/oggstream.cpp +++ b/ogg/oggstream.cpp @@ -43,24 +43,29 @@ OggStream::~OggStream() void OggStream::internalParseHeader() { static const string context("parsing OGG page header"); + // read basic information from first page OggIterator &iterator = m_container.m_iterator; const OggPage &firstPage = iterator.pages()[m_startPage]; m_version = firstPage.streamStructureVersion(); m_id = firstPage.streamSerialNumber(); + // ensure iterator is setup properly iterator.setFilter(m_id); iterator.setPageIndex(m_startPage); + // iterate through segments using OggIterator bool hasIdentificationHeader = false; bool hasCommentHeader = false; for(; iterator; ++iterator) { const uint32 currentSize = iterator.currentSegmentSize(); m_size += currentSize; + if(currentSize >= 8) { // determine stream format inputStream().seekg(iterator.currentSegmentOffset()); const uint64 sig = reader().readUInt64BE(); + if((sig & 0x00ffffffffffff00u) == 0x00766F7262697300u) { // Vorbis header detected // set Vorbis as format @@ -124,6 +129,7 @@ void OggStream::internalParseHeader() default: ; } + } else if(sig == 0x4F70757348656164u) { // Opus header detected // set Opus as format @@ -167,6 +173,7 @@ void OggStream::internalParseHeader() } else { addNotification(NotificationType::Critical, "Opus identification header appears more then once. Oversupplied occurrence will be ignored.", context); } + } else if(sig == 0x4F70757354616773u) { // Opus comment detected // set Opus as format @@ -187,6 +194,7 @@ void OggStream::internalParseHeader() } else { addNotification(NotificationType::Critical, "Opus tags/comment header appears more then once. Oversupplied occurrence will be ignored.", context); } + } else if((sig & 0x00ffffffffffff00u) == 0x007468656F726100u) { // Theora header detected // set Theora as format @@ -201,9 +209,10 @@ void OggStream::internalParseHeader() addNotification(NotificationType::Warning, "Stream format is inconsistent.", context); } // TODO: read more information about Theora stream - } // currently only Vorbis, Opus and Theora can be detected - } + } // currently only Vorbis, Opus and Theora can be detected, TODO: detect more formats + } // TODO: reduce code duplication } + if(m_duration.isNull() && m_size && m_bitrate) { // calculate duration from stream size and bitrate, assuming 1 % overhead m_duration = TimeSpan::fromSeconds(static_cast(m_size) / (m_bitrate * 125.0) * 1.1); diff --git a/ogg/oggstream.h b/ogg/oggstream.h index 878fe1c..a4f0d54 100644 --- a/ogg/oggstream.h +++ b/ogg/oggstream.h @@ -19,16 +19,22 @@ public: ~OggStream(); TrackType type() const; + std::size_t startPage() const; protected: void internalParseHeader(); private: - std::vector::size_type m_startPage; + std::size_t m_startPage; OggContainer &m_container; uint32 m_currentSequenceNumber; }; +inline std::size_t OggStream::startPage() const +{ + return m_startPage; +} + inline TrackType OggStream::type() const { return TrackType::OggStream; diff --git a/tagparser.pro b/tagparser.pro index b6f4086..487d8f5 100644 --- a/tagparser.pro +++ b/tagparser.pro @@ -145,7 +145,6 @@ SOURCES += \ vorbis/vorbisidentificationheader.cpp \ opus/opusidentificationheader.cpp \ ogg/oggiterator.cpp \ - vorbis/vorbiscommentids.cpp \ abstractchapter.cpp \ matroska/matroskaeditionentry.cpp \ matroska/matroskachapter.cpp \ diff --git a/tagvalue.cpp b/tagvalue.cpp index 6a6204f..875cd42 100644 --- a/tagvalue.cpp +++ b/tagvalue.cpp @@ -164,35 +164,46 @@ TagValue &TagValue::operator=(const TagValue &other) */ bool TagValue::operator==(const TagValue &other) const { - if(m_type != other.m_type || m_desc != other.m_desc || (!m_desc.empty() && m_descEncoding != other.m_descEncoding) + if(m_desc != other.m_desc || (!m_desc.empty() && m_descEncoding != other.m_descEncoding) || m_mimeType != other.m_mimeType || m_lng != other.m_lng || m_labeledAsReadonly != other.m_labeledAsReadonly) { return false; } - switch(m_type) { - case TagDataType::Text: - if(m_size != other.m_size && m_encoding != other.m_encoding) { + if(m_type == other.m_type) { + switch(m_type) { + case TagDataType::Text: + if(m_size != other.m_size && m_encoding != other.m_encoding) { + return false; + } + return strncmp(m_ptr.get(), other.m_ptr.get(), m_size) == 0; + case TagDataType::PositionInSet: + return toPositionInSet() == other.toPositionInSet(); + case TagDataType::Integer: + return toInteger() == other.toInteger(); + case TagDataType::StandardGenreIndex: + return toStandardGenreIndex() == other.toStandardGenreIndex(); + case TagDataType::TimeSpan: + return toTimeSpan() == other.toTimeSpan(); + case TagDataType::DateTime: + return toDateTime() == other.toDateTime(); + case TagDataType::Picture: + case TagDataType::Binary: + case TagDataType::Undefined: + if(m_size != other.m_size) { + return false; + } + return strncmp(m_ptr.get(), other.m_ptr.get(), m_size) == 0; + default: return false; } - return strncmp(m_ptr.get(), other.m_ptr.get(), m_size) == 0; - case TagDataType::PositionInSet: - return toPositionInSet() == other.toPositionInSet(); - case TagDataType::Integer: - return toInteger() == other.toInteger(); - case TagDataType::StandardGenreIndex: - return toStandardGenreIndex() == other.toStandardGenreIndex(); - case TagDataType::TimeSpan: - return toTimeSpan() == other.toTimeSpan(); - case TagDataType::DateTime: - return toDateTime() == other.toDateTime(); - case TagDataType::Picture: - case TagDataType::Binary: - case TagDataType::Undefined: - if(m_size != other.m_size) { + } else { + // different types + try { + // try to convert both values to string + // if the string representations are equal, both values can also be considered equal + return toString() == other.toString(); + } catch(const ConversionException &) { return false; } - return strncmp(m_ptr.get(), other.m_ptr.get(), m_size) == 0; - default: - return false; } } diff --git a/vorbis/vorbiscomment.cpp b/vorbis/vorbiscomment.cpp index 6871327..3a9a69d 100644 --- a/vorbis/vorbiscomment.cpp +++ b/vorbis/vorbiscomment.cpp @@ -59,7 +59,7 @@ string VorbisComment::fieldId(KnownField field) const case KnownField::DiskPosition: return diskNumber(); case KnownField::PartNumber: return partNumber(); case KnownField::Composer: return composer(); - case KnownField::Encoder: return encodedBy(); + case KnownField::Encoder: return encoder(); case KnownField::EncoderSettings: return encoderSettings(); case KnownField::Description: return description(); case KnownField::RecordLabel: return label(); @@ -73,7 +73,7 @@ string VorbisComment::fieldId(KnownField field) const KnownField VorbisComment::knownField(const string &id) const { using namespace VorbisCommentIds; - static const map map({ + static const map fieldMap({ {album(), KnownField::Album}, {artist(), KnownField::Artist}, {comment(), KnownField::Comment}, @@ -85,7 +85,7 @@ KnownField VorbisComment::knownField(const string &id) const {diskNumber(), KnownField::DiskPosition}, {partNumber(), KnownField::PartNumber}, {composer(), KnownField::Composer}, - {encodedBy(), KnownField::Encoder}, + {encoder(), KnownField::Encoder}, {encoderSettings(), KnownField::EncoderSettings}, {description(), KnownField::Description}, {label(), KnownField::RecordLabel}, @@ -93,7 +93,7 @@ KnownField VorbisComment::knownField(const string &id) const {lyricist(), KnownField::Lyricist} }); try { - return map.at(id); + return fieldMap.at(id); } catch(out_of_range &) { return KnownField::Invalid; } @@ -181,7 +181,7 @@ void VorbisComment::make(std::ostream &stream, bool noSignature) string vendor; try { m_vendor.toString(vendor); - } catch(ConversionException &) { + } catch(const ConversionException &) { addNotification(NotificationType::Warning, "Can not convert the assigned vendor to string.", context); } BinaryWriter writer(&stream); diff --git a/vorbis/vorbiscomment.h b/vorbis/vorbiscomment.h index f99500d..9ceffe9 100644 --- a/vorbis/vorbiscomment.h +++ b/vorbis/vorbiscomment.h @@ -5,13 +5,56 @@ #include "../caseinsensitivecomparer.h" #include "../fieldbasedtag.h" +#include "../mediaformat.h" namespace Media { class OggIterator; +class VorbisComment; + +/*! + * \brief The OggParameter struct holds the OGG parameter for a VorbisComment. + */ +struct LIB_EXPORT OggParameter +{ + OggParameter(); + void set(std::size_t pageIndex, std::size_t segmentIndex, GeneralMediaFormat streamFormat = GeneralMediaFormat::Vorbis); + + std::size_t firstPageIndex; + std::size_t firstSegmentIndex; + std::size_t lastPageIndex; + std::size_t lastSegmentIndex; + GeneralMediaFormat streamFormat; + bool removed; +}; + +/*! + * \brief Creates new parameters. + * \remarks The OggContainer class is responsible for assigning sane values. + */ +inline OggParameter::OggParameter() : + firstPageIndex(0), + firstSegmentIndex(0), + lastPageIndex(0), + lastSegmentIndex(0), + streamFormat(GeneralMediaFormat::Vorbis), // default to Vorbis here + removed(false) +{} + +/*! + * \brief Sets firstPageIndex/lastPageIndex, firstSegmentIndex/lastSegmentIndex and streamFormat. + */ +inline void OggParameter::set(std::size_t pageIndex, std::size_t segmentIndex, GeneralMediaFormat streamFormat) +{ + firstPageIndex = lastPageIndex = pageIndex; + firstSegmentIndex = lastSegmentIndex = segmentIndex; + this->streamFormat = streamFormat; +} class LIB_EXPORT VorbisComment : public FieldMapBasedTag { + friend class OggContainer; + public: VorbisComment(); @@ -30,9 +73,12 @@ public: const TagValue &vendor() const; void setVendor(const TagValue &vendor); + OggParameter &oggParams(); + const OggParameter &oggParams() const; private: TagValue m_vendor; + OggParameter m_oggParams; }; /*! @@ -48,7 +94,15 @@ inline TagType VorbisComment::type() const inline const char *VorbisComment::typeName() const { - return "Vorbis comment"; + switch(m_oggParams.streamFormat) { + case GeneralMediaFormat::Opus: + return "Opus comment"; + case GeneralMediaFormat::Theora: + return "Theora comment"; + default: + // just assume Vorbis otherwise + return "Vorbis comment"; + } } inline TagTextEncoding VorbisComment::proposedTextEncoding() const @@ -61,16 +115,46 @@ inline bool VorbisComment::canEncodingBeUsed(TagTextEncoding encoding) const return encoding == TagTextEncoding::Utf8; } +/*! + * \brief Returns the vendor. + * \remarks Also accessable via value(KnownField::Vendor). + */ inline const TagValue &VorbisComment::vendor() const { return m_vendor; } +/*! + * \brief Returns the vendor. + * \remarks Also accessable via setValue(KnownField::Vendor, vendor). + */ inline void VorbisComment::setVendor(const TagValue &vendor) { m_vendor = vendor; } +/*! + * \brief Returns the OGG parameter for the comment. + * + * Consists of first page index, first segment index, last page index, last segment index and tag index (in this order). + * These values are used and managed by the OggContainer class and do not affect the behavior of the VorbisComment instance. + */ +inline OggParameter &VorbisComment::oggParams() +{ + return m_oggParams; +} + +/*! + * \brief Returns the OGG parameter for the comment. + * + * Consists of first page index, first segment index, last page index, last segment index and tag index (in this order). + * These values are used and managed by the OggContainer class and do not affect the behavior of the VorbisComment instance. + */ +inline const OggParameter &VorbisComment::oggParams() const +{ + return m_oggParams; +} + } #endif // MEDIA_VORBISCOMMENT_H diff --git a/vorbis/vorbiscommentids.cpp b/vorbis/vorbiscommentids.cpp deleted file mode 100644 index 276819a..0000000 --- a/vorbis/vorbiscommentids.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "./vorbiscommentids.h" - -namespace Media { - -namespace VorbisCommentIds { - -} - -} diff --git a/vorbis/vorbiscommentids.h b/vorbis/vorbiscommentids.h index 0c353ac..078a83d 100644 --- a/vorbis/vorbiscommentids.h +++ b/vorbis/vorbiscommentids.h @@ -64,8 +64,8 @@ inline LIB_EXPORT const char *author() { inline LIB_EXPORT const char *conductor() { return "CONDUCTOR"; } -inline LIB_EXPORT const char *encodedBy() { - return "ENCODED-BY"; +inline LIB_EXPORT const char *encoder() { + return "ENCODER"; } inline LIB_EXPORT const char *publisher() { return "PUBLISHER";