From a84ac37dbe3fb1858223ead83ef10f3c0f97c3fc Mon Sep 17 00:00:00 2001 From: Martchus Date: Mon, 16 May 2016 20:56:53 +0200 Subject: [PATCH] Add support for raw FLAC streams --- CMakeLists.txt | 2 + README.md | 10 +- abstracttrack.h | 5 +- exceptions.h | 10 ++ flac/flacmetadata.cpp | 12 +- flac/flacmetadata.h | 3 +- flac/flacstream.cpp | 253 +++++++++++++++++++++++++++++++++ flac/flacstream.h | 75 ++++++++++ id3/id3v2frame.h | 3 +- matroska/matroskacontainer.cpp | 1 - mediafileinfo.cpp | 188 ++++++++++++++++++------ mediafileinfo.h | 12 -- mediaformat.h | 9 ++ ogg/oggcontainer.cpp | 7 +- ogg/oggiterator.cpp | 33 ++++- ogg/oggiterator.h | 18 ++- ogg/oggpage.h | 2 +- signature.cpp | 8 ++ signature.h | 3 + tagvalue.h | 2 +- vorbis/vorbiscomment.cpp | 73 +++++++--- vorbis/vorbiscomment.h | 27 +--- vorbis/vorbiscommentfield.cpp | 94 +++++++++--- vorbis/vorbiscommentfield.h | 29 +++- vorbis/vorbispackagetypes.h | 2 + 25 files changed, 739 insertions(+), 142 deletions(-) create mode 100644 flac/flacstream.cpp create mode 100644 flac/flacstream.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ba5ff4..bfb6e95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ set(HEADER_FILES opus/opusidentificationheader.h flac/flactooggmappingheader.h flac/flacmetadata.h + flac/flacstream.h positioninset.h signature.h size.h @@ -109,6 +110,7 @@ set(SRC_FILES opus/opusidentificationheader.cpp flac/flactooggmappingheader.cpp flac/flacmetadata.cpp + flac/flacstream.cpp signature.cpp statusprovider.cpp tag.cpp diff --git a/README.md b/README.md index 78429d3..a63f89a 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # Tag Parser -C++ library for reading and writing MP4 (iTunes), ID3, Vorbis and Matroska tags. +C++ library for reading and writing MP4 (iTunes), ID3, Vorbis, Opus, FLAC and Matroska tags. ## Supported formats The tag library can read and write the following tag formats: -- iTunes-style MP4 tags (MP4-DASH is supported) +- iTunes-style MP4/M4A tags (MP4-DASH is supported) - ID3v1 and ID3v2 tags - - conversion between ID3v1 and different versions of ID3v2 + - conversion between ID3v1 and different versions of ID3v2 is possible - Vorbis, Opus and FLAC comments in Ogg streams - cover art via "METADATA_BLOCK_PICTURE" is supported +- Vorbis comments and "METADATA_BLOCK_PICTURE" in raw FLAC streams - Matroska/WebM tags and attachments ## File layout options @@ -23,7 +24,8 @@ or appending the tag. Usage of padding can be configured: However, it is also possible to force rewriting the entire file. -Taking advantage of padding is currently not supported when dealing with Ogg streams. +Taking advantage of padding is currently not supported when dealing with Ogg streams (it is supported when +dealing with raw FLAC streams). ## Additional features The library can also display technical information such as the ID, format, language, bitrate, diff --git a/abstracttrack.h b/abstracttrack.h index 46c07c4..64b236b 100644 --- a/abstracttrack.h +++ b/abstracttrack.h @@ -13,7 +13,7 @@ #include #include -#include +#include #include namespace Media { @@ -35,7 +35,8 @@ enum class TrackType Mp4Track, /**< The track is a Media::Mp4Track. */ WaveAudioStream, /**< The track is a Media::WaveAudioStream. */ OggStream, /**< The track is a Media::OggStream. */ - AdtsStream /**< The track is a Media::AdtsStream. */ + AdtsStream, /**< The track is a Media::AdtsStream. */ + FlacStream, /**< The track is a Media::FlacStream. */ }; class LIB_EXPORT AbstractTrack : public StatusProvider diff --git a/exceptions.h b/exceptions.h index 2f1ac22..aba8b8d 100644 --- a/exceptions.h +++ b/exceptions.h @@ -64,6 +64,16 @@ public: virtual const char *what() const USE_NOTHROW; }; +/*! + * \brief Throws TruncatedDataException() if the specified \a sizeDenotation exceeds maxSize; otherwise maxSize is reduced by \a sizeDenotation. + */ +#define CHECK_MAX_SIZE(sizeDenotation) \ + if(maxSize < sizeDenotation) { \ + throw TruncatedDataException(); \ + } else { \ + maxSize -= sizeDenotation; \ + } + } #endif // MEDIA_EXCEPTIONS_H diff --git a/flac/flacmetadata.cpp b/flac/flacmetadata.cpp index 32a7e06..2c1ee05 100644 --- a/flac/flacmetadata.cpp +++ b/flac/flacmetadata.cpp @@ -54,7 +54,7 @@ void FlacMetaDataBlockHeader::makeHeader(std::ostream &outputStream) /*! * \brief Parses the FLAC "METADATA_BLOCK_STREAMINFO" which is read using the specified \a iterator. - * \remarks The specified \a buffer must be at least 34 bytes long. + * \remarks The specified \a buffer must be at least 0x22 bytes long. */ void FlacMetaDataBlockStreamInfo::parse(const char *buffer) { @@ -78,18 +78,24 @@ void FlacMetaDataBlockStreamInfo::parse(const char *buffer) /*! * \brief Parses the FLAC "METADATA_BLOCK_PICTURE". + * + * \a maxSize specifies the maximum size of the structure. */ -void FlacMetaDataBlockPicture::parse(istream &inputStream) +void FlacMetaDataBlockPicture::parse(istream &inputStream, uint32 maxSize) { + CHECK_MAX_SIZE(32); BinaryReader reader(&inputStream); m_pictureType = reader.readUInt32BE(); - auto size = reader.readUInt32BE(); + uint32 size = reader.readUInt32BE(); + CHECK_MAX_SIZE(size); m_value.setMimeType(reader.readString(size)); size = reader.readUInt32BE(); + CHECK_MAX_SIZE(size); m_value.setDescription(reader.readString(size)); // skip width, height, color depth, number of colors used inputStream.seekg(4 * 4, ios_base::cur); size = reader.readUInt32BE(); + CHECK_MAX_SIZE(size); auto data = make_unique(size); inputStream.read(data.get(), size); m_value.assignData(move(data), size, TagDataType::Picture); diff --git a/flac/flacmetadata.h b/flac/flacmetadata.h index 9ba6271..523f097 100644 --- a/flac/flacmetadata.h +++ b/flac/flacmetadata.h @@ -66,6 +66,7 @@ inline FlacMetaDataBlockHeader::FlacMetaDataBlockHeader() : /*! * \brief Returns whether this is the last metadata block before the audio blocks. + * \remarks The default value is 0/false. */ inline byte FlacMetaDataBlockHeader::isLast() const { @@ -258,7 +259,7 @@ class LIB_EXPORT FlacMetaDataBlockPicture public: FlacMetaDataBlockPicture(TagValue &tagValue); - void parse(std::istream &inputStream); + void parse(std::istream &inputStream, uint32 maxSize); uint32 requiredSize() const; void make(std::ostream &outputStream); diff --git a/flac/flacstream.cpp b/flac/flacstream.cpp new file mode 100644 index 0000000..3f200b8 --- /dev/null +++ b/flac/flacstream.cpp @@ -0,0 +1,253 @@ +#include "./flacstream.h" +#include "./flacmetadata.h" + +#include "../vorbis/vorbiscomment.h" + +#include "../exceptions.h" +#include "../mediafileinfo.h" +#include "../mediaformat.h" + +#include "resources/config.h" + +#include + +#include + +using namespace std; +using namespace IoUtilities; +using namespace ConversionUtilities; +using namespace ChronoUtilities; + +namespace Media { + +/*! + * \class Media::FlacStream + * \brief Implementation of Media::AbstractTrack for raw FLAC streams. + */ + +/*! + * \brief Constructs a new track for the specified \a mediaFileInfo at the specified \a startOffset. + * + * The stream of the \a mediaFileInfo instance is used as input stream. + */ +FlacStream::FlacStream(MediaFileInfo &mediaFileInfo, uint64 startOffset) : + AbstractTrack(mediaFileInfo.stream(), startOffset), + m_mediaFileInfo(mediaFileInfo), + m_paddingSize(0), + m_streamOffset(0) +{ + m_mediaType = MediaType::Audio; +} + +/*! + * \brief Creates a new Vorbis comment for the stream. + * \remarks Just returns the current Vorbis comment if already present. + */ +VorbisComment *FlacStream::createVorbisComment() +{ + if(!m_vorbisComment) { + m_vorbisComment = make_unique(); + } + return m_vorbisComment.get(); +} + +void FlacStream::internalParseHeader() +{ + static const string context("parsing raw FLAC header"); + if(!m_istream) { + throw NoDataFoundException(); + } + + m_istream->seekg(m_startOffset, ios_base::beg); + char buffer[0x22]; + + // check signature + if(m_reader.readUInt32BE() == 0x664C6143) { + m_format = GeneralMediaFormat::Flac; + + // parse meta data blocks + for(FlacMetaDataBlockHeader header; !header.isLast(); ) { + // parse block header + m_istream->read(buffer, 4); + header.parseHeader(buffer); + + // remember start offset + const auto startOffset = m_istream->tellg(); + + // parse relevant meta data + switch(static_cast(header.type())) { + case FlacMetaDataBlockType::StreamInfo: + if(header.dataSize() >= 0x22) { + m_istream->read(buffer, 0x22); + FlacMetaDataBlockStreamInfo streamInfo; + streamInfo.parse(buffer); + m_channelCount = streamInfo.channelCount(); + m_samplingFrequency = streamInfo.samplingFrequency(); + m_sampleCount = streamInfo.totalSampleCount(); + m_bitsPerSample = streamInfo.bitsPerSample(); + } else { + addNotification(NotificationType::Critical, "\"METADATA_BLOCK_STREAMINFO\" is truncated and will be ignored.", context); + } + break; + + case FlacMetaDataBlockType::VorbisComment: + // parse Vorbis comment + // if more then one comment exist, simply thread those comments as one + if(!m_vorbisComment) { + m_vorbisComment = make_unique(); + } + try { + m_vorbisComment->parse(*m_istream, header.dataSize(), VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte); + } catch(const Failure &) { + // error is logged via notifications, just continue with the next metadata block + } + break; + + case FlacMetaDataBlockType::Picture: + try { + // parse the cover + VorbisCommentField coverField; + coverField.setId(m_vorbisComment->fieldId(KnownField::Cover)); + FlacMetaDataBlockPicture picture(coverField.value()); + picture.parse(*m_istream, header.dataSize()); + coverField.setTypeInfo(picture.pictureType()); + + // add the cover to the Vorbis comment + if(!m_vorbisComment) { + // create one if none exists yet + m_vorbisComment = make_unique(); + m_vorbisComment->setVendor(TagValue(APP_NAME " v" APP_VERSION, TagTextEncoding::Utf8)); + } + m_vorbisComment->fields().insert(make_pair(coverField.id(), move(coverField))); + + } catch(const TruncatedDataException &) { + addNotification(NotificationType::Critical, "\"METADATA_BLOCK_PICTURE\" is truncated and will be ignored.", context); + } + break; + + case FlacMetaDataBlockType::Padding: + m_paddingSize += 4 + header.dataSize(); + break; + + default: + ; + } + + // seek to next block + m_istream->seekg(startOffset + static_cast(header.dataSize())); + + // TODO: check first FLAC frame + } + + m_streamOffset = m_istream->tellg(); + + } else { + addNotification(NotificationType::Critical, "Signature (fLaC) not found.", context); + throw InvalidDataException(); + } +} + +/*! + * \brief Writes the FLAC metadata header to the specified \a outputStream. + * + * This basically copies all "METADATA_BLOCK_HEADER" of the current stream to the specified \a outputStream, except: + * + * - Vorbis comment is updated. + * - "METADATA_BLOCK_PICTURE" are updated. + * - Padding is skipped + * + * \returns Returns the start offset of the last "METADATA_BLOCK_HEADER" withing \a outputStream. + */ +uint32 FlacStream::makeHeader(ostream &outputStream) +{ + istream &originalStream = m_mediaFileInfo.stream(); + originalStream.seekg(m_startOffset + 4); + CopyHelper<512> copy; + + // write signature + BE::getBytes(static_cast(0x664C6143u), copy.buffer()); + outputStream.write(copy.buffer(), 4); + + uint32 lastStartOffset = 0; + + // write meta data blocks which don't need to be adjusted + for(FlacMetaDataBlockHeader header; !header.isLast(); ) { + // parse block header + m_istream->read(copy.buffer(), 4); + header.parseHeader(copy.buffer()); + + // parse relevant meta data + switch(static_cast(header.type())) { + case FlacMetaDataBlockType::VorbisComment: + case FlacMetaDataBlockType::Picture: + case FlacMetaDataBlockType::Padding: + m_istream->seekg(header.dataSize(), ios_base::cur); + break; // written separately/ignored + default: + m_istream->seekg(-4, ios_base::cur); + lastStartOffset = outputStream.tellp(); + copy.copy(originalStream, outputStream, 4 + header.dataSize()); + } + } + + // write Vorbis comment + if(m_vorbisComment) { + // leave 4 bytes space for the "METADATA_BLOCK_HEADER" + lastStartOffset = outputStream.tellp(); + outputStream.write(copy.buffer(), 4); + + // determine cover ID since covers must be written separately + const auto coverId = m_vorbisComment->fieldId(KnownField::Cover); + + // write Vorbis comment + m_vorbisComment->make(outputStream, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte | VorbisCommentFlags::NoCovers); + + // write "METADATA_BLOCK_HEADER" + const uint32 endOffset = outputStream.tellp(); + FlacMetaDataBlockHeader header; + header.setType(FlacMetaDataBlockType::VorbisComment); + header.setDataSize(endOffset - lastStartOffset - 4); + header.setLast(!m_vorbisComment->hasField(coverId)); + outputStream.seekp(lastStartOffset); + header.makeHeader(outputStream); + outputStream.seekp(endOffset); + + // write cover fields separately as "METADATA_BLOCK_PICTURE" + if(!header.isLast()) { + header.setType(FlacMetaDataBlockType::Picture); + const auto coverFields = m_vorbisComment->fields().equal_range(coverId); + for(auto i = coverFields.first; i != coverFields.second; ) { + lastStartOffset = outputStream.tellp(); + FlacMetaDataBlockPicture pictureBlock(i->second.value()); + pictureBlock.setPictureType(i->second.typeInfo()); + header.setDataSize(pictureBlock.requiredSize()); + header.setLast(++i == coverFields.second); + header.makeHeader(outputStream); + pictureBlock.make(outputStream); + } + } + } + + return lastStartOffset; +} + +/*! + * \brief Writes padding of the specified \a size to the specified \a stream. + * \remarks Size must be at least 4 bytes. + */ +void FlacStream::makePadding(ostream &stream, uint32 size, bool isLast) +{ + // make header + FlacMetaDataBlockHeader header; + header.setType(FlacMetaDataBlockType::Padding); + header.setLast(isLast); + header.setDataSize(size -= 4); + header.makeHeader(stream); + + // write zeroes + for(; size; --size) { + stream.put(0); + } +} + +} diff --git a/flac/flacstream.h b/flac/flacstream.h new file mode 100644 index 0000000..0857d2b --- /dev/null +++ b/flac/flacstream.h @@ -0,0 +1,75 @@ +#ifndef FLACSTREAM_H +#define FLACSTREAM_H + +#include "../abstracttrack.h" + +#include + +#include + +namespace Media { + +class MediaFileInfo; +class VorbisComment; + +class LIB_EXPORT FlacStream : public AbstractTrack +{ +public: + FlacStream(MediaFileInfo &mediaFileInfo, uint64 startOffset); + ~FlacStream(); + + TrackType type() const; + VorbisComment *vorbisComment() const; + VorbisComment *createVorbisComment(); + uint32 paddingSize() const; + uint32 streamOffset() const; + + uint32 makeHeader(std::ostream &stream); + static void makePadding(std::ostream &stream, uint32 size, bool isLast); + +protected: + void internalParseHeader(); + +private: + MediaFileInfo &m_mediaFileInfo; + std::unique_ptr m_vorbisComment; + uint32 m_paddingSize; + uint32 m_streamOffset; +}; + +inline FlacStream::~FlacStream() +{} + +inline TrackType FlacStream::type() const +{ + return TrackType::FlacStream; +} + +/*! + * \brief Returns the Vorbis comment if one is present in the stream. + */ +inline VorbisComment *FlacStream::vorbisComment() const +{ + return m_vorbisComment.get(); +} + +/*! + * \brief Returns the padding size. + */ +inline uint32 FlacStream::paddingSize() const +{ + return m_paddingSize; +} + +/*! + * \brief Returns the start offset of the actual FLAC frames. + * \remarks This equals the size of the metadata header blocks. + */ +inline uint32 FlacStream::streamOffset() const +{ + return m_streamOffset; +} + +} + +#endif // FLACSTREAM_H diff --git a/id3/id3v2frame.h b/id3/id3v2frame.h index 53fca66..e901ba6 100644 --- a/id3/id3v2frame.h +++ b/id3/id3v2frame.h @@ -12,8 +12,7 @@ #include #include -#include -#include +#include #include namespace Media diff --git a/matroska/matroskacontainer.cpp b/matroska/matroskacontainer.cpp index c9f5eee..709044d 100644 --- a/matroska/matroskacontainer.cpp +++ b/matroska/matroskacontainer.cpp @@ -9,7 +9,6 @@ #include "../exceptions.h" #include "../backuphelper.h" -// include configuration from separate header file when building with CMake #include "resources/config.h" #include diff --git a/mediafileinfo.cpp b/mediafileinfo.cpp index ed1cd20..904af49 100644 --- a/mediafileinfo.cpp +++ b/mediafileinfo.cpp @@ -27,6 +27,9 @@ #include "./ogg/oggcontainer.h" +#include "./flac/flacstream.h" +#include "./flac/flacmetadata.h" + #include #include #include @@ -306,19 +309,31 @@ void MediaFileInfo::parseTracks() m_container->parseTracks(); } else { switch(m_containerFormat) { - case ContainerFormat::RiffWave: - m_singleTrack = make_unique(stream(), m_containerOffset); + case ContainerFormat::Adts: + m_singleTrack = make_unique(stream(), m_containerOffset); + break; + case ContainerFormat::Flac: + m_singleTrack = make_unique(*this, m_containerOffset); break; case ContainerFormat::MpegAudioFrames: m_singleTrack = make_unique(stream(), m_containerOffset); break; - case ContainerFormat::Adts: - m_singleTrack = make_unique(stream(), m_containerOffset); + case ContainerFormat::RiffWave: + m_singleTrack = make_unique(stream(), m_containerOffset); break; default: throw NotImplementedException(); } m_singleTrack->parseHeader(); + + switch(m_containerFormat) { + case ContainerFormat::Flac: + // FLAC streams might container padding + m_paddingSize += static_cast(m_singleTrack.get())->paddingSize(); + break; + default: + ; + } } m_tracksParsingStatus = ParsingStatus::Ok; } catch(const NotImplementedException &) { @@ -539,27 +554,34 @@ bool MediaFileInfo::createAppropriateTags(bool treatUnknownFilesAsMp3Files, TagU m_container->createTag(); } } else { - // no container object present; creation of ID3 tag is possible - if(!hasAnyTag() && !treatUnknownFilesAsMp3Files) { - switch(containerFormat()) { - case ContainerFormat::MpegAudioFrames: - case ContainerFormat::Adts: - break; - default: - return false; - } - } - // create ID3 tags according to id3v2usage and id3v2usage - if(id3v1usage == TagUsage::Always) { - // always create ID3v1 tag -> ensure there is one - createId3v1Tag(); - } - if(id3v2usage == TagUsage::Always) { - // always create ID3v2 tag -> ensure there is one and set version - if(!hasId3v2Tag()) { - createId3v2Tag()->setVersion(id3v2version, 0); + // no container object present + if(m_containerFormat == ContainerFormat::Flac) { + // creation of Vorbis comment is possible + static_cast(m_singleTrack.get())->createVorbisComment(); + } else { + // creation of ID3 tag is possible + if(!hasAnyTag() && !treatUnknownFilesAsMp3Files) { + switch(containerFormat()) { + case ContainerFormat::MpegAudioFrames: + case ContainerFormat::Adts: + break; + default: + return false; + } + } + // create ID3 tags according to id3v2usage and id3v2usage + if(id3v1usage == TagUsage::Always) { + // always create ID3v1 tag -> ensure there is one + createId3v1Tag(); + } + if(id3v2usage == TagUsage::Always) { + // always create ID3v2 tag -> ensure there is one and set version + if(!hasId3v2Tag()) { + createId3v2Tag()->setVersion(id3v2version, 0); + } } } + if(mergeMultipleSuccessiveId3v2Tags) { mergeId3v2Tags(); } @@ -1031,12 +1053,13 @@ bool MediaFileInfo::areTracksSupported() const bool MediaFileInfo::areTagsSupported() const { switch(m_containerFormat) { - case ContainerFormat::Mp4: - case ContainerFormat::MpegAudioFrames: - case ContainerFormat::Ogg: - case ContainerFormat::Matroska: - case ContainerFormat::Webm: case ContainerFormat::Adts: + case ContainerFormat::Flac: + case ContainerFormat::Matroska: + case ContainerFormat::MpegAudioFrames: + case ContainerFormat::Mp4: + case ContainerFormat::Ogg: + case ContainerFormat::Webm: // these container formats are supported return true; default: @@ -1081,7 +1104,11 @@ const vector > &MediaFileInfo::matroskaTags() const */ VorbisComment *MediaFileInfo::vorbisComment() const { - return m_containerFormat == ContainerFormat::Ogg && m_container && m_container->tagCount() > 0 ? static_cast(m_container.get())->tags().front().get() : nullptr; + return m_containerFormat == ContainerFormat::Ogg && m_container && m_container->tagCount() + ? static_cast(m_container.get())->tags().front().get() + : (m_containerFormat == ContainerFormat::Flac && m_singleTrack + ? static_cast(m_singleTrack.get())->vorbisComment() + : nullptr); } /*! @@ -1330,11 +1357,17 @@ bool MediaFileInfo::id3v2ToId3v1() */ void MediaFileInfo::tags(vector &tags) const { - if(hasId3v1Tag()) + if(hasId3v1Tag()) { tags.push_back(m_id3v1Tag.get()); + } for(const unique_ptr &tag : m_id3v2Tags) { tags.push_back(tag.get()); } + if(m_containerFormat == ContainerFormat::Flac && m_singleTrack) { + if(auto *vorbisComment = static_cast(m_singleTrack.get())->vorbisComment()) { + tags.push_back(vorbisComment); + } + } if(m_container) { for(size_t i = 0, count = m_container->tagCount(); i < count; ++i) { tags.push_back(m_container->tag(i)); @@ -1342,6 +1375,17 @@ void MediaFileInfo::tags(vector &tags) const } } +/*! + * \brief Returns an indication whether a tag of any format is assigned. + */ +bool MediaFileInfo::hasAnyTag() const +{ + return hasId3v1Tag() + || hasId3v2Tag() + || (m_container && m_container->tagCount()) + || (m_containerFormat == ContainerFormat::Flac && static_cast(m_singleTrack.get())->vorbisComment()); +} + /*! * \brief Returns all tags assigned to the current file. * @@ -1371,9 +1415,9 @@ void MediaFileInfo::invalidated() */ void MediaFileInfo::makeMp3File() { - const string context("making MP3 file"); + static const string context("making MP3 file"); // there's no need to rewrite the complete file if there are no ID3v2 tags present or to be written - if(!isForcingRewrite() && m_id3v2Tags.empty() && m_actualId3v2TagOffsets.empty() && m_saveFilePath.empty()) { + if(!isForcingRewrite() && m_id3v2Tags.empty() && m_actualId3v2TagOffsets.empty() && m_saveFilePath.empty() && m_containerFormat != ContainerFormat::Flac) { if(m_actualExistingId3v1Tag) { // there is currently an ID3v1 tag at the end of the file if(m_id3v1Tag) { @@ -1434,30 +1478,57 @@ void MediaFileInfo::makeMp3File() addNotifications(*tag); } + // check whether it is a raw FLAC stream + FlacStream *flacStream = (m_containerFormat == ContainerFormat::Flac ? static_cast(m_singleTrack.get()) : nullptr); + uint32 streamOffset; // where the actual stream starts + stringstream flacMetaData(ios_base::in | ios_base::out | ios_base::binary); + flacMetaData.exceptions(ios_base::badbit | ios_base::failbit); + uint32 startOfLastMetaDataBlock; + + if(flacStream) { + // if it is a raw FLAC stream, make FLAC metadata + startOfLastMetaDataBlock = flacStream->makeHeader(flacMetaData); + tagsSize += flacMetaData.tellp(); + streamOffset = flacStream->streamOffset(); + } else { + // make no further metadata, just use the container offset as stream offset + streamOffset = static_cast(m_containerOffset); + } + // check whether rewrite is required - bool rewriteRequired = isForcingRewrite() || !m_saveFilePath.empty() || (tagsSize > static_cast(m_containerOffset)); + bool rewriteRequired = isForcingRewrite() || !m_saveFilePath.empty() || (tagsSize > streamOffset); uint32 padding = 0; if(!rewriteRequired) { // rewriting is not forced and new tag is not too big for available space // -> calculate new padding - padding = static_cast(m_containerOffset) - tagsSize; + padding = streamOffset - tagsSize; // -> check whether the new padding matches specifications if(padding < minPadding() || padding > maxPadding()) { rewriteRequired = true; } } - if(makers.empty()) { - // an ID3v2 tag shouldn't be written + if(makers.empty() && !flacStream) { + // an ID3v2 tag is not written and it is not a FLAC stream // -> can't include padding if(padding) { // but padding would be present -> need to rewrite - padding = 0; + padding = 0; // can't write the preferred padding despite rewriting rewriteRequired = true; } } else if(rewriteRequired) { // rewriting is forced or new ID3v2 tag is too big for available space // -> use preferred padding when rewriting anyways padding = preferredPadding(); + } else if(makers.empty() && flacStream && padding && padding < 4) { + // no ID3v2 tag -> must include padding in FLAC stream + // but padding of 1, 2, and 3 byte isn't possible -> need to rewrite + padding = preferredPadding(); + rewriteRequired = true; + } + if(rewriteRequired && flacStream && makers.empty() && padding) { + // the first 4 byte of FLAC padding actually don't count because these + // can not be used for additional meta data + padding += 4; } updateStatus(rewriteRequired ? "Preparing streams for rewriting ..." : "Preparing streams for updating ..."); @@ -1490,7 +1561,6 @@ void MediaFileInfo::makeMp3File() } } - } else { // !rewriteRequired // reopen original file to ensure it is opened for writing try { @@ -1511,30 +1581,56 @@ void MediaFileInfo::makeMp3File() i->make(outputStream, 0); } // include padding into the last ID3v2 tag - makers.back().make(outputStream, padding); - } else { - // just write padding + makers.back().make(outputStream, (flacStream && padding && padding < 4) ? 0 : padding); + } + + if(flacStream) { + if(padding && startOfLastMetaDataBlock) { + // if appending padding, ensure the last flag of the last "METADATA_BLOCK_HEADER" is not set + flacMetaData.seekg(startOfLastMetaDataBlock); + flacMetaData.seekp(startOfLastMetaDataBlock); + flacMetaData.put(static_cast(flacMetaData.peek()) & (0x80u - 1)); + flacMetaData.seekg(0); + } + + // write FLAC metadata + outputStream << flacMetaData.rdbuf(); + + // write padding + if(padding) { + flacStream->makePadding(outputStream, padding, true); + } + } + + if(makers.empty() && !flacStream){ + // just write padding (however, padding should be set to 0 in this case?) for(; padding; --padding) { outputStream.put(0); } } - // copy / skip media data + // copy / skip actual stream data // -> determine media data size - uint64 mediaDataSize = size() - m_containerOffset; + uint64 mediaDataSize = size() - streamOffset; if(m_actualExistingId3v1Tag) { mediaDataSize -= 128; } if(rewriteRequired) { // copy data from original file - updateStatus("Writing MPEG audio frames ..."); - backupStream.seekg(m_containerOffset); + switch(m_containerFormat) { + case ContainerFormat::MpegAudioFrames: + updateStatus("Writing MPEG audio frames ..."); + break; + default: + updateStatus("Writing frames ..."); + } + backupStream.seekg(streamOffset); CopyHelper<0x4000> copyHelper; copyHelper.callbackCopy(backupStream, stream(), mediaDataSize, bind(&StatusProvider::isAborted, this), bind(&StatusProvider::updatePercentage, this, _1)); updatePercentage(100.0); } else { - // just skip media data + // just skip actual stream data outputStream.seekp(mediaDataSize, ios_base::cur); } diff --git a/mediafileinfo.h b/mediafileinfo.h index 9c29e44..0dcb52c 100644 --- a/mediafileinfo.h +++ b/mediafileinfo.h @@ -6,8 +6,6 @@ #include "./basicfileinfo.h" #include "./abstractcontainer.h" -#include -#include #include #include @@ -24,8 +22,6 @@ class OggContainer; class EbmlElement; class MatroskaTag; class AbstractTrack; -class WaveAudioStream; -class MpegAudioFrameStream; class VorbisComment; enum class MediaType; @@ -335,14 +331,6 @@ inline bool MediaFileInfo::hasId3v2Tag() const return m_id3v2Tags.size(); } -/*! - * \brief Returns an indication whether a tag of any format is assigned. - */ -inline bool MediaFileInfo::hasAnyTag() const -{ - return hasId3v1Tag() || hasId3v2Tag() || (m_container && m_container->tagCount()); -} - /*! * \brief Returns a pointer to the assigned ID3v1 tag or nullptr if none is assigned. * diff --git a/mediaformat.h b/mediaformat.h index 823d063..3491300 100644 --- a/mediaformat.h +++ b/mediaformat.h @@ -259,6 +259,7 @@ public: operator bool() const; MediaFormat &operator+=(const MediaFormat &other); bool operator==(GeneralMediaFormat general) const; + bool operator!=(GeneralMediaFormat general) const; GeneralMediaFormat general; unsigned char sub; @@ -299,6 +300,14 @@ inline bool MediaFormat::operator==(GeneralMediaFormat general) const return this->general == general; } +/*! + * \brief Returns whether the media format is not the specified general media format. + */ +inline bool MediaFormat::operator!=(GeneralMediaFormat general) const +{ + return this->general != general; +} + /*! * \brief Returns whether the media format is known. */ diff --git a/ogg/oggcontainer.cpp b/ogg/oggcontainer.cpp index d246984..8c8541d 100644 --- a/ogg/oggcontainer.cpp +++ b/ogg/oggcontainer.cpp @@ -235,11 +235,12 @@ void OggContainer::internalParseTags() break; case GeneralMediaFormat::Opus: // skip header (has already been detected by OggStream) - m_iterator.seekForward(8); + m_iterator.ignore(8); comment->parse(m_iterator, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte); break; case GeneralMediaFormat::Flac: - comment->parse(m_iterator, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, 4); + m_iterator.ignore(4); + comment->parse(m_iterator, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte); break; default: addNotification(NotificationType::Critical, "Stream format not supported.", "parsing tags from OGG streams"); @@ -287,7 +288,7 @@ void OggContainer::internalParseTracks() */ void OggContainer::makeVorbisCommentSegment(stringstream &buffer, CopyHelper<65307> ©Helper, vector &newSegmentSizes, VorbisComment *comment, OggParameter *params) { - auto offset = buffer.tellp(); + const auto offset = buffer.tellp(); switch(params->streamFormat) { case GeneralMediaFormat::Vorbis: comment->make(buffer); diff --git a/ogg/oggiterator.cpp b/ogg/oggiterator.cpp index 9936322..12bbfc2 100644 --- a/ogg/oggiterator.cpp +++ b/ogg/oggiterator.cpp @@ -123,6 +123,7 @@ void OggIterator::previousSegment() * - Page headers are skipped (this is the whole purpose of this method). * \throws Throws a TruncatedDataException if the end of the stream is reached before \a count bytes * have been read. + * \sa readAll() * \sa currentCharacterOffset() * \sa seekForward() */ @@ -149,6 +150,36 @@ void OggIterator::read(char *buffer, size_t count) } } +/*! + * \brief Reads all bytes from the OGG stream and writes it to the specified \a buffer. + * \remarks + * - Might increase the current page index and/or the current segment index. + * - Page headers are skipped (this is the whole purpose of this method). + * - Does not read more than \a max bytes from the buffer. + * \sa read() + * \sa currentCharacterOffset() + * \sa seekForward() + */ +size_t OggIterator::readAll(char *buffer, size_t max) +{ + size_t bytesRead = 0; + while(*this && max) { + uint32 available = currentSegmentSize() - m_bytesRead; + stream().seekg(currentCharacterOffset()); + if(max <= available) { + stream().read(buffer + bytesRead, max); + m_bytesRead += max; + return bytesRead + max; + } else { + stream().read(buffer + bytesRead, available); + nextSegment(); + bytesRead += available; + max -= available; + } + } + return bytesRead; +} + /*! * \brief Advances the position of the next character to be read from the OGG stream by \a count bytes. * \remarks @@ -159,7 +190,7 @@ void OggIterator::read(char *buffer, size_t count) * \sa currentCharacterOffset() * \sa read() */ -void OggIterator::seekForward(size_t count) +void OggIterator::ignore(size_t count) { uint32 available = currentSegmentSize() - m_bytesRead; while(*this) { diff --git a/ogg/oggiterator.h b/ogg/oggiterator.h index 2547ab9..2dd98c2 100644 --- a/ogg/oggiterator.h +++ b/ogg/oggiterator.h @@ -3,7 +3,7 @@ #include "./oggpage.h" -#include +#include #include namespace Media { @@ -31,13 +31,15 @@ public: std::vector::size_type currentSegmentIndex() const; uint64 currentSegmentOffset() const; uint64 currentCharacterOffset() const; + uint64 tellg() const; uint32 currentSegmentSize() const; void setFilter(uint32 streamSerialId); void removeFilter(); bool areAllPagesFetched() const; - void read(char *buffer, size_t count); - void seekForward(size_t count); - bool bytesRemaining(size_t atLeast) const; + void read(char *buffer, std::size_t count); + size_t readAll(char *buffer, std::size_t max); + void ignore(std::size_t count = 1); + bool bytesRemaining(std::size_t atLeast) const; operator bool() const; OggIterator &operator++(); @@ -201,6 +203,14 @@ inline uint64 OggIterator::currentCharacterOffset() const return m_offset + m_bytesRead; } +/*! + * \brief Same as currentCharacterOffset(); only provided for compliance with std::istream. + */ +inline uint64 OggIterator::tellg() const +{ + return currentCharacterOffset(); +} + /*! * \brief Returns the size of the current segment. * diff --git a/ogg/oggpage.h b/ogg/oggpage.h index 03747de..a58cf65 100644 --- a/ogg/oggpage.h +++ b/ogg/oggpage.h @@ -6,7 +6,7 @@ #include #include -#include +#include namespace Media { diff --git a/signature.cpp b/signature.cpp index 6a97a00..14bbde5 100644 --- a/signature.cpp +++ b/signature.cpp @@ -42,6 +42,7 @@ enum Sig48 : uint64 enum Sig32 : uint32 { Elf = 0x7F454C46u, + Flac = 0x664C6143u, JavaClassFile = 0xCAFEBABEu, Ebml = 0x1A45DFA3u, Mp4 = 0x66747970u, @@ -150,6 +151,8 @@ ContainerFormat parseSignature(const char *buffer, int bufferSize) switch(sig >> 32) { // check 32-bit signatures case Elf: return ContainerFormat::Elf; + case Flac: + return ContainerFormat::Flac; case JavaClassFile: return ContainerFormat::JavaClassFile; case Ebml: @@ -235,6 +238,7 @@ const char *containerFormatAbbreviation(ContainerFormat containerFormat, MediaTy case ContainerFormat::Ar: return "a"; case ContainerFormat::Asf: return "asf"; case ContainerFormat::Elf: return "elf"; + case ContainerFormat::Flac: return "flac"; case ContainerFormat::FlashVideo: return "flv"; case ContainerFormat::Gif87a: case ContainerFormat::Gif89a: return "gif"; @@ -315,6 +319,8 @@ const char *containerFormatName(ContainerFormat containerFormat) return "Advanced Systems Format"; case ContainerFormat::Elf: return "Executable and Linkable Format"; + case ContainerFormat::Flac: + return "raw Free Lossless Audio Codec frames"; case ContainerFormat::FlashVideo: return "Flash Video"; case ContainerFormat::Gif87a: @@ -417,6 +423,8 @@ const char *containerMimeType(ContainerFormat containerFormat, MediaType mediaTy switch(containerFormat) { case ContainerFormat::Asf: return "video/x-ms-asf"; + case ContainerFormat::Flac: + return "audio/flac"; case ContainerFormat::FlashVideo: return "video/x-flv"; case ContainerFormat::Gif87a: diff --git a/signature.h b/signature.h index 6632ee2..32aa69e 100644 --- a/signature.h +++ b/signature.h @@ -10,6 +10,8 @@ namespace Media { /*! * \brief Specifies the container format. + * + * Raw streams like ADTS or raw FLAC count as container format in this context. */ enum class ContainerFormat { @@ -19,6 +21,7 @@ enum class ContainerFormat Asf, /**< Advanced Systems Format */ Bzip2, /** bzip2 compressed file */ Elf, /**< Executable and Linkable Format */ + Flac, /** < Free Lossless Audio Codec (raw stream) */ FlashVideo, /**< Flash (FLV) */ Gif87a, /**< Graphics Interchange Format (1987) */ Gif89a, /**< Graphics Interchange Format (1989) */ diff --git a/tagvalue.h b/tagvalue.h index 861a0a7..7ec58f7 100644 --- a/tagvalue.h +++ b/tagvalue.h @@ -7,7 +7,7 @@ #include #include -#include +#include #include #include diff --git a/vorbis/vorbiscomment.cpp b/vorbis/vorbiscomment.cpp index 6c6c6d5..f297cbe 100644 --- a/vorbis/vorbiscomment.cpp +++ b/vorbis/vorbiscomment.cpp @@ -100,51 +100,51 @@ KnownField VorbisComment::knownField(const string &id) const } /*! - * \brief Parses tag information using the specified OGG \a iterator. - * - * \throws Throws std::ios_base::failure when an IO error occurs. - * \throws Throws Media::Failure or a derived exception when a parsing - * error occurs. + * \brief Internal implementation for parsing. */ -void VorbisComment::parse(OggIterator &iterator, VorbisCommentFlags flags, size_t offset) +template +void VorbisComment::internalParse(StreamType &stream, uint64 maxSize, VorbisCommentFlags flags) { // prepare parsing invalidateStatus(); static const string context("parsing Vorbis comment"); - auto startOffset = iterator.currentSegmentOffset() + offset; - iterator.seekForward(offset); + uint64 startOffset = static_cast(stream.tellg()); try { // read signature: 0x3 + "vorbis" char sig[8]; bool skipSignature = flags & VorbisCommentFlags::NoSignature; if(!skipSignature) { - iterator.read(sig, 7); + CHECK_MAX_SIZE(7); + stream.read(sig, 7); skipSignature = (ConversionUtilities::BE::toUInt64(sig) & 0xffffffffffffff00u) == 0x03766F7262697300u; } if(skipSignature) { // read vendor (length prefixed string) { - iterator.read(sig, 4); + CHECK_MAX_SIZE(4); + stream.read(sig, 4); const auto vendorSize = LE::toUInt32(sig); - if(iterator.currentCharacterOffset() + vendorSize <= iterator.streamSize()) { + if(vendorSize <= maxSize) { auto buff = make_unique(vendorSize); - iterator.read(buff.get(), vendorSize); + stream.read(buff.get(), vendorSize); m_vendor.assignData(move(buff), vendorSize, TagDataType::Text, TagTextEncoding::Utf8); // TODO: Is the vendor string actually UTF-8 (like the field values)? } else { addNotification(NotificationType::Critical, "Vendor information is truncated.", context); throw TruncatedDataException(); } + maxSize -= vendorSize; } // read field count - iterator.read(sig, 4); + CHECK_MAX_SIZE(4); + stream.read(sig, 4); uint32 fieldCount = LE::toUInt32(sig); VorbisCommentField field; const string &fieldId = field.id(); for(uint32 i = 0; i < fieldCount; ++i) { // read fields try { - field.parse(iterator); + field.parse(stream, maxSize); fields().insert(pair(fieldId, field)); } catch(const TruncatedDataException &) { addNotifications(field); @@ -156,20 +156,44 @@ void VorbisComment::parse(OggIterator &iterator, VorbisCommentFlags flags, size_ field.invalidateNotifications(); } if(!(flags & VorbisCommentFlags::NoFramingByte)) { - iterator.seekForward(1); // skip framing byte + stream.ignore(); // skip framing byte } - m_size = static_cast(static_cast(iterator.currentCharacterOffset()) - startOffset); + m_size = static_cast(static_cast(stream.tellg()) - startOffset); } else { addNotification(NotificationType::Critical, "Signature is invalid.", context); throw InvalidDataException(); } } catch(const TruncatedDataException &) { - m_size = static_cast(static_cast(iterator.currentCharacterOffset()) - startOffset); + m_size = static_cast(static_cast(stream.tellg()) - startOffset); addNotification(NotificationType::Critical, "Vorbis comment is truncated.", context); throw; } } +/*! + * \brief Parses tag information using the specified OGG \a iterator. + * + * \throws Throws std::ios_base::failure when an IO error occurs. + * \throws Throws Media::Failure or a derived exception when a parsing + * error occurs. + */ +void VorbisComment::parse(OggIterator &iterator, VorbisCommentFlags flags) +{ + internalParse(iterator, iterator.streamSize(), flags); +} + +/*! + * \brief Parses tag information using the specified OGG \a iterator. + * + * \throws Throws std::ios_base::failure when an IO error occurs. + * \throws Throws Media::Failure or a derived exception when a parsing + * error occurs. + */ +void VorbisComment::parse(istream &stream, uint64 maxSize, VorbisCommentFlags flags) +{ + internalParse(stream, maxSize, flags); +} + /*! * \brief Writes tag information to the specified \a stream. * @@ -197,14 +221,18 @@ void VorbisComment::make(std::ostream &stream, VorbisCommentFlags flags) // write vendor writer.writeUInt32LE(vendor.size()); writer.writeString(vendor); - // write field count - writer.writeUInt32LE(fieldCount()); + // write field count later + const auto fieldCountOffset = stream.tellp(); + writer.writeUInt32LE(0); // write fields + uint32 fieldsWritten = 0; for(auto i : fields()) { VorbisCommentField &field = i.second; if(!field.value().isEmpty()) { try { - field.make(writer); + if(field.make(writer, flags)) { + ++fieldsWritten; + } } catch(const Failure &) { // nothing to do here since notifications will be added anyways } @@ -213,6 +241,11 @@ void VorbisComment::make(std::ostream &stream, VorbisCommentFlags flags) field.invalidateNotifications(); } } + // write field count + const auto framingByteOffset = stream.tellp(); + stream.seekp(fieldCountOffset); + writer.writeUInt32LE(fieldsWritten); + stream.seekp(framingByteOffset); // write framing byte if(!(flags & VorbisCommentFlags::NoFramingByte)) { stream.put(0x01); diff --git a/vorbis/vorbiscomment.h b/vorbis/vorbiscomment.h index d022f5e..8e8068b 100644 --- a/vorbis/vorbiscomment.h +++ b/vorbis/vorbiscomment.h @@ -12,26 +12,6 @@ namespace Media { class OggIterator; class VorbisComment; -/*! - * \brief The VorbisCommentFlags enum specifies flags which controls parsing and making of Vorbis comments. - */ -enum class VorbisCommentFlags : byte -{ - None = 0x0, /**< Regular parsing/making. */ - NoSignature = 0x1, /**< Skips the signature when parsing; does not make the signature when making. */ - NoFramingByte = 0x2 /**< Doesn't expect the framing bit to be present when parsing; does not make the framing bit when making. */ -}; - -inline bool operator &(VorbisCommentFlags lhs, VorbisCommentFlags rhs) -{ - return static_cast(lhs) & static_cast(rhs); -} - -inline VorbisCommentFlags operator |(VorbisCommentFlags lhs, VorbisCommentFlags rhs) -{ - return static_cast(static_cast(lhs) | static_cast(rhs)); -} - class LIB_EXPORT VorbisComment : public FieldMapBasedTag { public: @@ -47,12 +27,17 @@ public: std::string fieldId(KnownField field) const; KnownField knownField(const std::string &id) const; - void parse(OggIterator &iterator, VorbisCommentFlags flags = VorbisCommentFlags::None, std::size_t offset = 0); + void parse(OggIterator &iterator, VorbisCommentFlags flags = VorbisCommentFlags::None); + void parse(std::istream &stream, uint64 maxSize, VorbisCommentFlags flags = VorbisCommentFlags::None); void make(std::ostream &stream, VorbisCommentFlags flags = VorbisCommentFlags::None); const TagValue &vendor() const; void setVendor(const TagValue &vendor); +private: + template + void internalParse(StreamType &stream, uint64 maxSize, VorbisCommentFlags flags); + private: TagValue m_vendor; }; diff --git a/vorbis/vorbiscommentfield.cpp b/vorbis/vorbiscommentfield.cpp index d311a66..806f511 100644 --- a/vorbis/vorbiscommentfield.cpp +++ b/vorbis/vorbiscommentfield.cpp @@ -42,25 +42,26 @@ VorbisCommentField::VorbisCommentField(const identifierType &id, const TagValue {} /*! - * \brief Parses a field from the stream read using the specified \a reader. - * - * The position of the current character in the input stream is expected to be - * at the beginning of the field to be parsed. - * - * \throws Throws std::ios_base::failure when an IO error occurs. - * \throws Throws Media::Failure or a derived exception when a parsing - * error occurs. + * \brief Internal implementation for parsing. */ -void VorbisCommentField::parse(OggIterator &iterator) +template +void VorbisCommentField::internalParse(StreamType &stream, uint64 &maxSize) { static const string context("parsing Vorbis comment field"); char buff[4]; - iterator.read(buff, 4); - if(auto size = LE::toUInt32(buff)) { // read size - if(iterator.currentCharacterOffset() + size <= iterator.streamSize()) { + if(maxSize < 4) { + addNotification(NotificationType::Critical, "Field expected.", context); + throw TruncatedDataException(); + } else { + maxSize -= 4; + } + stream.read(buff, 4); + if(const auto size = LE::toUInt32(buff)) { // read size + if(size <= maxSize) { + maxSize -= size; // read data auto data = make_unique(size); - iterator.read(data.get(), size); + stream.read(data.get(), size); uint32 idSize = 0; for(const char *i = data.get(), *end = data.get() + size; i != end && *i != '='; ++i, ++idSize); // extract id @@ -77,13 +78,16 @@ void VorbisCommentField::parse(OggIterator &iterator) bufferStream.exceptions(ios_base::failbit | ios_base::badbit); bufferStream.rdbuf()->pubsetbuf(reinterpret_cast(decoded.first.get()), decoded.second); FlacMetaDataBlockPicture pictureBlock(value()); - pictureBlock.parse(bufferStream); + pictureBlock.parse(bufferStream, decoded.second); setTypeInfo(pictureBlock.pictureType()); - } catch (const ios_base::failure &) { + } catch(const ios_base::failure &) { addNotification(NotificationType::Critical, "An IO error occured when reading the METADATA_BLOCK_PICTURE struct.", context); throw Failure(); - } catch (const ConversionException &) { - addNotification(NotificationType::Critical, "Base64 data from METADATA_BLOCK_PICTURE is invalid.", context); + } catch(const TruncatedDataException &) { + addNotification(NotificationType::Critical, "METADATA_BLOCK_PICTURE is truncated.", context); + throw; + } catch(const ConversionException &) { + addNotification(NotificationType::Critical, "Base64 coding of METADATA_BLOCK_PICTURE is invalid.", context); throw InvalidDataException(); } } else if(id().size() + 1 < size) { @@ -97,14 +101,62 @@ void VorbisCommentField::parse(OggIterator &iterator) } } +/*! + * \brief Parses a field using the specified \a iterator. + * + * The currentCharacterOffset() of the iterator is expected to be + * at the beginning of the field to be parsed. + * + * \throws Throws std::ios_base::failure when an IO error occurs. + * \throws Throws Media::Failure or a derived exception when a parsing + * error occurs. + */ +void VorbisCommentField::parse(OggIterator &iterator) +{ + uint64 maxSize = iterator.streamSize() - iterator.currentCharacterOffset(); + internalParse(iterator, maxSize); +} + +/*! + * \brief Parses a field using the specified \a iterator. + * + * The currentCharacterOffset() of the iterator is expected to be + * at the beginning of the field to be parsed. + * + * \throws Throws std::ios_base::failure when an IO error occurs. + * \throws Throws Media::Failure or a derived exception when a parsing + * error occurs. + */ +void VorbisCommentField::parse(OggIterator &iterator, uint64 &maxSize) +{ + internalParse(iterator, maxSize); +} + +/*! + * \brief Parses a field from the specified \a stream. + * + * The position of the current character in the input stream is expected to be + * at the beginning of the field to be parsed. + * + * \throws Throws std::ios_base::failure when an IO error occurs. + * \throws Throws Media::Failure or a derived exception when a parsing + * error occurs. + */ +void VorbisCommentField::parse(istream &stream, uint64 &maxSize) +{ + internalParse(stream, maxSize); +} + /*! * \brief Writes the field to a stream using the specified \a writer. * * \throws Throws std::ios_base::failure when an IO error occurs. * \throws Throws Media::Failure or a derived exception when a making * error occurs. + * \returns Returns whether the field has been written. (Some fields might be skipped + * when specific \a flags are set.) */ -void VorbisCommentField::make(BinaryWriter &writer) +bool VorbisCommentField::make(BinaryWriter &writer, VorbisCommentFlags flags) { static const string context("making Vorbis comment field"); if(id().empty()) { @@ -114,6 +166,9 @@ void VorbisCommentField::make(BinaryWriter &writer) // try to convert value to string string valueString; if(id() == VorbisCommentIds::cover()) { + if(flags & VorbisCommentFlags::NoCovers) { + return false; + } // make cover if(value().type() != TagDataType::Picture) { addNotification(NotificationType::Critical, "Assigned value of cover field is not picture data.", context); @@ -143,10 +198,11 @@ void VorbisCommentField::make(BinaryWriter &writer) writer.writeString(id()); writer.writeChar('='); writer.writeString(valueString); - } catch(ConversionException &) { + } catch(const ConversionException &) { addNotification(NotificationType::Critical, "Assigned value can not be converted appropriately.", context); throw InvalidDataException(); } + return true; } } diff --git a/vorbis/vorbiscommentfield.h b/vorbis/vorbiscommentfield.h index 7281480..b596713 100644 --- a/vorbis/vorbiscommentfield.h +++ b/vorbis/vorbiscommentfield.h @@ -11,6 +11,27 @@ class BinaryWriter; namespace Media { +/*! + * \brief The VorbisCommentFlags enum specifies flags which controls parsing and making of Vorbis comments. + */ +enum class VorbisCommentFlags : byte +{ + None = 0x0, /**< Regular parsing/making. */ + NoSignature = 0x1, /**< Skips the signature when parsing and making. */ + NoFramingByte = 0x2, /**< Doesn't expect the framing bit to be present when parsing; does not make the framing bit when making. */ + NoCovers = 0x4 /**< Skips all covers when making. */ +}; + +inline bool operator &(VorbisCommentFlags lhs, VorbisCommentFlags rhs) +{ + return static_cast(lhs) & static_cast(rhs); +} + +inline VorbisCommentFlags operator |(VorbisCommentFlags lhs, VorbisCommentFlags rhs) +{ + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + class VorbisCommentField; /*! @@ -45,12 +66,18 @@ public: VorbisCommentField(const identifierType &id, const TagValue &value); void parse(OggIterator &iterator); - void make(IoUtilities::BinaryWriter &writer); + void parse(OggIterator &iterator, uint64 &maxSize); + void parse(std::istream &stream, uint64 &maxSize); + bool make(IoUtilities::BinaryWriter &writer, VorbisCommentFlags flags = VorbisCommentFlags::None); bool isAdditionalTypeInfoUsed() const; bool supportsNestedFields() const; protected: void cleared(); + +private: + template + void internalParse(StreamType &stream, uint64 &maxSize); }; /*! diff --git a/vorbis/vorbispackagetypes.h b/vorbis/vorbispackagetypes.h index 96254b6..ce1098a 100644 --- a/vorbis/vorbispackagetypes.h +++ b/vorbis/vorbispackagetypes.h @@ -1,6 +1,8 @@ #ifndef VORBISPACKAGETYPES_H #define VORBISPACKAGETYPES_H +#include + namespace Media { /*!