Tag Parser 12.5.3
C++ library for reading and writing MP4 (iTunes), ID3, Vorbis, Opus, FLAC and Matroska tags
Loading...
Searching...
No Matches
oggcontainer.cpp
Go to the documentation of this file.
1#include "./oggcontainer.h"
2
4
5#include "../backuphelper.h"
6#include "../mediafileinfo.h"
8#include "../tagtarget.h"
9
10#include <c++utilities/conversion/stringbuilder.h>
11#include <c++utilities/io/copy.h>
12
13#include <limits>
14#include <memory>
15
16using namespace std;
17using namespace CppUtilities;
18
19namespace TagParser {
20
25
26std::string_view OggVorbisComment::typeName() const
27{
28 switch (m_oggParams.streamFormat) {
30 return "Vorbis comment (in FLAC stream)";
32 return "Vorbis comment (in Opus stream)";
34 return "Vorbis comment (in Theora stream)";
35 default:
36 return "Vorbis comment";
37 }
38}
39
44
54
58
60{
61 m_iterator.reset();
62}
63
75{
76 if (!target.tracks().empty()) {
77 // return the tag for the first matching track ID
78 for (auto &tag : m_tags) {
79 if (!tag->target().tracks().empty() && tag->target().tracks().front() == target.tracks().front() && !tag->oggParams().removed) {
80 return tag.get();
81 }
82 }
83 // not tag found -> try to re-use a tag which has been flagged as removed
84 for (auto &tag : m_tags) {
85 if (!tag->target().tracks().empty() && tag->target().tracks().front() == target.tracks().front()) {
86 tag->oggParams().removed = false;
87 return tag.get();
88 }
89 }
90 } else if (OggVorbisComment *comment = tag(0)) {
91 // no track ID specified -> just return the first tag (if one exists)
92 return comment;
93 } else if (!m_tags.empty()) {
94 // no track ID specified -> just return the first tag (try to re-use a tag which has been flagged as removed)
95 m_tags.front()->oggParams().removed = false;
96 return m_tags.front().get();
97 }
98
99 // a new tag needs to be created
100 // -> determine an appropriate track for the tag
101 // -> just use the first Vorbis/Opus track
102 for (const auto &track : m_tracks) {
103 if (target.tracks().empty() || target.tracks().front() == track->id()) {
104 const auto format = track->format().general;
105 if (format == GeneralMediaFormat::Vorbis || format == GeneralMediaFormat::Opus) {
106 // check whether start page has a valid value
107 if (track->startPage() < m_iterator.pages().size()) {
108 announceComment(track->startPage(), numeric_limits<size_t>::max(), false, track->format().general);
109 m_tags.back()->setTarget(target);
110 return m_tags.back().get();
111 } else {
112 // TODO: error handling?
113 }
114 }
115 // TODO: allow adding tags to FLAC tracks (not really important, because a tag should always be present)
116 }
117 }
118 return nullptr;
119}
120
122{
123 auto i = std::size_t();
124 for (const auto &tag : m_tags) {
125 if (!tag->oggParams().removed) {
126 if (index == i) {
127 return tag.get();
128 }
129 ++i;
130 }
131 }
132 return nullptr;
133}
134
136{
137 auto count = std::size_t();
138 for (const auto &tag : m_tags) {
139 if (!tag->oggParams().removed) {
140 ++count;
141 }
142 }
143 return count;
144}
145
156{
157 for (auto &existingTag : m_tags) {
158 if (static_cast<Tag *>(existingTag.get()) == tag) {
159 existingTag->removeAllFields();
160 existingTag->oggParams().removed = true;
161 return true;
162 }
163 }
164 return false;
165}
166
178{
179 for (auto &existingTag : m_tags) {
180 existingTag->removeAllFields();
181 existingTag->oggParams().removed = true;
182 }
183}
184
186{
187 CPP_UTILITIES_UNUSED(progress)
188
189 static const auto context = std::string("parsing Ogg bitstream header");
190 auto pagesSkipped = false, continueFromHere = false;
191
192 // iterate through pages using OggIterator helper class
193 try {
194 // ensure iterator is setup properly
195 for (m_iterator.removeFilter(), m_iterator.reset(); m_iterator;
196 continueFromHere ? [&] { continueFromHere = false; }() : m_iterator.nextPage()) {
197 progress.stopIfAborted();
198 const OggPage &page = m_iterator.currentPage();
199 if (m_validateChecksums && page.checksum() != OggPage::computeChecksum(stream(), page.startOffset())) {
200 diag.emplace_back(DiagLevel::Warning,
201 argsToString(
202 "The denoted checksum of the Ogg page at ", m_iterator.currentSegmentOffset(), " does not match the computed checksum."),
203 context);
204 }
206 auto lastNewStreamOffset = std::uint64_t();
207 if (const auto streamIndex = m_streamsBySerialNo.find(page.streamSerialNumber()); streamIndex != m_streamsBySerialNo.end()) {
208 stream = m_tracks[streamIndex->second].get();
209 } else {
210 // new stream serial number recognized -> add new stream
211 m_streamsBySerialNo[page.streamSerialNumber()] = m_tracks.size();
212 stream = m_tracks.emplace_back(make_unique<OggStream>(*this, m_iterator.currentPageIndex())).get();
213 lastNewStreamOffset = page.startOffset();
214 }
215 if (!pagesSkipped) {
216 stream->m_size += page.dataSize();
217 }
218 if (stream->m_currentSequenceNumber != page.sequenceNumber()) {
219 if (stream->m_currentSequenceNumber) {
220 diag.emplace_back(DiagLevel::Warning,
221 argsToString("Page of stream ", page.streamSerialNumber(), " missing; page sequence number ", stream->m_currentSequenceNumber,
222 " omitted at ", page.startOffset(), ", found ", page.sequenceNumber(), " instead."),
223 context);
224 }
225 stream->m_currentSequenceNumber = page.sequenceNumber() + 1;
226 } else {
227 ++stream->m_currentSequenceNumber;
228 }
229
230 // skip pages in the middle of a big file (still more than 100 MiB to parse) if no new track has been seen since the last 20 MiB
231 if (!fileInfo().isForcingFullParse() && (fileInfo().size() - page.startOffset()) > (100 * 0x100000)
232 && (page.startOffset() - lastNewStreamOffset) > (20 * 0x100000)) {
233 if (m_iterator.resyncAt(fileInfo().size() - (20 * 0x100000))) {
234 const OggPage &resyncedPage = m_iterator.currentPage();
235 // prevent warning about missing pages by resetting the sequence number of all streams and invalidate the stream size
236 for (auto &trackStream : m_tracks) {
237 trackStream->m_currentSequenceNumber = 0;
238 trackStream->m_size = 0;
239 }
240 pagesSkipped = continueFromHere = true;
241 diag.emplace_back(DiagLevel::Information,
242 argsToString("Pages in the middle of the file (", dataSizeToString(resyncedPage.startOffset() - page.startOffset()),
243 ") have been skipped to improve parsing speed. Hence track sizes can not be computed. Maybe not even all tracks could be "
244 "detected. Force a full parse to prevent this."),
245 context);
246 } else {
247 // abort if skipping pages didn't work
248 diag.emplace_back(DiagLevel::Critical,
249 "Unable to re-sync after skipping Ogg pages in the middle of the file. Try forcing a full parse.", context);
250 return;
251 }
252 }
253 }
254 } catch (const TruncatedDataException &) {
255 // thrown when page exceeds max size
256 diag.emplace_back(DiagLevel::Critical, "The Ogg file is truncated.", context);
257 } catch (const InvalidDataException &) {
258 // thrown when first 4 byte do not match capture pattern
259 const auto expectedOffset = m_iterator.currentSegmentOffset();
260 diag.emplace_back(DiagLevel::Critical, argsToString("Capture pattern \"OggS\" at ", expectedOffset, " expected."), context);
261 if (m_iterator.resyncAt(expectedOffset)) {
262 diag.emplace_back(DiagLevel::Warning,
263 argsToString("Found next capture pattern \"OggS\" at ", m_iterator.currentPageOffset(), ". Skipped ",
264 m_iterator.currentPageOffset() - expectedOffset, " invalid bytes."),
265 context);
266 continueFromHere = true;
267 } else {
268 diag.emplace_back(DiagLevel::Critical,
269 argsToString(
270 "Aborting after not being able to find any \"OggS\" capture patterns within 65307 bytes (from offset ", expectedOffset, ")."),
271 context);
272 }
273 }
274}
275
277{
278 // tracks needs to be parsed before because tags are stored at stream level
279 parseTracks(diag, progress);
280 auto flags = VorbisCommentFlags::None;
281 if (fileInfo().fileHandlingFlags() & MediaFileHandlingFlags::ConvertTotalFields) {
283 }
284 for (auto &comment : m_tags) {
285 OggParameter &params = comment->oggParams();
286 m_iterator.setPageIndex(params.firstPageIndex);
287 m_iterator.setSegmentIndex(params.firstSegmentIndex);
288 m_iterator.setFilter(m_iterator.currentPage().streamSerialNumber());
289 const auto startOffset = m_iterator.startOffset();
290 const auto context = argsToString("parsing tag in Ogg page at ", startOffset);
291 auto padding = std::uint64_t();
292 switch (params.streamFormat) {
294 comment->parse(m_iterator, flags, padding, diag);
295 break;
297 // skip header (has already been detected by OggStream)
298 m_iterator.ignore(8);
299 comment->parse(m_iterator, flags | VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, padding, diag);
300 break;
302 m_iterator.ignore(4);
303 comment->parse(m_iterator, flags | VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, padding, diag);
304 break;
305 default:
306 diag.emplace_back(DiagLevel::Critical, "Stream format not supported.", context);
307 }
308 params.lastPageIndex = m_iterator.currentPageIndex();
309 params.lastSegmentIndex = m_iterator.currentSegmentIndex();
310 fileInfo().reportPaddingSizeChanged(fileInfo().paddingSize() + padding);
311
312 // do a few sanity checks on the continued-flag and absolute granule position as some Ogg demuxers are picky about them
313 static constexpr auto noPacketsFinishOnPage = std::numeric_limits<std::uint64_t>::max();
314 if (params.firstPageIndex != params.lastPageIndex) {
315 const auto pageCount = params.lastPageIndex - params.firstPageIndex;
316 for (auto i = params.firstPageIndex; i < params.lastPageIndex; ++i) {
317 if (const auto &page = m_iterator.pages()[i]; page.absoluteGranulePosition() != noPacketsFinishOnPage) {
318 diag.emplace_back(DiagLevel::Warning,
319 argsToString("Tag spans over ", pageCount, " pages but absolute granule position of unfinished page at ", page.startOffset(),
320 " is not set to \"-1\" (it is ", page.absoluteGranulePosition(), ")."),
321 context);
322 }
323 }
324 for (auto i = params.firstPageIndex + 1; i <= params.lastPageIndex; ++i) {
325 if (const auto &page = m_iterator.pages()[i]; !page.isContinued()) {
326 diag.emplace_back(DiagLevel::Warning,
327 argsToString("The tag is continued in Ogg page at ", page.startOffset(), " but this page is not marked as continued packet."),
328 context);
329 }
330 }
331 }
332 if (const auto &page = m_iterator.pages()[params.lastPageIndex]; page.absoluteGranulePosition() == noPacketsFinishOnPage) {
333 diag.emplace_back(
334 DiagLevel::Warning, argsToString("Absolute granule position of final page at ", page.startOffset(), " is set to \"-1\"."), context);
335 }
336 }
337}
338
350void OggContainer::announceComment(std::size_t pageIndex, std::size_t segmentIndex, bool lastMetaDataBlock, GeneralMediaFormat mediaFormat)
351{
352 auto &tag = m_tags.emplace_back(make_unique<OggVorbisComment>());
353 tag->oggParams().set(pageIndex, segmentIndex, lastMetaDataBlock, mediaFormat);
354 tag->target().tracks().emplace_back(m_iterator.pages()[pageIndex].streamSerialNumber());
355}
356
358{
359 static const string context("parsing Ogg stream");
360 for (auto &stream : m_tracks) {
361 if (progress.isAborted()) {
363 }
364 try { // try to parse header
365 stream->parseHeader(diag, progress);
366 if (stream->duration() > m_duration) {
367 m_duration = stream->duration();
368 }
369 } catch (const Failure &) {
370 diag.emplace_back(DiagLevel::Critical, argsToString("Unable to parse stream at ", stream->startOffset(), '.'), context);
371 }
372 }
373}
374
379void OggContainer::makeVorbisCommentSegment(stringstream &buffer, CopyHelper<65307> &copyHelper, vector<std::uint32_t> &newSegmentSizes,
380 VorbisComment *comment, OggParameter *params, Diagnostics &diag)
381{
382 const auto offset = buffer.tellp();
383 switch (params->streamFormat) {
385 comment->make(buffer, VorbisCommentFlags::None, diag);
386 break;
388 BE::getBytes(static_cast<std::uint64_t>(0x4F70757354616773u), copyHelper.buffer());
389 buffer.write(copyHelper.buffer(), 8);
391 break;
393 // Vorbis comment must be wrapped in "METADATA_BLOCK_HEADER"
395 header.setLast(params->lastMetaDataBlock);
397
398 // write the header later, when the size is known
399 buffer.write(copyHelper.buffer(), 4);
400
402
403 // finally make the header
404 header.setDataSize(static_cast<std::uint32_t>(buffer.tellp() - offset - 4));
405 if (header.dataSize() > 0xFFFFFF) {
406 diag.emplace_back(
407 DiagLevel::Critical, "Size of Vorbis comment exceeds size limit for FLAC \"METADATA_BLOCK_HEADER\".", "making Vorbis Comment");
408 }
409 buffer.seekp(offset);
410 header.makeHeader(buffer);
411 buffer.seekp(header.dataSize(), ios_base::cur);
412 break;
413 }
414 default:;
415 }
416 MediaFileInfo::writePadding(buffer, fileInfo().preferredPadding());
417
418 newSegmentSizes.push_back(static_cast<std::uint32_t>(buffer.tellp() - offset));
419}
420
422{
423 const auto context = std::string("making Ogg file");
424 progress.nextStepOrStop("Prepare for rewriting Ogg file ...");
425 parseTags(diag, progress); // tags need to be parsed before the file can be rewritten
426 auto originalPath = fileInfo().path(), backupPath = std::string();
427 auto backupStream = NativeFileStream();
428
429 if (fileInfo().saveFilePath().empty()) {
430 // move current file to temp dir and reopen it as backupStream, recreate original file
431 try {
432 BackupHelper::createBackupFileCanonical(fileInfo().backupDirectory(), originalPath, backupPath, fileInfo().stream(), backupStream);
433 // recreate original file, define buffer variables
434 fileInfo().stream().open(originalPath, ios_base::out | ios_base::binary | ios_base::trunc);
435 } catch (const std::ios_base::failure &failure) {
436 diag.emplace_back(
437 DiagLevel::Critical, argsToString("Creation of temporary file (to rewrite the original file) failed: ", failure.what()), context);
438 throw;
439 }
440 } else {
441 // open the current file as backupStream and create a new outputStream at the specified "save file path"
442 try {
443 backupStream.exceptions(ios_base::badbit | ios_base::failbit);
444 backupStream.open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::binary);
445 fileInfo().close();
446 fileInfo().stream().open(
447 BasicFileInfo::pathForOpen(fileInfo().saveFilePath()).data(), ios_base::out | ios_base::binary | ios_base::trunc);
448 } catch (const std::ios_base::failure &failure) {
449 diag.emplace_back(DiagLevel::Critical, argsToString("Opening streams to write output file failed: ", failure.what()), context);
450 throw;
451 }
452 }
453
454 const auto totalFileSize = fileInfo().size();
455 try {
456 progress.nextStepOrStop("Writing Ogg pages ...");
457
458 // prepare iterating comments
459 OggVorbisComment *currentComment;
460 OggParameter *currentParams;
461 auto tagIterator = m_tags.cbegin(), tagEnd = m_tags.cend();
462 if (tagIterator != tagEnd) {
463 currentParams = &(currentComment = tagIterator->get())->oggParams();
464 } else {
465 currentComment = nullptr;
466 currentParams = nullptr;
467 }
468
469 // define misc variables
470 const OggPage *lastPage = nullptr;
471 static constexpr auto oggPageHeaderSize = 27u;
472 auto lastPageNewOffset = std::uint64_t();
473 auto copyHelper = CopyHelper<65307>();
474 auto updatedPageOffsets = std::vector<std::uint64_t>();
475 auto nextPageOffset = std::uint64_t();
476 auto pageSequenceNumberBySerialNo = std::unordered_map<std::uint32_t, std::uint32_t>();
477
478 // iterate through all pages of the original file
479 auto updateTick = 0u;
480 for (m_iterator.setStream(backupStream), m_iterator.removeFilter(), m_iterator.reset(); m_iterator; m_iterator.nextPage(), ++updateTick) {
481 const OggPage &currentPage = m_iterator.currentPage();
482 if (updateTick % 10) {
483 progress.updateStepPercentage(static_cast<std::uint8_t>(currentPage.startOffset() * 100ul / totalFileSize));
484 progress.stopIfAborted();
485 }
486
487 // check for gaps
488 // note: This is not just to print diag messages but also for taking into account that the parser might skip pages
489 // unless a full parse has been enforced.
490 if (lastPage && currentPage.startOffset() != nextPageOffset) {
491 m_iterator.pages().resize(m_iterator.currentPageIndex() - 1); // drop all further pages after the last consecutively parsed one
492 if (m_iterator.resyncAt(nextPageOffset)) {
493 // try again at the page we've just found
494 const auto actuallyNextPageOffset = m_iterator.currentPageOffset();
495 if (actuallyNextPageOffset != nextPageOffset) {
496 diag.emplace_back(DiagLevel::Critical,
497 argsToString("Expected Ogg page at offset ", nextPageOffset, " but found the next Ogg page only at offset ",
498 actuallyNextPageOffset, ". Skipped ", (actuallyNextPageOffset - nextPageOffset), " invalid bytes."),
499 context);
500 nextPageOffset = actuallyNextPageOffset;
501 }
502 m_iterator.previousPage();
503 continue;
504 } else {
505 diag.emplace_back(DiagLevel::Critical,
506 argsToString(
507 "Expected Ogg page at offset ", nextPageOffset, " but could not find any further pages. Skipped the rest of the file."),
508 context);
509 break;
510 }
511 }
512 const auto pageSize = currentPage.totalSize();
513 auto &pageSequenceNumber = pageSequenceNumberBySerialNo[currentPage.streamSerialNumber()];
514 lastPage = &currentPage;
515 lastPageNewOffset = static_cast<std::uint64_t>(stream().tellp());
516 nextPageOffset = currentPage.startOffset() + pageSize;
517
518 // check whether the Vorbis Comment is present in this Ogg page
519 if (currentComment && m_iterator.currentPageIndex() >= currentParams->firstPageIndex
520 && m_iterator.currentPageIndex() <= currentParams->lastPageIndex && !currentPage.segmentSizes().empty()) {
521 // page needs to be rewritten (not just copied)
522 // -> write segments to a buffer first
523 auto buffer = std::stringstream(std::ios_base::in | std::ios_base::out | std::ios_base::binary);
524 auto newSegmentSizes = std::vector<std::uint32_t>();
525 newSegmentSizes.reserve(currentPage.segmentSizes().size());
526 auto segmentOffset = m_iterator.currentSegmentOffset();
527 auto segmentIndex = std::vector<std::uint32_t>::size_type();
528 for (const auto segmentSize : currentPage.segmentSizes()) {
529 if (!segmentSize) {
530 ++segmentIndex;
531 continue;
532 }
533 // check whether this segment contains the Vorbis Comment
534 if (currentParams
535 && (m_iterator.currentPageIndex() >= currentParams->firstPageIndex && segmentIndex >= currentParams->firstSegmentIndex)
536 && (m_iterator.currentPageIndex() <= currentParams->lastPageIndex && segmentIndex <= currentParams->lastSegmentIndex)) {
537 // prevent making the comment twice if it spreads over multiple pages/segments
538 if (!currentParams->removed
539 && ((m_iterator.currentPageIndex() == currentParams->firstPageIndex
540 && m_iterator.currentSegmentIndex() == currentParams->firstSegmentIndex))) {
541 makeVorbisCommentSegment(buffer, copyHelper, newSegmentSizes, currentComment, currentParams, diag);
542 }
543
544 // proceed with next comment?
545 if (m_iterator.currentPageIndex() > currentParams->lastPageIndex
546 || (m_iterator.currentPageIndex() == currentParams->lastPageIndex && segmentIndex > currentParams->lastSegmentIndex)) {
547 if (++tagIterator != tagEnd) {
548 currentParams = &(currentComment = tagIterator->get())->oggParams();
549 } else {
550 currentComment = nullptr;
551 currentParams = nullptr;
552 }
553 }
554 } else {
555 // copy other segments unchanged
556 backupStream.seekg(static_cast<std::streamoff>(segmentOffset));
557 copyHelper.copy(backupStream, buffer, segmentSize);
558 newSegmentSizes.push_back(segmentSize);
559
560 // check whether there is a new comment to be inserted into the current page
561 if (currentParams && m_iterator.currentPageIndex() == currentParams->lastPageIndex
562 && currentParams->firstSegmentIndex == numeric_limits<size_t>::max()) {
563 if (!currentParams->removed) {
564 makeVorbisCommentSegment(buffer, copyHelper, newSegmentSizes, currentComment, currentParams, diag);
565 }
566 // proceed with next comment
567 if (++tagIterator != tagEnd) {
568 currentParams = &(currentComment = tagIterator->get())->oggParams();
569 } else {
570 currentComment = nullptr;
571 currentParams = nullptr;
572 }
573 }
574 }
575 segmentOffset += segmentSize;
576 ++segmentIndex;
577 }
578
579 // write buffered data to actual stream
580 if (auto newSegmentSizesIterator = newSegmentSizes.cbegin(), newSegmentSizesEnd = newSegmentSizes.cend();
581 newSegmentSizesIterator != newSegmentSizesEnd) {
582 auto bytesLeft = *newSegmentSizesIterator;
583 auto continuePreviousSegment = false, needsZeroLacingValue = false;
584 // write pages until all data in the buffer is written
585 while (newSegmentSizesIterator != newSegmentSizesEnd) {
586 // memorize offset to update checksum later
587 updatedPageOffsets.push_back(static_cast<std::uint64_t>(stream().tellp()));
588 // copy page header from original file (except for the segment table)
589 backupStream.seekg(static_cast<streamoff>(currentPage.startOffset()));
590 copyHelper.copy(backupStream, stream(), oggPageHeaderSize);
591 // use flags of original page as base and adjust "continued packet"-flag as needed
592 auto flags = (currentPage.headerTypeFlag() & 0xFE) | (continuePreviousSegment ? 0x01 : 0x00);
593 continuePreviousSegment = true;
594 // ensure "first page of logical bitstream"-flag is cleared for additional pages we need to insert
595 // ensure "last page of logical bitstream"-flag is cleared for the first page
596 flags = flags & (newSegmentSizesIterator != newSegmentSizes.cbegin() ? 0xFD : 0xF);
597 // override flags copied from original file
598 stream().seekp(-22, ios_base::cur);
599 stream().put(static_cast<char>(flags));
600 // update absolute granule position later (8 byte) and keep stream serial number (4 byte)
601 stream().seekp(12, ios_base::cur);
602 // adjust page sequence number
603 writer().writeUInt32LE(pageSequenceNumber);
604 // skip checksum (4 bytes) and number of page segments (1 byte); those are update later
605 stream().seekp(5, ios_base::cur);
606 // write segment sizes as long as there are segment sizes to be written and
607 // the max number of segment sizes (255) is not exceeded
608 auto segmentSizesWritten = std::int16_t(); // in the current page header only
609 auto currentSize = std::uint32_t();
610 while ((bytesLeft || needsZeroLacingValue) && segmentSizesWritten < 0xFF) {
611 while (bytesLeft > 0xFF && segmentSizesWritten < 0xFF) {
612 stream().put(static_cast<char>(0xFF));
613 currentSize += 0xFF;
614 bytesLeft -= 0xFF;
615 ++segmentSizesWritten;
616 }
617 if ((bytesLeft || needsZeroLacingValue) && segmentSizesWritten < 0xFF) {
618 // bytes left is here <= 0xFF
619 stream().put(static_cast<char>(bytesLeft));
620 currentSize += bytesLeft;
621 needsZeroLacingValue = bytesLeft == 0xFF;
622 bytesLeft = 0;
623 ++segmentSizesWritten;
624 }
625 if (!bytesLeft && !needsZeroLacingValue) {
626 // sizes for the segment have been written
627 // -> continue with next segment
628 if (++newSegmentSizesIterator != newSegmentSizesEnd) {
629 bytesLeft = *newSegmentSizesIterator;
630 continuePreviousSegment = false;
631 }
632 }
633 }
634
635 // remove continue flag if there are no bytes left in the current segment
636 if (!bytesLeft && !needsZeroLacingValue) {
637 continuePreviousSegment = false;
638 }
639
640 // set the absolute granule postion
641 if (newSegmentSizesIterator != newSegmentSizesEnd) {
642 // set absolute granule position to special value "-1" if there are still bytes to be written in the current packet
643 stream().seekp(-21 - segmentSizesWritten, ios_base::cur);
644 writer().writeInt64LE(-1);
645 stream().seekp(12, ios_base::cur);
646 } else if (currentParams->lastPageIndex != currentParams->firstPageIndex) {
647 // ensure the written absolute granule position matches the one from the last page of the existing file
648 backupStream.seekg(static_cast<streamoff>(m_iterator.pages()[currentParams->lastPageIndex].startOffset() + 6));
649 stream().seekp(-21 - segmentSizesWritten, ios_base::cur);
650 copyHelper.copy(backupStream, stream(), 8);
651 stream().seekp(12, ios_base::cur);
652 } else {
653 // leave the absolute granule position unchanged
654 stream().seekp(-1 - segmentSizesWritten, ios_base::cur);
655 }
656
657 // page is full or all segment data has been covered
658 // -> write segment table size (segmentSizesWritten) and segment data
659 // -> seek back and write updated page segment number
660 stream().put(static_cast<char>(segmentSizesWritten));
661 stream().seekp(segmentSizesWritten, ios_base::cur);
662 // -> write actual page data
663 copyHelper.copy(buffer, stream(), currentSize);
664
665 ++pageSequenceNumber;
666 }
667 }
668
669 } else {
670 if (pageSequenceNumber != m_iterator.currentPageIndex()) {
671 // just update page sequence number
672 backupStream.seekg(static_cast<streamoff>(currentPage.startOffset()));
673 updatedPageOffsets.push_back(static_cast<std::uint64_t>(stream().tellp())); // memorize offset to update checksum later
674 copyHelper.copy(backupStream, stream(), oggPageHeaderSize);
675 stream().seekp(-9, ios_base::cur);
676 writer().writeUInt32LE(pageSequenceNumber);
677 stream().seekp(5, ios_base::cur);
678 copyHelper.copy(backupStream, stream(), pageSize - oggPageHeaderSize);
679 } else {
680 // copy page unchanged
681 backupStream.seekg(static_cast<streamoff>(currentPage.startOffset()));
682 copyHelper.copy(backupStream, stream(), pageSize);
683 }
684 ++pageSequenceNumber;
685 }
686 }
687
688 // report new size
689 fileInfo().reportSizeChanged(static_cast<std::uint64_t>(stream().tellp()));
690 progress.updateStepPercentage(100);
691
692 // "save as path" is now the regular path
693 if (!fileInfo().saveFilePath().empty()) {
694 fileInfo().reportPathChanged(fileInfo().saveFilePath());
695 fileInfo().setSaveFilePath(string());
696 }
697
698 // close backups stream; reopen new file as readable stream
699 auto &stream = fileInfo().stream();
700 backupStream.close();
701 fileInfo().close();
702 stream.open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::out | ios_base::binary);
703
704 // ensure the "last page of logical bitstream"-flag is set on the last Ogg page (in case the last page was written/modified by us)
705 if (lastPage && lastPageNewOffset) {
706 const auto offset = static_cast<std::streamoff>(lastPageNewOffset + 5ul);
707 stream.seekg(offset);
708 if (const auto flag = stream.get(); !(flag & 0x04)) {
709 updatedPageOffsets.emplace_back(lastPageNewOffset);
710 stream.seekp(offset);
711 stream.put(static_cast<char>(flag | 0x04));
712 }
713 }
714
715 // update checksums of modified pages
716 progress.nextStepOrStop("Updating checksums ...");
717 updateTick = 0u;
718 for (auto offset : updatedPageOffsets) {
719 if (updateTick++ % 10) {
720 progress.updateStepPercentage(static_cast<std::uint8_t>(offset * 100ul / fileInfo().size()));
721 progress.stopIfAborted();
722 }
724 }
725
726 // prevent deferring final write operations (to catch and handle possible errors here)
727 stream.flush();
728 progress.updateStepPercentage(100);
729
730 // clear iterator
731 m_iterator.clear(stream, startOffset(), fileInfo().size());
732
733 } catch (...) {
734 m_iterator.setStream(fileInfo().stream());
735 BackupHelper::handleFailureAfterFileModifiedCanonical(fileInfo(), originalPath, backupPath, fileInfo().stream(), backupStream, diag, context);
736 }
737}
738
739} // namespace TagParser
The AbortableProgressFeedback class provides feedback about an ongoing operation via callbacks.
bool isAborted() const
Returns whether the operation has been aborted via tryToAbort().
void stopIfAborted() const
Throws an OperationAbortedException if aborted.
void nextStepOrStop(const std::string &step, std::uint8_t stepPercentage=0)
Throws an OperationAbortedException if aborted; otherwise the data for the next step is set.
std::iostream & stream()
Returns the related stream.
std::uint64_t startOffset() const
Returns the start offset in the related stream.
void parseTracks(Diagnostics &diag, AbortableProgressFeedback &progress)
Parses the tracks of the file if not parsed yet.
CppUtilities::BinaryWriter & writer()
Returns the related BinaryWriter.
void parseTags(Diagnostics &diag, AbortableProgressFeedback &progress)
Parses the tag information if not parsed yet.
CppUtilities::TimeSpan m_duration
void reportPathChanged(std::string_view newPath)
Call this function to report that the path changed.
const std::string & path() const
Returns the path of the current file.
std::uint64_t size() const
Returns size of the current file in bytes.
CppUtilities::NativeFileStream & stream()
Returns the std::fstream for the current instance.
void close()
A possibly opened std::fstream will be closed.
static std::string_view pathForOpen(std::string_view url)
Returns removes the "file:/" prefix from url to be able to pass it to functions like open(),...
void reportSizeChanged(std::uint64_t newSize)
Call this function to report that the size changed.
void updateStepPercentage(std::uint8_t stepPercentage)
Updates the current step percentage and invokes the second callback specified on construction (or the...
The Diagnostics class is a container for DiagMessage.
The class inherits from std::exception and serves as base class for exceptions thrown by the elements...
Definition exceptions.h:11
The FlacMetaDataBlockHeader class is a FLAC "METADATA_BLOCK_HEADER" parser and maker.
constexpr std::uint32_t dataSize() const
Returns the length in bytes of the meta data (excluding the size of the header itself).
void setType(FlacMetaDataBlockType type)
Sets the block type.
void setLast(std::uint8_t last)
Sets whether this is the last metadata block before the audio blocks.
void setDataSize(std::uint32_t dataSize)
Sets the length in bytes of the meta data (excluding the size of the header itself).
void makeHeader(std::ostream &outputStream)
Writes the header to the specified outputStream.
The exception that is thrown when the data to be parsed or to be made seems invalid and therefore can...
Definition exceptions.h:25
The MediaFileInfo class allows to read and write tag information providing a container/tag format ind...
void setSaveFilePath(std::string_view saveFilePath)
Sets the "save file path".
static void writePadding(std::ostream &outputStream, uint64_t size)
Writes the specified number of zeroes to outputStream.
void reportPaddingSizeChanged(std::uint64_t newPaddingSize)
Sets the padding size.
void internalParseTags(Diagnostics &diag, AbortableProgressFeedback &progress) override
Internally called to parse the tags.
OggVorbisComment * tag(std::size_t index) override
Returns the tag with the specified index.
std::size_t tagCount() const override
Returns the number of tags attached to the container.
void internalParseTracks(Diagnostics &diag, AbortableProgressFeedback &progress) override
Internally called to parse the tracks.
OggContainer(MediaFileInfo &fileInfo, std::uint64_t startOffset)
Constructs a new container for the specified stream at the specified startOffset.
void internalParseHeader(Diagnostics &diag, AbortableProgressFeedback &progress) override
Internally called to parse the header.
void removeAllTags() override
Actually just flags all tags as removed and clears all assigned fields.
void reset() override
Discards all parsing results.
OggVorbisComment * createTag(const TagTarget &target=TagTarget()) override
Creates a new tag.
void internalMakeFile(Diagnostics &diag, AbortableProgressFeedback &progress) override
Internally called to make the file.
bool removeTag(Tag *tag) override
Actually just flags the specified tag as removed and clears all assigned fields.
const std::vector< OggPage > & pages() const
Returns a vector of containing the Ogg pages that have been fetched yet.
The OggPage class is used to parse Ogg pages.
Definition oggpage.h:13
std::uint32_t sequenceNumber() const
Returns the page sequence number.
Definition oggpage.h:185
std::uint32_t dataSize() const
Returns the data size in byte.
Definition oggpage.h:238
std::uint32_t totalSize() const
Returns the total size of the page in byte.
Definition oggpage.h:246
std::uint32_t streamSerialNumber() const
Returns the stream serial number.
Definition oggpage.h:166
const std::vector< std::uint32_t > & segmentSizes() const
Returns the sizes of the segments of the page in byte.
Definition oggpage.h:220
std::uint8_t headerTypeFlag() const
Returns the header type flag.
Definition oggpage.h:105
std::uint64_t startOffset() const
Returns the start offset of the page.
Definition oggpage.h:86
std::uint32_t checksum() const
Returns the page checksum.
Definition oggpage.h:200
static std::uint32_t computeChecksum(std::istream &stream, std::uint64_t startOffset)
Computes the actual checksum of the page read from the specified stream at the specified startOffset.
Definition oggpage.cpp:79
static void updateChecksum(std::iostream &stream, std::uint64_t startOffset)
Updates the checksum of the page read from the specified stream at the specified startOffset.
Definition oggpage.cpp:116
Specialization of TagParser::VorbisComment for Vorbis comments inside an Ogg stream.
OggParameter & oggParams()
Returns the Ogg parameter for the comment.
std::string_view typeName() const override
Returns the type name of the tag.
The exception that is thrown when an operation has been stopped and thus not successfully completed b...
Definition exceptions.h:46
The TagTarget class specifies the target of a tag.
Definition tagtarget.h:20
const IdContainerType & tracks() const
Returns the tracks.
Definition tagtarget.h:105
The Tag class is used to store, read and write tag information.
Definition tag.h:179
const TagTarget & target() const
Definition tag.h:257
The exception that is thrown when the data to be parsed is truncated and therefore can not be parsed ...
Definition exceptions.h:39
Implementation of TagParser::Tag for Vorbis comments.
TAG_PARSER_EXPORT void handleFailureAfterFileModifiedCanonical(MediaFileInfo &fileInfo, const std::string &originalPath, const std::string &backupPath, CppUtilities::NativeFileStream &outputStream, CppUtilities::NativeFileStream &backupStream, Diagnostics &diag, const std::string &context="making file")
Handles a failure/abort which occurred after the file has been modified.
TAG_PARSER_EXPORT void createBackupFileCanonical(const std::string &backupDir, std::string &originalPath, std::string &backupPath, CppUtilities::NativeFileStream &originalStream, CppUtilities::NativeFileStream &backupStream)
Creates a backup file like createBackupFile() but canonicalizes originalPath before doing the backup.
Contains all classes and functions of the TagInfo library.
Definition aaccodebook.h:10
GeneralMediaFormat
The GeneralMediaFormat enum specifies the general format of media data (PCM, MPEG-4,...
Definition mediaformat.h:30
The OggParameter struct holds the Ogg parameter for a VorbisComment.
GeneralMediaFormat streamFormat
std::size_t lastSegmentIndex
std::size_t firstSegmentIndex
void set(std::size_t pageIndex, std::size_t segmentIndex, bool lastMetaDataBlock, GeneralMediaFormat streamFormat=GeneralMediaFormat::Vorbis)
Sets the firstPageIndex/lastPageIndex, the firstSegmentIndex/lastSegmentIndex, whether the associated...