fixed some OGG/Vorbis related issues

This commit is contained in:
Martchus 2016-03-22 22:52:36 +01:00
parent 4a43df7d8f
commit b8e8dcd778
17 changed files with 466 additions and 190 deletions

View File

@ -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

View File

@ -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.

View File

@ -3,6 +3,8 @@
#include "./abstractcontainer.h"
#include <c++utilities/misc/memory.h>
#include <algorithm>
#include <memory>
#include <vector>
@ -202,7 +204,7 @@ inline const std::vector<std::unique_ptr<TagType> > &GenericContainer<FileInfoTy
*
* The tags need to be parsed before (see parseTags()).
*
* The container keeps ownership over the returned tags.
* The container keeps ownership over the returned tags. Do not push or remove elements to the returned vector.
*
* \sa areTagsParsed()
*/
@ -232,7 +234,7 @@ inline const std::vector<std::unique_ptr<TrackType> > &GenericContainer<FileInfo
*
* The tags need to be parsed before (see parseTracks()).
*
* The container keeps ownership over the returned tracks.
* The container keeps ownership over the returned tracks. Do not push or remove elements to the returned vector.
*
* \sa areTracksParsed()
*/
@ -243,7 +245,7 @@ inline std::vector<std::unique_ptr<TrackType> > &GenericContainer<FileInfoType,
}
template <class FileInfoType, class TagType, class TrackType, class ElementType>
inline TagType *GenericContainer<FileInfoType, TagType, TrackType, ElementType>::createTag(const TagTarget &target)
TagType *GenericContainer<FileInfoType, TagType, TrackType, ElementType>::createTag(const TagTarget &target)
{
if(!target.isEmpty()) {
for(auto &tag : m_tags) {
@ -254,7 +256,7 @@ inline TagType *GenericContainer<FileInfoType, TagType, TrackType, ElementType>:
} else if(!m_tags.empty()) {
return m_tags.front().get();
}
m_tags.emplace_back(new TagType());
m_tags.emplace_back(std::make_unique<TagType>());
auto &tag = m_tags.back();
tag->setTarget(target);
return tag.get();
@ -308,7 +310,7 @@ bool GenericContainer<FileInfoType, TagType, TrackType, ElementType>::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<void *>(i->get()) == static_cast<void *>(track)) {
if(static_cast<AbstractTrack *>(i->get()) == track) {
i->release();
m_tracks.erase(i);
removed = true;

View File

@ -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<size_t>(-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<Tag *>(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 &params = 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<OggPage>::size_type pageIndex, vector<uint32>::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<VorbisComment>());
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> &copyHelper, vector<uint32> &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<uint64> updatedPageOffsets;
uint32 pageSequenceNumber = 0;
unordered_map<uint32, uint32> 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 &currentPage = m_iterator.currentPage();
auto pageSize = currentPage.totalSize();
const OggPage &currentPage = 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<size_t>(-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);

View File

@ -16,27 +16,6 @@ namespace Media {
class MediaFileInfo;
struct LIB_EXPORT VorbisCommentInfo
{
VorbisCommentInfo(std::vector<OggPage>::size_type firstPageIndex, std::vector<OggPage>::size_type firstSegmentIndex, std::vector<OggPage>::size_type tagIndex, GeneralMediaFormat streamFormat = GeneralMediaFormat::Vorbis);
std::vector<OggPage>::size_type firstPageIndex;
std::vector<OggPage>::size_type firstSegmentIndex;
std::vector<OggPage>::size_type lastPageIndex;
std::vector<OggPage>::size_type lastSegmentIndex;
std::vector<std::unique_ptr<VorbisComment> >::size_type tagIndex;
GeneralMediaFormat streamFormat;
};
inline VorbisCommentInfo::VorbisCommentInfo(std::vector<OggPage>::size_type firstPageIndex, std::vector<OggPage>::size_type firstSegmentIndex, std::vector<OggPage>::size_type tagIndex, GeneralMediaFormat mediaFormat) :
firstPageIndex(firstPageIndex),
firstSegmentIndex(firstSegmentIndex),
lastPageIndex(0),
lastSegmentIndex(0),
tagIndex(tagIndex),
streamFormat(mediaFormat)
{}
class LIB_EXPORT OggContainer : public GenericContainer<MediaFileInfo, VorbisComment, OggStream, OggPage>
{
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<OggPage>::size_type pageIndex, std::vector<uint32>::size_type segmentIndex, GeneralMediaFormat mediaFormat = GeneralMediaFormat::Vorbis);
void ariseComment(std::size_t pageIndex, std::size_t segmentIndex, GeneralMediaFormat mediaFormat = GeneralMediaFormat::Vorbis);
std::unordered_map<uint32, std::vector<std::unique_ptr<OggStream> >::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<VorbisCommentInfo> m_commentTable;
OggIterator m_iterator;
bool m_validateChecksums;
};

View File

@ -2,6 +2,10 @@
#include "../exceptions.h"
#include <iostream>
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<int32>(m_streamSize - m_offset));
m_pages.push_back(page);
m_pages.emplace_back(*m_stream, m_offset, static_cast<int32>(m_streamSize - m_offset));
return true;
}
}

View File

@ -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;

View File

@ -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

View File

@ -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.
*

View File

@ -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<double>(m_size) / (m_bitrate * 125.0) * 1.1);

View File

@ -19,16 +19,22 @@ public:
~OggStream();
TrackType type() const;
std::size_t startPage() const;
protected:
void internalParseHeader();
private:
std::vector<OggPage>::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;

View File

@ -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 \

View File

@ -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;
}
}

View File

@ -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<string, KnownField> map({
static const map<string, KnownField> 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);

View File

@ -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<VorbisCommentField, CaseInsensitiveStringComparer>
{
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

View File

@ -1,9 +0,0 @@
#include "./vorbiscommentids.h"
namespace Media {
namespace VorbisCommentIds {
}
}

View File

@ -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";