297 lines
15 KiB
C++
297 lines
15 KiB
C++
#include "./oggcontainer.h"
|
|
|
|
#include "../mediafileinfo.h"
|
|
#include "../backuphelper.h"
|
|
|
|
#include <c++utilities/io/copy.h>
|
|
#include <c++utilities/misc/memory.h>
|
|
|
|
using namespace std;
|
|
using namespace IoUtilities;
|
|
|
|
namespace Media {
|
|
|
|
/*!
|
|
* \class Media::OggContainer
|
|
* \brief Implementation of Media::AbstractContainer for OGG files.
|
|
*/
|
|
|
|
/*!
|
|
* \brief Constructs a new container for the specified \a stream at the specified \a startOffset.
|
|
*/
|
|
OggContainer::OggContainer(MediaFileInfo &fileInfo, uint64 startOffset) :
|
|
GenericContainer<MediaFileInfo, VorbisComment, OggStream, OggPage>(fileInfo, startOffset),//AbstractContainer(stream, startOffset)
|
|
m_iterator(fileInfo.stream(), startOffset, fileInfo.size()),
|
|
m_validateChecksums(false)
|
|
{}
|
|
|
|
OggContainer::~OggContainer()
|
|
{}
|
|
|
|
void OggContainer::reset()
|
|
{
|
|
m_commentTable.clear();
|
|
m_iterator.reset();
|
|
}
|
|
|
|
void OggContainer::internalParseHeader()
|
|
{
|
|
static const string context("parsing OGG bitstream header");
|
|
// iterate through pages using OggIterator helper class
|
|
try {
|
|
// ensure iterator is setup properly
|
|
for(m_iterator.removeFilter(), m_iterator.reset(); m_iterator; m_iterator.nextPage()) {
|
|
const OggPage &page = m_iterator.currentPage();
|
|
if(m_validateChecksums) {
|
|
if(page.checksum() != OggPage::computeChecksum(stream(), page.startOffset())) {
|
|
addNotification(NotificationType::Warning, "The denoted checksum of the OGG page at " + ConversionUtilities::numberToString(m_iterator.currentSegmentOffset()) + " does not match the computed checksum.", context);
|
|
}
|
|
}
|
|
OggStream *stream;
|
|
try {
|
|
stream = m_tracks[m_streamsBySerialNo.at(page.streamSerialNumber())].get();
|
|
} catch(const out_of_range &) {
|
|
// new stream serial number recognized -> add new stream
|
|
m_streamsBySerialNo[page.streamSerialNumber()] = m_tracks.size();
|
|
m_tracks.emplace_back(make_unique<OggStream>(*this, m_iterator.currentPageIndex()));
|
|
stream = m_tracks.back().get();
|
|
}
|
|
if(stream->m_currentSequenceNumber != page.sequenceNumber()) {
|
|
if(stream->m_currentSequenceNumber) {
|
|
addNotification(NotificationType::Warning, "Page is missing (page sequence number omitted).", context);
|
|
}
|
|
stream->m_currentSequenceNumber = page.sequenceNumber() + 1;
|
|
} else {
|
|
++stream->m_currentSequenceNumber;
|
|
}
|
|
}
|
|
} 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) {
|
|
case GeneralMediaFormat::Vorbis:
|
|
m_tags[vorbisCommentInfo.tagIndex]->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);
|
|
break;
|
|
default:
|
|
addNotification(NotificationType::Critical, "Stream format not supported.", "parsing tags from OGG streams");
|
|
}
|
|
vorbisCommentInfo.lastPageIndex = m_iterator.currentPageIndex();
|
|
vorbisCommentInfo.lastSegmentIndex = m_iterator.currentSegmentIndex();
|
|
}
|
|
}
|
|
|
|
void OggContainer::ariseComment(vector<OggPage>::size_type pageIndex, vector<uint32>::size_type segmentIndex, GeneralMediaFormat mediaFormat)
|
|
{
|
|
m_commentTable.emplace_back(pageIndex, segmentIndex, m_tags.size(), mediaFormat);
|
|
m_tags.emplace_back(make_unique<VorbisComment>());
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void OggContainer::internalMakeFile()
|
|
{
|
|
const string context("making OGG file");
|
|
updateStatus("Prepare for rewriting OGG file ...");
|
|
parseTags(); // tags need to be parsed before the file can be rewritten
|
|
fileInfo().close();
|
|
string backupPath;
|
|
fstream backupStream;
|
|
try {
|
|
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();
|
|
vector<uint64> updatedPageOffsets;
|
|
uint32 pageSequenceNumber = 0;
|
|
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();
|
|
// 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
|
|
&& !currentPage.segmentSizes().empty()) {
|
|
// page needs to be rewritten (not just copied)
|
|
// -> write segments to a buffer first
|
|
stringstream buffer(ios_base::in | ios_base::out | ios_base::binary);
|
|
vector<uint32> newSegmentSizes;
|
|
newSegmentSizes.reserve(currentPage.segmentSizes().size());
|
|
uint64 segmentOffset = m_iterator.currentSegmentOffset();
|
|
vector<uint32>::size_type segmentIndex = 0;
|
|
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() > commentTableIterator->lastPageIndex
|
|
|| (m_iterator.currentPageIndex() == commentTableIterator->lastPageIndex && segmentIndex > commentTableIterator->lastSegmentIndex)) {
|
|
++commentTableIterator;
|
|
}
|
|
} else {
|
|
// copy other segments unchanged
|
|
backupStream.seekg(segmentOffset);
|
|
copy.copy(backupStream, buffer, segmentSize);
|
|
newSegmentSizes.push_back(segmentSize);
|
|
}
|
|
segmentOffset += segmentSize;
|
|
}
|
|
++segmentIndex;
|
|
}
|
|
// write buffered data to actual stream
|
|
auto newSegmentSizesIterator = newSegmentSizes.cbegin(), newSegmentSizesEnd = newSegmentSizes.cend();
|
|
bool continuePreviousSegment = false;
|
|
if(newSegmentSizesIterator != newSegmentSizesEnd) {
|
|
uint32 bytesLeft = *newSegmentSizesIterator;
|
|
// write pages until all data in the buffer is written
|
|
while(newSegmentSizesIterator != newSegmentSizesEnd) {
|
|
// 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
|
|
// set continue flag
|
|
stream().seekp(-22, ios_base::cur);
|
|
stream().put(currentPage.headerTypeFlag() & (continuePreviousSegment ? 0xFF : 0xFE));
|
|
continuePreviousSegment = true;
|
|
// adjust page sequence number
|
|
stream().seekp(12, ios_base::cur);
|
|
writer().writeUInt32LE(pageSequenceNumber);
|
|
stream().seekp(5, ios_base::cur);
|
|
int16 segmentSizesWritten = 0; // in the current page header only
|
|
// 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 >= 0xFF && segmentSizesWritten < 0xFF) {
|
|
stream().put(0xFF);
|
|
currentSize += 0xFF;
|
|
bytesLeft -= 0xFF;
|
|
++segmentSizesWritten;
|
|
}
|
|
if(bytesLeft > 0 && segmentSizesWritten < 0xFF) {
|
|
// bytes left is here < 0xFF
|
|
stream().put(bytesLeft);
|
|
currentSize += bytesLeft;
|
|
bytesLeft = 0;
|
|
++segmentSizesWritten;
|
|
}
|
|
if(bytesLeft == 0) {
|
|
// sizes for the segment have been written
|
|
// -> continue with next segment
|
|
if(++newSegmentSizesIterator != newSegmentSizesEnd) {
|
|
bytesLeft = *newSegmentSizesIterator;
|
|
continuePreviousSegment = false;
|
|
}
|
|
}
|
|
}
|
|
// there are no bytes left in the current segment; remove continue flag
|
|
if(bytesLeft == 0) {
|
|
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
|
|
stream().seekp(-1 - segmentSizesWritten, ios_base::cur);
|
|
stream().put(segmentSizesWritten);
|
|
stream().seekp(segmentSizesWritten, ios_base::cur);
|
|
// -> write actual page data
|
|
copy.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);
|
|
stream().seekp(-9, ios_base::cur);
|
|
writer().writeUInt32LE(pageSequenceNumber);
|
|
stream().seekp(5, ios_base::cur);
|
|
copy.copy(backupStream, stream(), pageSize - 27);
|
|
} else {
|
|
// copy page unchanged
|
|
backupStream.seekg(currentPage.startOffset());
|
|
copy.copy(backupStream, stream(), pageSize);
|
|
}
|
|
++pageSequenceNumber;
|
|
}
|
|
}
|
|
// close backups stream; reopen new file as readable stream
|
|
backupStream.close();
|
|
fileInfo().close();
|
|
fileInfo().open();
|
|
// update checksums of modified pages
|
|
for(auto offset : updatedPageOffsets) {
|
|
OggPage::updateChecksum(fileInfo().stream(), offset);
|
|
}
|
|
// clear iterator
|
|
m_iterator = OggIterator(fileInfo().stream(), startOffset(), fileInfo().size());
|
|
} catch(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(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);
|
|
m_iterator.setStream(fileInfo().stream());
|
|
throw;
|
|
}
|
|
}
|
|
|
|
}
|