From bbafd16dccdceedebee9dea67f302d4921c36efe Mon Sep 17 00:00:00 2001 From: Martchus Date: Sat, 14 May 2016 00:24:01 +0200 Subject: [PATCH] Support FLAC in Ogg --- CMakeLists.txt | 4 + README.md | 4 +- abstractcontainer.cpp | 3 +- flac/flacmetadata.cpp | 127 +++++++++++ flac/flacmetadata.h | 312 ++++++++++++++++++++++++++++ flac/flactooggmappingheader.cpp | 56 +++++ flac/flactooggmappingheader.h | 72 +++++++ genericcontainer.h | 17 +- mp4/mp4tagfield.cpp | 14 +- ogg/oggcontainer.cpp | 130 +++++++++--- ogg/oggcontainer.h | 117 ++++++++++- ogg/oggiterator.cpp | 5 +- ogg/oggpage.h | 10 + ogg/oggstream.cpp | 94 +++++++-- opus/opusidentificationheader.h | 4 +- size.h | 4 +- tag.h | 6 +- tagtarget.cpp | 17 ++ tagtarget.h | 3 - vorbis/vorbiscomment.cpp | 28 ++- vorbis/vorbiscomment.h | 86 ++------ vorbis/vorbiscommentfield.cpp | 54 ++--- vorbis/vorbisidentificationheader.h | 4 +- 23 files changed, 975 insertions(+), 196 deletions(-) create mode 100644 flac/flacmetadata.cpp create mode 100644 flac/flacmetadata.h create mode 100644 flac/flactooggmappingheader.cpp create mode 100644 flac/flactooggmappingheader.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 697c139..6ba5ff4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,8 @@ set(HEADER_FILES ogg/oggpage.h ogg/oggstream.h opus/opusidentificationheader.h + flac/flactooggmappingheader.h + flac/flacmetadata.h positioninset.h signature.h size.h @@ -105,6 +107,8 @@ set(SRC_FILES ogg/oggpage.cpp ogg/oggstream.cpp opus/opusidentificationheader.cpp + flac/flactooggmappingheader.cpp + flac/flacmetadata.cpp signature.cpp statusprovider.cpp tag.cpp diff --git a/README.md b/README.md index cb6c24f..78429d3 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ C++ library for reading and writing MP4 (iTunes), ID3, Vorbis and Matroska tags. The tag library can read and write the following tag formats: - iTunes-style MP4 tags (MP4-DASH is supported) - ID3v1 and ID3v2 tags -- Vorbis and Opus comments (cover art via "METADATA_BLOCK_PICTURE" is supported) in Ogg streams + - conversion between ID3v1 and different versions of ID3v2 +- Vorbis, Opus and FLAC comments in Ogg streams + - cover art via "METADATA_BLOCK_PICTURE" is supported - Matroska/WebM tags and attachments ## File layout options diff --git a/abstractcontainer.cpp b/abstractcontainer.cpp index 931d6b9..a5c824f 100644 --- a/abstractcontainer.cpp +++ b/abstractcontainer.cpp @@ -252,9 +252,10 @@ void AbstractContainer::internalMakeFile() /*! * \brief Creates and returns a tag for the specified \a target. * \remarks - * - If an empty \a target is specified it will be ignored. * - If there is already a tag (for the specified \a target) present, * no new tag will be created. The present tag will be returned instead. + * - If an empty \a target is specified it will be ignored. + * - If targets aren't supported the specified \a target will be ignored. * - If no tag could be created, nullptr is returned. * - The container keeps the ownership over the created tag. */ diff --git a/flac/flacmetadata.cpp b/flac/flacmetadata.cpp new file mode 100644 index 0000000..32a7e06 --- /dev/null +++ b/flac/flacmetadata.cpp @@ -0,0 +1,127 @@ +#include "./flacmetadata.h" + +#include "../exceptions.h" +#include "../tagvalue.h" + +#include +#include +#include +#include +#include + +#include + +using namespace std; +using namespace ConversionUtilities; +using namespace IoUtilities; + +namespace Media { + +/*! + * \class Media::FlacMetaDataBlockHeader + * \brief The FlacMetaDataBlockHeader class is a FLAC "METADATA_BLOCK_HEADER" parser and maker. + * \sa https://xiph.org/flac/format.html + */ + +/*! + * \brief Parses the FLAC "METADATA_BLOCK_HEADER" which is read using the specified \a iterator. + * \remarks The specified \a buffer must be at least 4 bytes long. + */ +void FlacMetaDataBlockHeader::parseHeader(const char *buffer) +{ + m_last = *buffer & 0x80; + m_type = *buffer & (0x80 - 1); + m_dataSize = BE::toUInt24(buffer + 1); +} + +/*! + * \brief Writes the header to the specified \a outputStream. + * \remarks Writes always 4 bytes. + */ +void FlacMetaDataBlockHeader::makeHeader(std::ostream &outputStream) +{ + byte buff[4]; + *buff = (m_last ? (0x80 | m_type) : m_type); + BE::getBytes24(m_dataSize, reinterpret_cast(buff) + 1); + outputStream.write(reinterpret_cast(buff), sizeof(buff)); +} + +/*! + * \class Media::FlacMetaDataBlockStreamInfo + * \brief The FlacMetaDataBlockStreamInfo class is a FLAC "METADATA_BLOCK_STREAMINFO" parser. + * \sa https://xiph.org/flac/format.html + */ + +/*! + * \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. + */ +void FlacMetaDataBlockStreamInfo::parse(const char *buffer) +{ + BitReader reader(buffer, 0x22); + m_minBlockSize = reader.readBits(16); + m_maxBlockSize = reader.readBits(16); + m_minFrameSize = reader.readBits(24); + m_maxFrameSize = reader.readBits(24); + m_samplingFrequency = reader.readBits(20); + m_channelCount = reader.readBits(3) + 1; + m_bitsPerSample = reader.readBits(5) + 1; + m_totalSampleCount = reader.readBits(36); + memcpy(m_md5Sum, buffer + 0x22 - sizeof(m_md5Sum), sizeof(m_md5Sum)); +} + +/*! + * \class Media::FlacMetaDataBlockPicture + * \brief The FlacMetaDataBlockPicture class is a FLAC "METADATA_BLOCK_PICTURE" parser and maker. + * \sa https://xiph.org/flac/format.html + */ + +/*! + * \brief Parses the FLAC "METADATA_BLOCK_PICTURE". + */ +void FlacMetaDataBlockPicture::parse(istream &inputStream) +{ + BinaryReader reader(&inputStream); + m_pictureType = reader.readUInt32BE(); + auto size = reader.readUInt32BE(); + m_value.setMimeType(reader.readString(size)); + size = reader.readUInt32BE(); + 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(); + auto data = make_unique(size); + inputStream.read(data.get(), size); + m_value.assignData(move(data), size, TagDataType::Picture); +} + +/*! + * \brief Returns the number of bytes make() will write. + * \remarks Any changes to the object will invalidate this value. + */ +uint32 FlacMetaDataBlockPicture::requiredSize() const +{ + return 32 + m_value.mimeType().size() + m_value.description().size() + m_value.dataSize(); +} + +/*! + * \brief Makes the FLAC "METADATA_BLOCK_PICTURE". + */ +void FlacMetaDataBlockPicture::make(ostream &outputStream) +{ + BinaryWriter writer(&outputStream); + writer.writeUInt32BE(pictureType()); + writer.writeUInt32BE(m_value.mimeType().size()); + writer.writeString(m_value.mimeType()); + writer.writeUInt32BE(m_value.description().size()); + writer.writeString(m_value.description()); + writer.writeUInt32BE(0); // skip width + writer.writeUInt32BE(0); // skip height + writer.writeUInt32BE(0); // skip color depth + writer.writeUInt32BE(0); // skip number of colors used + writer.writeUInt32BE(m_value.dataSize()); + writer.write(value().dataPointer(), m_value.dataSize()); +} + + +} diff --git a/flac/flacmetadata.h b/flac/flacmetadata.h new file mode 100644 index 0000000..9ba6271 --- /dev/null +++ b/flac/flacmetadata.h @@ -0,0 +1,312 @@ +#ifndef MEDIA_FLACMETADATAHEADER_H +#define MEDIA_FLACMETADATAHEADER_H + +#include +#include + +#include + +namespace Media { + +class TagValue; + +/*! + * \brief The FlacMetaDataBlockType enum specifies the type of FlacMetaDataBlockHeader. + */ +enum class FlacMetaDataBlockType : byte +{ + StreamInfo = 0, + Padding, + Application, + SeekTable, + VorbisComment, + CuseSheet, + Picture +}; + +constexpr bool operator ==(byte lhs, FlacMetaDataBlockType type) +{ + return lhs == static_cast(type); +} + +constexpr bool operator !=(byte lhs, FlacMetaDataBlockType type) +{ + return lhs != static_cast(type); +} + +class LIB_EXPORT FlacMetaDataBlockHeader +{ +public: + FlacMetaDataBlockHeader(); + + void parseHeader(const char *buffer); + void makeHeader(std::ostream &outputStream); + + byte isLast() const; + void setLast(byte last); + byte type() const; + void setType(FlacMetaDataBlockType type); + uint32 dataSize() const; + void setDataSize(uint32 dataSize); + +private: + byte m_last; + byte m_type; + uint32 m_dataSize; +}; + +/*! + * \brief Constructs a new FLAC "METADATA_BLOCK_HEADER". + */ +inline FlacMetaDataBlockHeader::FlacMetaDataBlockHeader() : + m_last(0), + m_type(0), + m_dataSize(0) +{} + +/*! + * \brief Returns whether this is the last metadata block before the audio blocks. + */ +inline byte FlacMetaDataBlockHeader::isLast() const +{ + return m_last; +} + +/*! + * \brief Sets whether this is the last metadata block before the audio blocks. + */ +inline void FlacMetaDataBlockHeader::setLast(byte last) +{ + m_last = last; +} + +/*! + * \brief Returns the block type. + * \sa FlacMetaDataBlockType + */ +inline byte FlacMetaDataBlockHeader::type() const +{ + return m_type; +} + +/*! + * \brief Sets the block type. + */ +inline void FlacMetaDataBlockHeader::setType(FlacMetaDataBlockType type) +{ + m_type = static_cast(type); +} + +/*! + * \brief Returns the length in bytes of the meta data (excluding the size of the header itself). + */ +inline uint32 FlacMetaDataBlockHeader::dataSize() const +{ + return m_dataSize; +} + +/*! + * \brief Sets the length in bytes of the meta data (excluding the size of the header itself). + * \remarks Max value is (2^24 - 1). + */ +inline void FlacMetaDataBlockHeader::setDataSize(uint32 dataSize) +{ + m_dataSize = dataSize; +} + +class LIB_EXPORT FlacMetaDataBlockStreamInfo +{ +public: + FlacMetaDataBlockStreamInfo(); + + void parse(const char *buffer); + + uint16 minBlockSize() const; + uint16 maxBlockSize() const; + uint32 minFrameSize() const; + uint32 maxFrameSize() const; + uint32 samplingFrequency() const; + byte channelCount() const; + byte bitsPerSample() const; + uint64 totalSampleCount() const; + const char *md5Sum() const; + +private: + uint16 m_minBlockSize; + uint16 m_maxBlockSize; + uint32 m_minFrameSize; + uint32 m_maxFrameSize; + uint32 m_samplingFrequency; + byte m_channelCount; + byte m_bitsPerSample; + uint64 m_totalSampleCount; + char m_md5Sum[16]; +}; + +/*! + * \brief Constructs a new FLAC "METADATA_BLOCK_STREAMINFO". + */ +inline FlacMetaDataBlockStreamInfo::FlacMetaDataBlockStreamInfo() : + m_minBlockSize(0), + m_maxBlockSize(0), + m_minFrameSize(0), + m_maxFrameSize(0), + m_samplingFrequency(0), + m_channelCount(0), + m_bitsPerSample(0), + m_totalSampleCount(0), + m_md5Sum{0} +{} + +/*! + * \brief Returns the minimum block size (in samples) used in the stream. + */ +inline uint16 FlacMetaDataBlockStreamInfo::minBlockSize() const +{ + return m_minBlockSize; +} + +/*! + * \brief Returns the maximum block size (in samples) used in the stream. + * + * (Minimum blocksize == maximum blocksize) implies a fixed-blocksize stream. + */ +inline uint16 FlacMetaDataBlockStreamInfo::maxBlockSize() const +{ + return m_maxBlockSize; +} + +/*! + * \brief Returns the minimum frame size (in bytes) used in the stream. + * + * May be 0 to imply the value is not known. + */ +inline uint32 FlacMetaDataBlockStreamInfo::minFrameSize() const +{ + return m_minFrameSize; +} + +/*! + * \brief The maximum frame size (in bytes) used in the stream. + * + * May be 0 to imply the value is not known. + */ +inline uint32 FlacMetaDataBlockStreamInfo::maxFrameSize() const +{ + return m_maxFrameSize; +} + +/*! + * \brief Returns the sampling frequency in Hz. + * + * Though 20 bits are available, the maximum sample rate is limited by the + * structure of frame headers to 655350Hz. Also, a value of 0 is invalid. + */ +inline uint32 FlacMetaDataBlockStreamInfo::samplingFrequency() const +{ + return m_samplingFrequency; +} + +/*! + * \brief Returns the number of channels. + * + * FLAC supports from 1 to 8 channels . + */ +inline byte FlacMetaDataBlockStreamInfo::channelCount() const +{ + return m_channelCount; +} + +/*! + * \brief Returns the bits per sample. + * + * FLAC supports from 4 to 32 bits per sample. + * Currently the reference encoder and decoders only support up + * to 24 bits per sample. + */ +inline byte FlacMetaDataBlockStreamInfo::bitsPerSample() const +{ + return m_bitsPerSample; +} + +/*! + * \brief Returns the total samples in stream. + * + * 'Samples' means inter-channel sample, i.e. one second of 44.1Khz audio + * will have 44100 samples regardless of the number of channels. + * + * A value of zero here means the number of total samples is unknown. + */ +inline uint64 FlacMetaDataBlockStreamInfo::totalSampleCount() const +{ + return m_totalSampleCount; +} + +/*! + * \brief Returns the MD5 signature of the unencoded audio data. + * + * This allows the decoder to determine if an error exists in the + * audio data even when the error does not result in an invalid bitstream. + */ +inline const char *FlacMetaDataBlockStreamInfo::md5Sum() const +{ + return m_md5Sum; +} + +class LIB_EXPORT FlacMetaDataBlockPicture +{ +public: + FlacMetaDataBlockPicture(TagValue &tagValue); + + void parse(std::istream &inputStream); + uint32 requiredSize() const; + void make(std::ostream &outputStream); + + uint32 pictureType() const; + void setPictureType(uint32 pictureType); + TagValue &value(); + +private: + uint32 m_pictureType; + TagValue &m_value; +}; + +/*! + * \brief Constructs a new FLAC "METADATA_BLOCK_PICTURE". + * + * The picture is read from/stored to the specified \a tagValue. + * The FlacMetaDataBlockPicture does not take ownership over + * the specified \a tagValue. + */ +inline FlacMetaDataBlockPicture::FlacMetaDataBlockPicture(TagValue &tagValue) : + m_pictureType(0), + m_value(tagValue) +{} + +/*! + * \brief Returns the picture type according to the ID3v2 APIC frame. + */ +inline uint32 FlacMetaDataBlockPicture::pictureType() const +{ + return m_pictureType; +} + +/*! + * \brief Sets the picture type according to the ID3v2 APIC frame. + */ +inline void FlacMetaDataBlockPicture::setPictureType(uint32 pictureType) +{ + m_pictureType = pictureType; +} + +/*! + * \brief Returns the tag value the picture is read from/stored to. + */ +inline TagValue &FlacMetaDataBlockPicture::value() +{ + return m_value; +} + +} + +#endif // MEDIA_FLACMETADATAHEADER_H diff --git a/flac/flactooggmappingheader.cpp b/flac/flactooggmappingheader.cpp new file mode 100644 index 0000000..deb5fc2 --- /dev/null +++ b/flac/flactooggmappingheader.cpp @@ -0,0 +1,56 @@ +#include "./flactooggmappingheader.h" + +#include "../ogg/oggiterator.h" + +#include "../exceptions.h" + +#include + +using namespace std; +using namespace ConversionUtilities; + +namespace Media { + +/*! + * \class Media::FlacToOggMappingHeader + * \brief The FlacToOggMappingHeader class is a FLAC-to-Ogg mapping header parser. + * \sa https://xiph.org/flac/ogg_mapping.html + */ + +/*! + * \brief Parses the FLAC-to-Ogg mapping header which is read using the specified \a iterator. + * \remarks The header is assumed to start at the current position of \a iterator. + */ +void FlacToOggMappingHeader::parseHeader(OggIterator &iterator) +{ + // prepare parsing + char buff[0x0D + 0x04 + 0x22 - 0x05]; + iterator.read(buff, 5); + if(*buff != 0x7Fu || BE::toUInt32(buff + 1) != 0x464C4143u) { + throw InvalidDataException(); // not FLAC-to-Ogg mapping header + } + iterator.read(buff, sizeof(buff)); + + // parse FLAC-to-Ogg mapping header + m_majorVersion = static_cast(*(buff + 0x00)); + m_minorVersion = static_cast(*(buff + 0x01)); + m_headerCount = BE::toUInt16(buff + 0x02); + if(BE::toUInt32(buff + 0x04) != 0x664C6143u) { + throw InvalidDataException(); // native FLAC signature not present + } + + // parse "METADATA_BLOCK_HEADER" + FlacMetaDataBlockHeader header; + header.parseHeader(buff + 0x0D - 0x05); + if(header.type() != FlacMetaDataBlockType::StreamInfo) { + throw InvalidDataException(); // "METADATA_BLOCK_STREAMINFO" expected + } + if(header.dataSize() < 0x22) { + throw TruncatedDataException(); // "METADATA_BLOCK_STREAMINFO" is truncated + } + + // parse "METADATA_BLOCK_STREAMINFO" + m_streamInfo.parse(buff + 0x0D + 0x04 - 0x05); +} + +} diff --git a/flac/flactooggmappingheader.h b/flac/flactooggmappingheader.h new file mode 100644 index 0000000..9d57ade --- /dev/null +++ b/flac/flactooggmappingheader.h @@ -0,0 +1,72 @@ +#ifndef MEDIA_FLACIDENTIFICATIONHEADER_H +#define MEDIA_FLACIDENTIFICATIONHEADER_H + +#include "./flacmetadata.h" + +namespace Media { + +class OggIterator; + +class LIB_EXPORT FlacToOggMappingHeader +{ +public: + FlacToOggMappingHeader(); + + void parseHeader(OggIterator &iterator); + + byte majorVersion() const; + byte minorVersion() const; + uint16 headerCount() const; + const FlacMetaDataBlockStreamInfo &streamInfo() const; + +private: + byte m_majorVersion; + byte m_minorVersion; + uint16 m_headerCount; + FlacMetaDataBlockStreamInfo m_streamInfo; +}; + +/*! + * \brief Constructs a new FLAC identification header. + */ +inline FlacToOggMappingHeader::FlacToOggMappingHeader() : + m_majorVersion(0), + m_minorVersion(0), + m_headerCount(0) +{} + +/*! + * \brief Returns the major version for the mapping (which should be 1 currently). + */ +inline byte FlacToOggMappingHeader::majorVersion() const +{ + return m_majorVersion; +} + +/*! + * \brief Returns the version for the mapping (which should be 0 currently). + */ +inline byte FlacToOggMappingHeader::minorVersion() const +{ + return m_minorVersion; +} + +/*! + * \brief Returns the number of header (non-audio) packets, not including this one. + */ +inline uint16 FlacToOggMappingHeader::headerCount() const +{ + return m_headerCount; +} + +/*! + * \brief Returns the stream info. + */ +inline const FlacMetaDataBlockStreamInfo &FlacToOggMappingHeader::streamInfo() const +{ + return m_streamInfo; +} + +} + +#endif // MEDIA_FLACIDENTIFICATIONHEADER_H diff --git a/genericcontainer.h b/genericcontainer.h index b04c644..c01c8bb 100644 --- a/genericcontainer.h +++ b/genericcontainer.h @@ -247,15 +247,20 @@ inline std::vector > &GenericContainer TagType *GenericContainer::createTag(const TagTarget &target) { - if(!target.isEmpty()) { - for(auto &tag : m_tags) { - if(tag->target() == target) { - return tag.get(); + // check whether a tag matching the specified target is already assigned + if(!m_tags.empty()) { + if(!target.isEmpty() && m_tags.front()->supportsTarget()) { + for(auto &tag : m_tags) { + if(tag->target() == target) { + return tag.get(); + } } + } else { + return m_tags.front().get(); } - } else if(!m_tags.empty()) { - return m_tags.front().get(); } + + // a new tag must be created m_tags.emplace_back(std::make_unique()); auto &tag = m_tags.back(); tag->setTarget(target); diff --git a/mp4/mp4tagfield.cpp b/mp4/mp4tagfield.cpp index 3ceae9a..800f917 100644 --- a/mp4/mp4tagfield.cpp +++ b/mp4/mp4tagfield.cpp @@ -413,11 +413,17 @@ Mp4TagFieldMaker::Mp4TagFieldMaker(Mp4TagField &field) : try { // try to use appropriate raw data type m_rawDataType = m_field.appropriateRawDataType(); - } catch(Failure &) { + } catch(const Failure &) { // unable to obtain appropriate raw data type - // assume utf-8 text - m_rawDataType = RawDataType::Utf8; - m_field.addNotification(NotificationType::Warning, "It was not possible to find an appropriate raw data type id. UTF-8 will be assumed.", context); + if(m_field.id() == Mp4TagAtomIds::Cover) { + // assume JPEG image + m_rawDataType = RawDataType::Utf8; + m_field.addNotification(NotificationType::Warning, "It was not possible to find an appropriate raw data type id. JPEG image will be assumed.", context); + } else { + // assume UTF-8 text + m_rawDataType = RawDataType::Utf8; + m_field.addNotification(NotificationType::Warning, "It was not possible to find an appropriate raw data type id. UTF-8 will be assumed.", context); + } } try { diff --git a/ogg/oggcontainer.cpp b/ogg/oggcontainer.cpp index 449a39b..d246984 100644 --- a/ogg/oggcontainer.cpp +++ b/ogg/oggcontainer.cpp @@ -1,5 +1,7 @@ #include "./oggcontainer.h" +#include "../flac/flacmetadata.h" + #include "../mediafileinfo.h" #include "../backuphelper.h" @@ -11,6 +13,25 @@ using namespace IoUtilities; namespace Media { +/*! + * \class Media::OggVorbisComment + * \brief Specialization of Media::VorbisComment for Vorbis comments inside an OGG stream. + */ + +const char *OggVorbisComment::typeName() const +{ + switch(m_oggParams.streamFormat) { + case GeneralMediaFormat::Flac: + return "Vorbis comment (in FLAC stream)"; + case GeneralMediaFormat::Opus: + return "Vorbis comment (in Opus stream)"; + case GeneralMediaFormat::Theora: + return "Vorbis comment (in Theora stream)"; + default: + return "Vorbis comment"; + } +} + /*! * \class Media::OggContainer * \brief Implementation of Media::AbstractContainer for OGG files. @@ -20,7 +41,7 @@ namespace Media { * \brief Constructs a new container for the specified \a stream at the specified \a startOffset. */ OggContainer::OggContainer(MediaFileInfo &fileInfo, uint64 startOffset) : - GenericContainer(fileInfo, startOffset), + GenericContainer(fileInfo, startOffset), m_iterator(fileInfo.stream(), startOffset, fileInfo.size()), m_validateChecksums(false) {} @@ -36,28 +57,36 @@ void OggContainer::reset() /*! * \brief Creates a new tag. * \sa AbstractContainer::createTag() - * \remarks Tracks must be parsed before because tags are stored on track level! + * \remarks + * - Tracks must be parsed before because tags are stored on track level! + * - The track can be specified via the \a target argument. However, only the first track of tracks() array is considered. + * - If tracks() array of \a target is empty, a random track will be picked. + * - Vorbis streams should always have a tag assigned yet. However, this + * methods allows creation of a tag if none has been assigned yet. + * - FLAC streams should always have a tag assigned yet and this method + * does NOT allow to create a tag in this case. */ -VorbisComment *OggContainer::createTag(const TagTarget &target) +OggVorbisComment *OggContainer::createTag(const TagTarget &target) { - if(!target.isEmpty()) { - // targets are not supported here, so the specified target should be empty - // -> just be consistent with generic implementation here + if(!target.tracks().empty()) { + // return the tag for the first matching track ID for(auto &tag : m_tags) { - if(tag->target() == target && !tag->oggParams().removed) { + if(!tag->target().tracks().empty() && tag->target().tracks().front() == target.tracks().front() && !tag->oggParams().removed) { return tag.get(); } } + // not tag found -> try to re-use a tag which has been flagged as removed for(auto &tag : m_tags) { - if(tag->target() == target) { + if(!tag->target().tracks().empty() && tag->target().tracks().front() == target.tracks().front()) { tag->oggParams().removed = false; return tag.get(); } } - } else if(VorbisComment *comment = tag(0)) { - comment->oggParams().removed = false; + } else if(OggVorbisComment *comment = tag(0)) { + // no track ID specified -> just return the first tag (if one exists) return comment; } else if(!m_tags.empty()) { + // no track ID specified -> just return the first tag (try to re-use a tag which has been flagged as removed) m_tags.front()->oggParams().removed = false; return m_tags.front().get(); } @@ -65,27 +94,29 @@ VorbisComment *OggContainer::createTag(const TagTarget &target) // a new tag needs to be created // -> determine an appropriate track for the tag // -> just use the first Vorbis/Opus track - // -> TODO: provide interface for specifying a specific track for(const auto &track : m_tracks) { - switch(track->format().general) { - case GeneralMediaFormat::Vorbis: - case GeneralMediaFormat::Opus: - // check whether start page has a valid value - if(track->startPage() < m_iterator.pages().size()) { - ariseComment(track->startPage(), static_cast(-1), track->format().general); - m_tags.back()->setTarget(target); // also for consistency - return m_tags.back().get(); - } else { - // TODO: error handling? + if(target.tracks().empty() || target.tracks().front() == track->id()) { + switch(track->format().general) { + case GeneralMediaFormat::Vorbis: + case GeneralMediaFormat::Opus: + // check whether start page has a valid value + if(track->startPage() < m_iterator.pages().size()) { + announceComment(track->startPage(), static_cast(-1), false, track->format().general); + m_tags.back()->setTarget(target); + return m_tags.back().get(); + } else { + // TODO: error handling? + } + default: + ; } - default: - ; + // TODO: allow adding tags to FLAC tracks (not really important, because a tag should always be present) } } return nullptr; } -VorbisComment *OggContainer::tag(size_t index) +OggVorbisComment *OggContainer::tag(size_t index) { size_t i = 0; for(const auto &tag : m_tags) { @@ -205,7 +236,10 @@ void OggContainer::internalParseTags() case GeneralMediaFormat::Opus: // skip header (has already been detected by OggStream) m_iterator.seekForward(8); - comment->parse(m_iterator, true); + comment->parse(m_iterator, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte); + break; + case GeneralMediaFormat::Flac: + comment->parse(m_iterator, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, 4); break; default: addNotification(NotificationType::Critical, "Stream format not supported.", "parsing tags from OGG streams"); @@ -215,10 +249,21 @@ void OggContainer::internalParseTags() } } -void OggContainer::ariseComment(std::size_t pageIndex, std::size_t segmentIndex, GeneralMediaFormat mediaFormat) +/*! + * \brief Announces the existence of a Vorbis comment. + * + * The start offset of the comment is specified by \a pageIndex and \a segmentIndex. + * + * The format of the stream the comment belongs to is specified by \a mediaFormat. + * Valid values are GeneralMediaFormat::Vorbis, GeneralMediaFormat::Opus + * and GeneralMediaFormat::Flac. + * + * \remarks This method is called by OggStream when parsing the header. + */ +void OggContainer::announceComment(std::size_t pageIndex, std::size_t segmentIndex, bool lastMetaDataBlock, GeneralMediaFormat mediaFormat) { - m_tags.emplace_back(make_unique()); - m_tags.back()->oggParams().set(pageIndex, segmentIndex, mediaFormat); + m_tags.emplace_back(make_unique()); + m_tags.back()->oggParams().set(pageIndex, segmentIndex, lastMetaDataBlock, mediaFormat); } void OggContainer::internalParseTracks() @@ -240,7 +285,7 @@ void OggContainer::internalParseTracks() * \brief Writes the specified \a comment with the given \a params to the specified \a buffer and * adds the number of bytes written to \a newSegmentSizes. */ -void makeVorbisCommentSegment(stringstream &buffer, CopyHelper<65307> ©Helper, vector &newSegmentSizes, VorbisComment *comment, OggParameter *params) +void OggContainer::makeVorbisCommentSegment(stringstream &buffer, CopyHelper<65307> ©Helper, vector &newSegmentSizes, VorbisComment *comment, OggParameter *params) { auto offset = buffer.tellp(); switch(params->streamFormat) { @@ -250,9 +295,29 @@ void makeVorbisCommentSegment(stringstream &buffer, CopyHelper<65307> ©Helpe case GeneralMediaFormat::Opus: ConversionUtilities::BE::getBytes(0x4F70757354616773u, copyHelper.buffer()); buffer.write(copyHelper.buffer(), 8); - comment->make(buffer, true); + comment->make(buffer, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte); break; - default: + case GeneralMediaFormat::Flac: { + // Vorbis comment must be wrapped in "METADATA_BLOCK_HEADER" + FlacMetaDataBlockHeader header; + header.setLast(params->lastMetaDataBlock); + header.setType(FlacMetaDataBlockType::VorbisComment); + + // write the header later, when the size is known + buffer.write(copyHelper.buffer(), 4); + + comment->make(buffer, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte); + + // finally make the header + header.setDataSize(buffer.tellp() - offset - 4); + if(header.dataSize() > 0xFFFFFF) { + addNotification(NotificationType::Critical, "Size of Vorbis comment exceeds size limit for FLAC \"METADATA_BLOCK_HEADER\".", "making Vorbis Comment"); + } + buffer.seekp(offset); + header.makeHeader(buffer); + buffer.seekp(header.dataSize(), ios_base::cur); + break; + } default: ; } newSegmentSizes.push_back(buffer.tellp() - offset); @@ -291,7 +356,7 @@ void OggContainer::internalMakeFile() try { // prepare iterating comments - VorbisComment *currentComment; + OggVorbisComment *currentComment; OggParameter *currentParams; auto tagIterator = m_tags.cbegin(), tagEnd = m_tags.cend(); if(tagIterator != tagEnd) { @@ -428,6 +493,7 @@ void OggContainer::internalMakeFile() stream().seekp(segmentSizesWritten, ios_base::cur); // -> write actual page data copyHelper.copy(buffer, stream(), currentSize); + ++pageSequenceNumber; } } diff --git a/ogg/oggcontainer.h b/ogg/oggcontainer.h index 434694f..9afeec5 100644 --- a/ogg/oggcontainer.h +++ b/ogg/oggcontainer.h @@ -12,11 +12,119 @@ #include #include +namespace IoUtilities { +template +class CopyHelper; +} + namespace Media { class MediaFileInfo; +class OggContainer; -class LIB_EXPORT OggContainer : public GenericContainer +/*! + * \brief The OggParameter struct holds the OGG parameter for a VorbisComment. + */ +struct LIB_EXPORT OggParameter +{ + OggParameter(); + void set(std::size_t pageIndex, std::size_t segmentIndex, bool lastMetaDataBlock, GeneralMediaFormat streamFormat = GeneralMediaFormat::Vorbis); + + std::size_t firstPageIndex; + std::size_t firstSegmentIndex; + std::size_t lastPageIndex; + std::size_t lastSegmentIndex; + bool lastMetaDataBlock; + GeneralMediaFormat streamFormat; + bool removed; +}; + +/*! + * \brief Creates new parameters. + * \remarks The OggContainer class is responsible for assigning sane values. + */ +inline OggParameter::OggParameter() : + firstPageIndex(0), + firstSegmentIndex(0), + lastPageIndex(0), + lastSegmentIndex(0), + lastMetaDataBlock(false), + streamFormat(GeneralMediaFormat::Vorbis), + removed(false) +{} + +/*! + * \brief Sets the firstPageIndex/lastPageIndex, the firstSegmentIndex/lastSegmentIndex, whether the associated meta data block is the last one and the streamFormat. + * \remarks Whether the associated meta data block is the last one is only relevant for FLAC streams. + */ +inline void OggParameter::set(std::size_t pageIndex, std::size_t segmentIndex, bool lastMetaDataBlock, GeneralMediaFormat streamFormat) +{ + firstPageIndex = lastPageIndex = pageIndex; + firstSegmentIndex = lastSegmentIndex = segmentIndex; + this->lastMetaDataBlock = lastMetaDataBlock; + this->streamFormat = streamFormat; +} + +class LIB_EXPORT OggVorbisComment : public VorbisComment +{ + friend class OggContainer; + +public: + OggVorbisComment(); + TagType type() const; + const char *typeName() const; + bool supportsTarget() const; + + OggParameter &oggParams(); + const OggParameter &oggParams() const; + +private: + OggParameter m_oggParams; +}; + +/*! + * \brief Constructs a new OGG Vorbis comment. + */ +inline OggVorbisComment::OggVorbisComment() +{} + +inline TagType OggVorbisComment::type() const +{ + return TagType::OggVorbisComment; +} + +/*! + * \brief Returns true; the target is used to specifiy the stream. + * \sa OggContainer::createTag(), TagTarget + */ +inline bool OggVorbisComment::supportsTarget() const +{ + return true; +} + +/*! + * \brief Returns the OGG parameter for the comment. + * + * Consists of first page index, first segment index, last page index, last segment index and tag index (in this order). + * These values are used and managed by the OggContainer class and do not affect the behavior of the VorbisComment instance. + */ +inline OggParameter &OggVorbisComment::oggParams() +{ + return m_oggParams; +} + +/*! + * \brief Returns the OGG parameter for the comment. + * + * Consists of first page index, first segment index, last page index, last segment index and tag index (in this order). + * These values are used and managed by the OggContainer class and do not affect the behavior of the VorbisComment instance. + */ +inline const OggParameter &OggVorbisComment::oggParams() const +{ + return m_oggParams; +} + +class LIB_EXPORT OggContainer : public GenericContainer { friend class OggStream; @@ -28,8 +136,8 @@ public: void setChecksumValidationEnabled(bool enabled); void reset(); - VorbisComment *createTag(const TagTarget &target); - VorbisComment *tag(std::size_t index); + OggVorbisComment *createTag(const TagTarget &target); + OggVorbisComment *tag(std::size_t index); std::size_t tagCount() const; bool removeTag(Tag *tag); void removeAllTags(); @@ -41,7 +149,8 @@ protected: void internalMakeFile(); private: - void ariseComment(std::size_t pageIndex, std::size_t segmentIndex, GeneralMediaFormat mediaFormat = GeneralMediaFormat::Vorbis); + void announceComment(std::size_t pageIndex, std::size_t segmentIndex, bool lastMetaDataBlock, GeneralMediaFormat mediaFormat = GeneralMediaFormat::Vorbis); + void makeVorbisCommentSegment(std::stringstream &buffer, IoUtilities::CopyHelper<65307> ©Helper, std::vector &newSegmentSizes, VorbisComment *comment, OggParameter *params); std::unordered_map >::size_type> m_streamsBySerialNo; diff --git a/ogg/oggiterator.cpp b/ogg/oggiterator.cpp index 12a3dc0..9936322 100644 --- a/ogg/oggiterator.cpp +++ b/ogg/oggiterator.cpp @@ -161,8 +161,9 @@ void OggIterator::read(char *buffer, size_t count) */ void OggIterator::seekForward(size_t count) { - while(*this && count) { - uint32 available = currentSegmentSize() - m_bytesRead; + uint32 available = currentSegmentSize() - m_bytesRead; + while(*this) { + available = currentSegmentSize() - m_bytesRead; if(count <= available) { m_bytesRead += count; return; diff --git a/ogg/oggpage.h b/ogg/oggpage.h index 0f1d6c9..03747de 100644 --- a/ogg/oggpage.h +++ b/ogg/oggpage.h @@ -28,6 +28,7 @@ public: bool isLastPage() const; uint64 absoluteGranulePosition() const; uint32 streamSerialNumber() const; + bool matchesStreamSerialNumber(uint32 streamSerialNumber) const; uint32 sequenceNumber() const; uint32 checksum() const; byte segmentTableSize() const; @@ -155,6 +156,15 @@ inline uint32 OggPage::streamSerialNumber() const return m_streamSerialNumber; } +/*! + * \brief Returns whether the stream serial number of the current instance matches the specified one. + * \sa streamSerialNumber() + */ +inline bool OggPage::matchesStreamSerialNumber(uint32 streamSerialNumber) const +{ + return m_streamSerialNumber == streamSerialNumber; +} + /*! * \brief Returns the page sequence number. * diff --git a/ogg/oggstream.cpp b/ogg/oggstream.cpp index 0e55f87..4220314 100644 --- a/ogg/oggstream.cpp +++ b/ogg/oggstream.cpp @@ -6,6 +6,8 @@ #include "../opus/opusidentificationheader.h" +#include "../flac/flactooggmappingheader.h" + #include "../mediafileinfo.h" #include "../exceptions.h" #include "../mediaformat.h" @@ -13,8 +15,10 @@ #include #include +#include using namespace std; +using namespace std::placeholders; using namespace ChronoUtilities; namespace Media { @@ -51,13 +55,15 @@ void OggStream::internalParseHeader() m_id = firstPage.streamSerialNumber(); // ensure iterator is setup properly - iterator.setFilter(m_id); + iterator.setFilter(firstPage.streamSerialNumber()); iterator.setPageIndex(m_startPage); + // predicate for finding pages of this stream by its stream serial number + const auto pred = bind(&OggPage::matchesStreamSerialNumber, _1, firstPage.streamSerialNumber()); + // iterate through segments using OggIterator - bool hasIdentificationHeader = false; - bool hasCommentHeader = false; - for(; iterator; ++iterator) { + // -> iterate through ALL segments to calculate the precise stream size (hence the out-commented part in the loop-condition) + for(bool hasIdentificationHeader = false, hasCommentHeader = false; iterator /* && (!hasIdentificationHeader && !hasCommentHeader) */; ++iterator) { const uint32 currentSize = iterator.currentSegmentSize(); m_size += currentSize; @@ -78,7 +84,9 @@ void OggStream::internalParseHeader() break; default: addNotification(NotificationType::Warning, "Stream format is inconsistent.", context); + continue; } + // check header type switch(sig >> 56) { case VorbisPackageTypes::Identification: @@ -99,12 +107,9 @@ void OggStream::internalParseHeader() } // determine sample count and duration if all pages have been fetched if(iterator.areAllPagesFetched()) { - auto pred = [this] (const OggPage &page) -> bool { - return page.streamSerialNumber() == this->id(); - }; const auto &pages = iterator.pages(); - auto firstPage = find_if(pages.cbegin(), pages.cend(), pred); - auto lastPage = find_if(pages.crbegin(), pages.crend(), pred); + const auto firstPage = find_if(pages.cbegin(), pages.cend(), pred); + const auto lastPage = find_if(pages.crbegin(), pages.crend(), pred); if(firstPage != pages.cend() && lastPage != pages.crend()) { m_sampleCount = lastPage->absoluteGranulePosition() - firstPage->absoluteGranulePosition(); m_duration = TimeSpan::fromSeconds(static_cast(m_sampleCount) / m_samplingFrequency); @@ -118,7 +123,7 @@ void OggStream::internalParseHeader() case VorbisPackageTypes::Comments: // Vorbis comment found -> notify container about comment if(!hasCommentHeader) { - m_container.ariseComment(iterator.currentPageIndex(), iterator.currentSegmentIndex(), GeneralMediaFormat::Vorbis); + m_container.announceComment(iterator.currentPageIndex(), iterator.currentSegmentIndex(), false, GeneralMediaFormat::Vorbis); hasCommentHeader = true; } else { addNotification(NotificationType::Critical, "Vorbis comment header appears more then once. Oversupplied occurrence will be ignored.", context); @@ -142,6 +147,7 @@ void OggStream::internalParseHeader() break; default: addNotification(NotificationType::Warning, "Stream format is inconsistent.", context); + continue; } if(!hasIdentificationHeader) { // parse identification header @@ -152,12 +158,9 @@ void OggStream::internalParseHeader() m_samplingFrequency = ind.sampleRate(); // determine sample count and duration if all pages have been fetched if(iterator.areAllPagesFetched()) { - auto pred = [this] (const OggPage &page) -> bool { - return page.streamSerialNumber() == this->id(); - }; const auto &pages = iterator.pages(); - auto firstPage = find_if(pages.cbegin(), pages.cend(), pred); - auto lastPage = find_if(pages.crbegin(), pages.crend(), pred); + const auto firstPage = find_if(pages.cbegin(), pages.cend(), pred); + const auto lastPage = find_if(pages.crbegin(), pages.crend(), pred); if(firstPage != pages.cend() && lastPage != pages.crend()) { m_sampleCount = lastPage->absoluteGranulePosition() - firstPage->absoluteGranulePosition(); // must apply "pre-skip" here do calculate effective sample count and duration? @@ -186,15 +189,64 @@ void OggStream::internalParseHeader() break; default: addNotification(NotificationType::Warning, "Stream format is inconsistent.", context); + continue; } + // notify container about comment if(!hasCommentHeader) { - m_container.ariseComment(iterator.currentPageIndex(), iterator.currentSegmentIndex(), GeneralMediaFormat::Opus); + m_container.announceComment(iterator.currentPageIndex(), iterator.currentSegmentIndex(), false, GeneralMediaFormat::Opus); hasCommentHeader = true; } else { addNotification(NotificationType::Critical, "Opus tags/comment header appears more then once. Oversupplied occurrence will be ignored.", context); } + } else if((sig & 0xFFFFFFFFFF000000u) == 0x7F464C4143000000u) { + // FLAC header detected + // set FLAC as format + switch(m_format.general) { + case GeneralMediaFormat::Unknown: + m_format = GeneralMediaFormat::Flac; + m_mediaType = MediaType::Audio; + break; + case GeneralMediaFormat::Flac: + break; + default: + addNotification(NotificationType::Warning, "Stream format is inconsistent.", context); + continue; + } + + if(!hasIdentificationHeader) { + // parse FLAC-to-Ogg mapping header + FlacToOggMappingHeader mapping; + const FlacMetaDataBlockStreamInfo &streamInfo = mapping.streamInfo(); + mapping.parseHeader(iterator); + m_bitsPerSample = streamInfo.bitsPerSample(); + m_channelCount = streamInfo.channelCount(); + m_samplingFrequency = streamInfo.samplingFrequency(); + m_sampleCount = streamInfo.totalSampleCount(); + hasIdentificationHeader = true; + } else { + addNotification(NotificationType::Critical, "FLAC-to-Ogg mapping header appears more then once. Oversupplied occurrence will be ignored.", context); + } + + if(!hasCommentHeader) { + // a Vorbis comment should be following + if(++iterator) { + char buff[4]; + iterator.read(buff, 4); + FlacMetaDataBlockHeader header; + header.parseHeader(buff); + if(header.type() == FlacMetaDataBlockType::VorbisComment) { + m_container.announceComment(iterator.currentPageIndex(), iterator.currentSegmentIndex(), header.isLast(), GeneralMediaFormat::Flac); + hasCommentHeader = true; + } else { + addNotification(NotificationType::Critical, "OGG page after FLAC-to-Ogg mapping header doesn't contain Vorbis comment.", context); + } + } else { + addNotification(NotificationType::Critical, "No more OGG pages after FLAC-to-Ogg mapping header (Vorbis comment expected).", context); + } + } + } else if((sig & 0x00ffffffffffff00u) == 0x007468656F726100u) { // Theora header detected // set Theora as format @@ -207,10 +259,18 @@ void OggStream::internalParseHeader() break; default: addNotification(NotificationType::Warning, "Stream format is inconsistent.", context); + continue; } // TODO: read more information about Theora stream + } // currently only Vorbis, Opus and Theora can be detected, TODO: detect more formats - } // TODO: reduce code duplication + + } else { + // just ignore segments of only 8 byte or even less + // TODO: print warning? + } + + // TODO: reduce code duplication } if(m_duration.isNull() && m_size && m_bitrate) { diff --git a/opus/opusidentificationheader.h b/opus/opusidentificationheader.h index 7218566..1ed63e4 100644 --- a/opus/opusidentificationheader.h +++ b/opus/opusidentificationheader.h @@ -4,8 +4,6 @@ #include #include -#include - namespace Media { class OggIterator; @@ -34,7 +32,7 @@ private: }; /*! - * \brief Constructs a new vorbis identification header. + * \brief Constructs a new Opus identification header. */ inline OpusIdentificationHeader::OpusIdentificationHeader() : m_version(0), diff --git a/size.h b/size.h index ef9b1a6..609f905 100644 --- a/size.h +++ b/size.h @@ -24,7 +24,7 @@ public: void setHeight(uint32 value); bool constexpr isNull() const; - bool constexpr operator==(const Size &other); + bool constexpr operator==(const Size &other) const; std::string toString() const; private: @@ -91,7 +91,7 @@ inline constexpr bool Size::isNull() const /*! * \brief Returns whether this instance equals \a other. */ -inline constexpr bool Size::operator==(const Size &other) +inline constexpr bool Size::operator==(const Size &other) const { return (m_width == other.m_width) && (m_height == other.m_height); } diff --git a/tag.h b/tag.h index 39795a5..9b46ade 100644 --- a/tag.h +++ b/tag.h @@ -26,7 +26,8 @@ enum class TagType : unsigned int Id3v2Tag = 0x02, /**< The tag is a Media::Id3v2Tag. */ Mp4Tag = 0x04, /**< The tag is a Media::Mp4Tag. */ MatroskaTag = 0x08, /**< The tag is a Media::MatroskaTag. */ - VorbisComment = 0x10 /**< The tag is a Media::VorbisComment. */ + VorbisComment = 0x10, /**< The tag is a Media::VorbisComment. */ + OggVorbisComment = 0x20 /**< The tag is a Media::OggVorbisComment. */ }; /*! @@ -216,8 +217,7 @@ inline uint32 Tag::size() const } /*! - * \brief Returns an indication whether a target is supported - * by the tag. + * \brief Returns an indication whether a target is supported by the tag. * * If no target is supported, setting a target using setTarget() * has no effect when saving the tag. diff --git a/tagtarget.cpp b/tagtarget.cpp index cef9dfa..af41c3a 100644 --- a/tagtarget.cpp +++ b/tagtarget.cpp @@ -11,6 +11,23 @@ using namespace ConversionUtilities; namespace Media { +/*! + * \class Media::TagTarget + * \brief The TagTarget class specifies the target of a tag. + * + * Tags might only target a specific track, chapter, ... + * + * Specifying a target is currently only fully supported by Matroska. + * + * Since Ogg saves tags at stream level, the stream can be specified + * by passing a TagTarget instance to OggContainer::createTag(). + * However, only the first track in the tracks() array is considered + * and any other values are just ignored. + * + * In any other tag formats, the specified target is (currently) + * completely ignored. + */ + /*! * \brief Returns the string representation of the current instance. */ diff --git a/tagtarget.h b/tagtarget.h index b6b0e59..b747070 100644 --- a/tagtarget.h +++ b/tagtarget.h @@ -9,9 +9,6 @@ namespace Media { -/*! - * \brief The TagTarget class stores target information. - */ class LIB_EXPORT TagTarget { public: diff --git a/vorbis/vorbiscomment.cpp b/vorbis/vorbiscomment.cpp index 3a9a69d..6c6c6d5 100644 --- a/vorbis/vorbiscomment.cpp +++ b/vorbis/vorbiscomment.cpp @@ -20,7 +20,7 @@ namespace Media { /*! * \class Media::VorbisComment - * \brief Implementation of Media::Tag for the Vorbis comment. + * \brief Implementation of Media::Tag for Vorbis comments. */ const TagValue &VorbisComment::value(KnownField field) const @@ -106,16 +106,17 @@ KnownField VorbisComment::knownField(const string &id) const * \throws Throws Media::Failure or a derived exception when a parsing * error occurs. */ -void VorbisComment::parse(OggIterator &iterator, bool skipSignature) +void VorbisComment::parse(OggIterator &iterator, VorbisCommentFlags flags, size_t offset) { // prepare parsing invalidateStatus(); static const string context("parsing Vorbis comment"); - iterator.stream().seekg(iterator.currentSegmentOffset()); - auto startOffset = iterator.currentSegmentOffset(); + auto startOffset = iterator.currentSegmentOffset() + offset; + iterator.seekForward(offset); try { // read signature: 0x3 + "vorbis" char sig[8]; + bool skipSignature = flags & VorbisCommentFlags::NoSignature; if(!skipSignature) { iterator.read(sig, 7); skipSignature = (ConversionUtilities::BE::toUInt64(sig) & 0xffffffffffffff00u) == 0x03766F7262697300u; @@ -124,11 +125,12 @@ void VorbisComment::parse(OggIterator &iterator, bool skipSignature) // read vendor (length prefixed string) { iterator.read(sig, 4); - auto vendorSize = LE::toUInt32(sig); + const auto vendorSize = LE::toUInt32(sig); if(iterator.currentCharacterOffset() + vendorSize <= iterator.streamSize()) { auto buff = make_unique(vendorSize); iterator.read(buff.get(), vendorSize); - m_vendor = string(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(); @@ -153,7 +155,9 @@ void VorbisComment::parse(OggIterator &iterator, bool skipSignature) addNotifications(field); field.invalidateNotifications(); } - iterator.seekForward(1); // skip framing + if(!(flags & VorbisCommentFlags::NoFramingByte)) { + iterator.seekForward(1); // skip framing byte + } m_size = static_cast(static_cast(iterator.currentCharacterOffset()) - startOffset); } else { addNotification(NotificationType::Critical, "Signature is invalid.", context); @@ -173,7 +177,7 @@ void VorbisComment::parse(OggIterator &iterator, bool skipSignature) * \throws Throws Media::Failure or a derived exception when a making * error occurs. */ -void VorbisComment::make(std::ostream &stream, bool noSignature) +void VorbisComment::make(std::ostream &stream, VorbisCommentFlags flags) { // prepare making invalidateStatus(); @@ -185,7 +189,7 @@ void VorbisComment::make(std::ostream &stream, bool noSignature) addNotification(NotificationType::Warning, "Can not convert the assigned vendor to string.", context); } BinaryWriter writer(&stream); - if(!noSignature) { + if(!(flags & VorbisCommentFlags::NoSignature)) { // write signature static const char sig[7] = {0x03, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73}; stream.write(sig, sizeof(sig)); @@ -201,7 +205,7 @@ void VorbisComment::make(std::ostream &stream, bool noSignature) if(!field.value().isEmpty()) { try { field.make(writer); - } catch(Failure &) { + } catch(const Failure &) { // nothing to do here since notifications will be added anyways } // add making notifications @@ -210,7 +214,9 @@ void VorbisComment::make(std::ostream &stream, bool noSignature) } } // write framing byte - stream.put(0x01); + if(!(flags & VorbisCommentFlags::NoFramingByte)) { + stream.put(0x01); + } } } diff --git a/vorbis/vorbiscomment.h b/vorbis/vorbiscomment.h index 9ceffe9..d022f5e 100644 --- a/vorbis/vorbiscomment.h +++ b/vorbis/vorbiscomment.h @@ -13,48 +13,27 @@ class OggIterator; class VorbisComment; /*! - * \brief The OggParameter struct holds the OGG parameter for a VorbisComment. + * \brief The VorbisCommentFlags enum specifies flags which controls parsing and making of Vorbis comments. */ -struct LIB_EXPORT OggParameter +enum class VorbisCommentFlags : byte { - OggParameter(); - void set(std::size_t pageIndex, std::size_t segmentIndex, GeneralMediaFormat streamFormat = GeneralMediaFormat::Vorbis); - - std::size_t firstPageIndex; - std::size_t firstSegmentIndex; - std::size_t lastPageIndex; - std::size_t lastSegmentIndex; - GeneralMediaFormat streamFormat; - bool removed; + 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. */ }; -/*! - * \brief Creates new parameters. - * \remarks The OggContainer class is responsible for assigning sane values. - */ -inline OggParameter::OggParameter() : - firstPageIndex(0), - firstSegmentIndex(0), - lastPageIndex(0), - lastSegmentIndex(0), - streamFormat(GeneralMediaFormat::Vorbis), // default to Vorbis here - removed(false) -{} - -/*! - * \brief Sets firstPageIndex/lastPageIndex, firstSegmentIndex/lastSegmentIndex and streamFormat. - */ -inline void OggParameter::set(std::size_t pageIndex, std::size_t segmentIndex, GeneralMediaFormat streamFormat) +inline bool operator &(VorbisCommentFlags lhs, VorbisCommentFlags rhs) { - firstPageIndex = lastPageIndex = pageIndex; - firstSegmentIndex = lastSegmentIndex = segmentIndex; - this->streamFormat = streamFormat; + 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 { - friend class OggContainer; - public: VorbisComment(); @@ -68,17 +47,14 @@ public: std::string fieldId(KnownField field) const; KnownField knownField(const std::string &id) const; - void parse(OggIterator &iterator, bool skipSignature = false); - void make(std::ostream &stream, bool noSignature = false); + void parse(OggIterator &iterator, VorbisCommentFlags flags = VorbisCommentFlags::None, std::size_t offset = 0); + void make(std::ostream &stream, VorbisCommentFlags flags = VorbisCommentFlags::None); const TagValue &vendor() const; void setVendor(const TagValue &vendor); - OggParameter &oggParams(); - const OggParameter &oggParams() const; private: TagValue m_vendor; - OggParameter m_oggParams; }; /*! @@ -94,15 +70,7 @@ inline TagType VorbisComment::type() const inline const char *VorbisComment::typeName() const { - switch(m_oggParams.streamFormat) { - case GeneralMediaFormat::Opus: - return "Opus comment"; - case GeneralMediaFormat::Theora: - return "Theora comment"; - default: - // just assume Vorbis otherwise - return "Vorbis comment"; - } + return "Vorbis comment"; } inline TagTextEncoding VorbisComment::proposedTextEncoding() const @@ -125,7 +93,7 @@ inline const TagValue &VorbisComment::vendor() const } /*! - * \brief Returns the vendor. + * \brief Sets the vendor. * \remarks Also accessable via setValue(KnownField::Vendor, vendor). */ inline void VorbisComment::setVendor(const TagValue &vendor) @@ -133,28 +101,6 @@ inline void VorbisComment::setVendor(const TagValue &vendor) m_vendor = vendor; } -/*! - * \brief Returns the OGG parameter for the comment. - * - * Consists of first page index, first segment index, last page index, last segment index and tag index (in this order). - * These values are used and managed by the OggContainer class and do not affect the behavior of the VorbisComment instance. - */ -inline OggParameter &VorbisComment::oggParams() -{ - return m_oggParams; -} - -/*! - * \brief Returns the OGG parameter for the comment. - * - * Consists of first page index, first segment index, last page index, last segment index and tag index (in this order). - * These values are used and managed by the OggContainer class and do not affect the behavior of the VorbisComment instance. - */ -inline const OggParameter &VorbisComment::oggParams() const -{ - return m_oggParams; -} - } #endif // MEDIA_VORBISCOMMENT_H diff --git a/vorbis/vorbiscommentfield.cpp b/vorbis/vorbiscommentfield.cpp index b3896d8..d311a66 100644 --- a/vorbis/vorbiscommentfield.cpp +++ b/vorbis/vorbiscommentfield.cpp @@ -1,6 +1,8 @@ #include "./vorbiscommentfield.h" #include "./vorbiscommentids.h" +#include "../flac/flacmetadata.h" + #include "../ogg/oggiterator.h" #include "../id3/id3v2frame.h" @@ -71,21 +73,12 @@ void VorbisCommentField::parse(OggIterator &iterator) // extract cover value try { auto decoded = decodeBase64(data.get() + idSize + 1, size - idSize - 1); - stringstream ss(ios_base::in | ios_base::out | ios_base::binary); - ss.exceptions(ios_base::failbit | ios_base::badbit); - ss.rdbuf()->pubsetbuf(reinterpret_cast(decoded.first.get()), decoded.second); - BinaryReader reader(&ss); - setTypeInfo(reader.readUInt32BE()); - auto size = reader.readUInt32BE(); - value().setMimeType(reader.readString(size)); - size = reader.readUInt32BE(); - value().setDescription(reader.readString(size)); - // skip width, height, color depth, number of colors used - ss.seekg(4 * 4, ios_base::cur); - size = reader.readUInt32BE(); - auto data = make_unique(size); - ss.read(data.get(), size); - value().assignData(move(data), size, TagDataType::Picture); + stringstream bufferStream(ios_base::in | ios_base::out | ios_base::binary); + bufferStream.exceptions(ios_base::failbit | ios_base::badbit); + bufferStream.rdbuf()->pubsetbuf(reinterpret_cast(decoded.first.get()), decoded.second); + FlacMetaDataBlockPicture pictureBlock(value()); + pictureBlock.parse(bufferStream); + setTypeInfo(pictureBlock.pictureType()); } catch (const ios_base::failure &) { addNotification(NotificationType::Critical, "An IO error occured when reading the METADATA_BLOCK_PICTURE struct.", context); throw Failure(); @@ -95,7 +88,7 @@ void VorbisCommentField::parse(OggIterator &iterator) } } else if(id().size() + 1 < size) { // extract other values (as string) - setValue(string(data.get() + idSize + 1, size - idSize - 1)); + setValue(TagValue(string(data.get() + idSize + 1, size - idSize - 1), TagTextEncoding::Utf8)); } } else { addNotification(NotificationType::Critical, "Field is truncated.", context); @@ -127,24 +120,17 @@ void VorbisCommentField::make(BinaryWriter &writer) throw InvalidDataException(); } try { - uint32 dataSize = 32 + value().mimeType().size() + value().description().size() + value().dataSize(); - auto buffer = make_unique(dataSize); - stringstream ss(ios_base::in | ios_base::out | ios_base::binary); - ss.exceptions(ios_base::failbit | ios_base::badbit); - ss.rdbuf()->pubsetbuf(buffer.get(), dataSize); - BinaryWriter writer(&ss); - writer.writeUInt32BE(typeInfo()); - writer.writeUInt32BE(value().mimeType().size()); - writer.writeString(value().mimeType()); - writer.writeUInt32BE(value().description().size()); - writer.writeString(value().description()); - writer.writeUInt32BE(0); // skip width - writer.writeUInt32BE(0); // skip height - writer.writeUInt32BE(0); // skip color depth - writer.writeUInt32BE(0); // skip number of colors used - writer.writeUInt32BE(value().dataSize()); - writer.write(value().dataPointer(), value().dataSize()); - valueString = encodeBase64(reinterpret_cast(buffer.get()), dataSize); + FlacMetaDataBlockPicture pictureBlock(value()); + pictureBlock.setPictureType(typeInfo()); + + const auto requiredSize = pictureBlock.requiredSize(); + auto buffer = make_unique(requiredSize); + stringstream bufferStream(ios_base::in | ios_base::out | ios_base::binary); + bufferStream.exceptions(ios_base::failbit | ios_base::badbit); + bufferStream.rdbuf()->pubsetbuf(buffer.get(), requiredSize); + + pictureBlock.make(bufferStream); + valueString = encodeBase64(reinterpret_cast(buffer.get()), requiredSize); } catch (const ios_base::failure &) { addNotification(NotificationType::Critical, "An IO error occured when writing the METADATA_BLOCK_PICTURE struct.", context); throw Failure(); diff --git a/vorbis/vorbisidentificationheader.h b/vorbis/vorbisidentificationheader.h index 7db26f1..0e4225f 100644 --- a/vorbis/vorbisidentificationheader.h +++ b/vorbis/vorbisidentificationheader.h @@ -4,8 +4,6 @@ #include #include -#include - namespace Media { class OggIterator; @@ -38,7 +36,7 @@ private: }; /*! - * \brief Constructs a new vorbis identification header. + * \brief Constructs a new Vorbis identification header. */ inline VorbisIdentificationHeader::VorbisIdentificationHeader() : m_version(0),