Add support for raw FLAC streams

This commit is contained in:
Martchus 2016-05-16 20:56:53 +02:00
parent bbafd16dcc
commit a84ac37dbe
25 changed files with 739 additions and 142 deletions

View File

@ -36,6 +36,7 @@ set(HEADER_FILES
opus/opusidentificationheader.h opus/opusidentificationheader.h
flac/flactooggmappingheader.h flac/flactooggmappingheader.h
flac/flacmetadata.h flac/flacmetadata.h
flac/flacstream.h
positioninset.h positioninset.h
signature.h signature.h
size.h size.h
@ -109,6 +110,7 @@ set(SRC_FILES
opus/opusidentificationheader.cpp opus/opusidentificationheader.cpp
flac/flactooggmappingheader.cpp flac/flactooggmappingheader.cpp
flac/flacmetadata.cpp flac/flacmetadata.cpp
flac/flacstream.cpp
signature.cpp signature.cpp
statusprovider.cpp statusprovider.cpp
tag.cpp tag.cpp

View File

@ -1,13 +1,14 @@
# Tag Parser # 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 ## Supported formats
The tag library can read and write the following tag 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 - 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 - Vorbis, Opus and FLAC comments in Ogg streams
- cover art via "METADATA_BLOCK_PICTURE" is supported - cover art via "METADATA_BLOCK_PICTURE" is supported
- Vorbis comments and "METADATA_BLOCK_PICTURE" in raw FLAC streams
- Matroska/WebM tags and attachments - Matroska/WebM tags and attachments
## File layout options ## 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. 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 ## Additional features
The library can also display technical information such as the ID, format, language, bitrate, The library can also display technical information such as the ID, format, language, bitrate,

View File

@ -13,7 +13,7 @@
#include <c++utilities/chrono/datetime.h> #include <c++utilities/chrono/datetime.h>
#include <c++utilities/chrono/timespan.h> #include <c++utilities/chrono/timespan.h>
#include <iostream> #include <iosfwd>
#include <string> #include <string>
namespace Media { namespace Media {
@ -35,7 +35,8 @@ enum class TrackType
Mp4Track, /**< The track is a Media::Mp4Track. */ Mp4Track, /**< The track is a Media::Mp4Track. */
WaveAudioStream, /**< The track is a Media::WaveAudioStream. */ WaveAudioStream, /**< The track is a Media::WaveAudioStream. */
OggStream, /**< The track is a Media::OggStream. */ 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 class LIB_EXPORT AbstractTrack : public StatusProvider

View File

@ -64,6 +64,16 @@ public:
virtual const char *what() const USE_NOTHROW; 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 #endif // MEDIA_EXCEPTIONS_H

View File

@ -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. * \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) void FlacMetaDataBlockStreamInfo::parse(const char *buffer)
{ {
@ -78,18 +78,24 @@ void FlacMetaDataBlockStreamInfo::parse(const char *buffer)
/*! /*!
* \brief Parses the FLAC "METADATA_BLOCK_PICTURE". * \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); BinaryReader reader(&inputStream);
m_pictureType = reader.readUInt32BE(); m_pictureType = reader.readUInt32BE();
auto size = reader.readUInt32BE(); uint32 size = reader.readUInt32BE();
CHECK_MAX_SIZE(size);
m_value.setMimeType(reader.readString(size)); m_value.setMimeType(reader.readString(size));
size = reader.readUInt32BE(); size = reader.readUInt32BE();
CHECK_MAX_SIZE(size);
m_value.setDescription(reader.readString(size)); m_value.setDescription(reader.readString(size));
// skip width, height, color depth, number of colors used // skip width, height, color depth, number of colors used
inputStream.seekg(4 * 4, ios_base::cur); inputStream.seekg(4 * 4, ios_base::cur);
size = reader.readUInt32BE(); size = reader.readUInt32BE();
CHECK_MAX_SIZE(size);
auto data = make_unique<char[]>(size); auto data = make_unique<char[]>(size);
inputStream.read(data.get(), size); inputStream.read(data.get(), size);
m_value.assignData(move(data), size, TagDataType::Picture); m_value.assignData(move(data), size, TagDataType::Picture);

View File

@ -66,6 +66,7 @@ inline FlacMetaDataBlockHeader::FlacMetaDataBlockHeader() :
/*! /*!
* \brief Returns whether this is the last metadata block before the audio blocks. * \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 inline byte FlacMetaDataBlockHeader::isLast() const
{ {
@ -258,7 +259,7 @@ class LIB_EXPORT FlacMetaDataBlockPicture
public: public:
FlacMetaDataBlockPicture(TagValue &tagValue); FlacMetaDataBlockPicture(TagValue &tagValue);
void parse(std::istream &inputStream); void parse(std::istream &inputStream, uint32 maxSize);
uint32 requiredSize() const; uint32 requiredSize() const;
void make(std::ostream &outputStream); void make(std::ostream &outputStream);

253
flac/flacstream.cpp Normal file
View File

@ -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 <c++utilities/io/copy.h>
#include <sstream>
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<VorbisComment>();
}
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<FlacMetaDataBlockType>(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<VorbisComment>();
}
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<VorbisComment>();
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<decltype(startOffset)>(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<uint32>(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<FlacMetaDataBlockType>(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);
}
}
}

75
flac/flacstream.h Normal file
View File

@ -0,0 +1,75 @@
#ifndef FLACSTREAM_H
#define FLACSTREAM_H
#include "../abstracttrack.h"
#include <c++utilities/misc/memory.h>
#include <iosfwd>
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<VorbisComment> 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

View File

@ -12,8 +12,7 @@
#include <c++utilities/conversion/stringconversion.h> #include <c++utilities/conversion/stringconversion.h>
#include <string> #include <string>
#include <istream> #include <iosfwd>
#include <ostream>
#include <vector> #include <vector>
namespace Media namespace Media

View File

@ -9,7 +9,6 @@
#include "../exceptions.h" #include "../exceptions.h"
#include "../backuphelper.h" #include "../backuphelper.h"
// include configuration from separate header file when building with CMake
#include "resources/config.h" #include "resources/config.h"
#include <c++utilities/conversion/stringconversion.h> #include <c++utilities/conversion/stringconversion.h>

View File

@ -27,6 +27,9 @@
#include "./ogg/oggcontainer.h" #include "./ogg/oggcontainer.h"
#include "./flac/flacstream.h"
#include "./flac/flacmetadata.h"
#include <c++utilities/conversion/stringconversion.h> #include <c++utilities/conversion/stringconversion.h>
#include <c++utilities/chrono/timespan.h> #include <c++utilities/chrono/timespan.h>
#include <c++utilities/misc/memory.h> #include <c++utilities/misc/memory.h>
@ -306,19 +309,31 @@ void MediaFileInfo::parseTracks()
m_container->parseTracks(); m_container->parseTracks();
} else { } else {
switch(m_containerFormat) { switch(m_containerFormat) {
case ContainerFormat::RiffWave: case ContainerFormat::Adts:
m_singleTrack = make_unique<WaveAudioStream>(stream(), m_containerOffset); m_singleTrack = make_unique<AdtsStream>(stream(), m_containerOffset);
break;
case ContainerFormat::Flac:
m_singleTrack = make_unique<FlacStream>(*this, m_containerOffset);
break; break;
case ContainerFormat::MpegAudioFrames: case ContainerFormat::MpegAudioFrames:
m_singleTrack = make_unique<MpegAudioFrameStream>(stream(), m_containerOffset); m_singleTrack = make_unique<MpegAudioFrameStream>(stream(), m_containerOffset);
break; break;
case ContainerFormat::Adts: case ContainerFormat::RiffWave:
m_singleTrack = make_unique<AdtsStream>(stream(), m_containerOffset); m_singleTrack = make_unique<WaveAudioStream>(stream(), m_containerOffset);
break; break;
default: default:
throw NotImplementedException(); throw NotImplementedException();
} }
m_singleTrack->parseHeader(); m_singleTrack->parseHeader();
switch(m_containerFormat) {
case ContainerFormat::Flac:
// FLAC streams might container padding
m_paddingSize += static_cast<FlacStream *>(m_singleTrack.get())->paddingSize();
break;
default:
;
}
} }
m_tracksParsingStatus = ParsingStatus::Ok; m_tracksParsingStatus = ParsingStatus::Ok;
} catch(const NotImplementedException &) { } catch(const NotImplementedException &) {
@ -539,27 +554,34 @@ bool MediaFileInfo::createAppropriateTags(bool treatUnknownFilesAsMp3Files, TagU
m_container->createTag(); m_container->createTag();
} }
} else { } else {
// no container object present; creation of ID3 tag is possible // no container object present
if(!hasAnyTag() && !treatUnknownFilesAsMp3Files) { if(m_containerFormat == ContainerFormat::Flac) {
switch(containerFormat()) { // creation of Vorbis comment is possible
case ContainerFormat::MpegAudioFrames: static_cast<FlacStream *>(m_singleTrack.get())->createVorbisComment();
case ContainerFormat::Adts: } else {
break; // creation of ID3 tag is possible
default: if(!hasAnyTag() && !treatUnknownFilesAsMp3Files) {
return false; switch(containerFormat()) {
} case ContainerFormat::MpegAudioFrames:
} case ContainerFormat::Adts:
// create ID3 tags according to id3v2usage and id3v2usage break;
if(id3v1usage == TagUsage::Always) { default:
// always create ID3v1 tag -> ensure there is one return false;
createId3v1Tag(); }
} }
if(id3v2usage == TagUsage::Always) { // create ID3 tags according to id3v2usage and id3v2usage
// always create ID3v2 tag -> ensure there is one and set version if(id3v1usage == TagUsage::Always) {
if(!hasId3v2Tag()) { // always create ID3v1 tag -> ensure there is one
createId3v2Tag()->setVersion(id3v2version, 0); createId3v1Tag();
}
if(id3v2usage == TagUsage::Always) {
// always create ID3v2 tag -> ensure there is one and set version
if(!hasId3v2Tag()) {
createId3v2Tag()->setVersion(id3v2version, 0);
}
} }
} }
if(mergeMultipleSuccessiveId3v2Tags) { if(mergeMultipleSuccessiveId3v2Tags) {
mergeId3v2Tags(); mergeId3v2Tags();
} }
@ -1031,12 +1053,13 @@ bool MediaFileInfo::areTracksSupported() const
bool MediaFileInfo::areTagsSupported() const bool MediaFileInfo::areTagsSupported() const
{ {
switch(m_containerFormat) { switch(m_containerFormat) {
case ContainerFormat::Mp4:
case ContainerFormat::MpegAudioFrames:
case ContainerFormat::Ogg:
case ContainerFormat::Matroska:
case ContainerFormat::Webm:
case ContainerFormat::Adts: 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 // these container formats are supported
return true; return true;
default: default:
@ -1081,7 +1104,11 @@ const vector<unique_ptr<MatroskaTag> > &MediaFileInfo::matroskaTags() const
*/ */
VorbisComment *MediaFileInfo::vorbisComment() const VorbisComment *MediaFileInfo::vorbisComment() const
{ {
return m_containerFormat == ContainerFormat::Ogg && m_container && m_container->tagCount() > 0 ? static_cast<OggContainer *>(m_container.get())->tags().front().get() : nullptr; return m_containerFormat == ContainerFormat::Ogg && m_container && m_container->tagCount()
? static_cast<OggContainer *>(m_container.get())->tags().front().get()
: (m_containerFormat == ContainerFormat::Flac && m_singleTrack
? static_cast<FlacStream *>(m_singleTrack.get())->vorbisComment()
: nullptr);
} }
/*! /*!
@ -1330,11 +1357,17 @@ bool MediaFileInfo::id3v2ToId3v1()
*/ */
void MediaFileInfo::tags(vector<Tag *> &tags) const void MediaFileInfo::tags(vector<Tag *> &tags) const
{ {
if(hasId3v1Tag()) if(hasId3v1Tag()) {
tags.push_back(m_id3v1Tag.get()); tags.push_back(m_id3v1Tag.get());
}
for(const unique_ptr<Id3v2Tag> &tag : m_id3v2Tags) { for(const unique_ptr<Id3v2Tag> &tag : m_id3v2Tags) {
tags.push_back(tag.get()); tags.push_back(tag.get());
} }
if(m_containerFormat == ContainerFormat::Flac && m_singleTrack) {
if(auto *vorbisComment = static_cast<FlacStream *>(m_singleTrack.get())->vorbisComment()) {
tags.push_back(vorbisComment);
}
}
if(m_container) { if(m_container) {
for(size_t i = 0, count = m_container->tagCount(); i < count; ++i) { for(size_t i = 0, count = m_container->tagCount(); i < count; ++i) {
tags.push_back(m_container->tag(i)); tags.push_back(m_container->tag(i));
@ -1342,6 +1375,17 @@ void MediaFileInfo::tags(vector<Tag *> &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<FlacStream *>(m_singleTrack.get())->vorbisComment());
}
/*! /*!
* \brief Returns all tags assigned to the current file. * \brief Returns all tags assigned to the current file.
* *
@ -1371,9 +1415,9 @@ void MediaFileInfo::invalidated()
*/ */
void MediaFileInfo::makeMp3File() 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 // 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) { if(m_actualExistingId3v1Tag) {
// there is currently an ID3v1 tag at the end of the file // there is currently an ID3v1 tag at the end of the file
if(m_id3v1Tag) { if(m_id3v1Tag) {
@ -1434,30 +1478,57 @@ void MediaFileInfo::makeMp3File()
addNotifications(*tag); addNotifications(*tag);
} }
// check whether it is a raw FLAC stream
FlacStream *flacStream = (m_containerFormat == ContainerFormat::Flac ? static_cast<FlacStream *>(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<uint32>(m_containerOffset);
}
// check whether rewrite is required // check whether rewrite is required
bool rewriteRequired = isForcingRewrite() || !m_saveFilePath.empty() || (tagsSize > static_cast<uint32>(m_containerOffset)); bool rewriteRequired = isForcingRewrite() || !m_saveFilePath.empty() || (tagsSize > streamOffset);
uint32 padding = 0; uint32 padding = 0;
if(!rewriteRequired) { if(!rewriteRequired) {
// rewriting is not forced and new tag is not too big for available space // rewriting is not forced and new tag is not too big for available space
// -> calculate new padding // -> calculate new padding
padding = static_cast<uint32>(m_containerOffset) - tagsSize; padding = streamOffset - tagsSize;
// -> check whether the new padding matches specifications // -> check whether the new padding matches specifications
if(padding < minPadding() || padding > maxPadding()) { if(padding < minPadding() || padding > maxPadding()) {
rewriteRequired = true; rewriteRequired = true;
} }
} }
if(makers.empty()) { if(makers.empty() && !flacStream) {
// an ID3v2 tag shouldn't be written // an ID3v2 tag is not written and it is not a FLAC stream
// -> can't include padding // -> can't include padding
if(padding) { if(padding) {
// but padding would be present -> need to rewrite // but padding would be present -> need to rewrite
padding = 0; padding = 0; // can't write the preferred padding despite rewriting
rewriteRequired = true; rewriteRequired = true;
} }
} else if(rewriteRequired) { } else if(rewriteRequired) {
// rewriting is forced or new ID3v2 tag is too big for available space // rewriting is forced or new ID3v2 tag is too big for available space
// -> use preferred padding when rewriting anyways // -> use preferred padding when rewriting anyways
padding = preferredPadding(); 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 ..."); updateStatus(rewriteRequired ? "Preparing streams for rewriting ..." : "Preparing streams for updating ...");
@ -1490,7 +1561,6 @@ void MediaFileInfo::makeMp3File()
} }
} }
} else { // !rewriteRequired } else { // !rewriteRequired
// reopen original file to ensure it is opened for writing // reopen original file to ensure it is opened for writing
try { try {
@ -1511,30 +1581,56 @@ void MediaFileInfo::makeMp3File()
i->make(outputStream, 0); i->make(outputStream, 0);
} }
// include padding into the last ID3v2 tag // include padding into the last ID3v2 tag
makers.back().make(outputStream, padding); makers.back().make(outputStream, (flacStream && padding && padding < 4) ? 0 : padding);
} else { }
// just write 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<byte>(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) { for(; padding; --padding) {
outputStream.put(0); outputStream.put(0);
} }
} }
// copy / skip media data // copy / skip actual stream data
// -> determine media data size // -> determine media data size
uint64 mediaDataSize = size() - m_containerOffset; uint64 mediaDataSize = size() - streamOffset;
if(m_actualExistingId3v1Tag) { if(m_actualExistingId3v1Tag) {
mediaDataSize -= 128; mediaDataSize -= 128;
} }
if(rewriteRequired) { if(rewriteRequired) {
// copy data from original file // copy data from original file
updateStatus("Writing MPEG audio frames ..."); switch(m_containerFormat) {
backupStream.seekg(m_containerOffset); case ContainerFormat::MpegAudioFrames:
updateStatus("Writing MPEG audio frames ...");
break;
default:
updateStatus("Writing frames ...");
}
backupStream.seekg(streamOffset);
CopyHelper<0x4000> copyHelper; CopyHelper<0x4000> copyHelper;
copyHelper.callbackCopy(backupStream, stream(), mediaDataSize, bind(&StatusProvider::isAborted, this), bind(&StatusProvider::updatePercentage, this, _1)); copyHelper.callbackCopy(backupStream, stream(), mediaDataSize, bind(&StatusProvider::isAborted, this), bind(&StatusProvider::updatePercentage, this, _1));
updatePercentage(100.0); updatePercentage(100.0);
} else { } else {
// just skip media data // just skip actual stream data
outputStream.seekp(mediaDataSize, ios_base::cur); outputStream.seekp(mediaDataSize, ios_base::cur);
} }

View File

@ -6,8 +6,6 @@
#include "./basicfileinfo.h" #include "./basicfileinfo.h"
#include "./abstractcontainer.h" #include "./abstractcontainer.h"
#include <string>
#include <fstream>
#include <vector> #include <vector>
#include <memory> #include <memory>
@ -24,8 +22,6 @@ class OggContainer;
class EbmlElement; class EbmlElement;
class MatroskaTag; class MatroskaTag;
class AbstractTrack; class AbstractTrack;
class WaveAudioStream;
class MpegAudioFrameStream;
class VorbisComment; class VorbisComment;
enum class MediaType; enum class MediaType;
@ -335,14 +331,6 @@ inline bool MediaFileInfo::hasId3v2Tag() const
return m_id3v2Tags.size(); 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. * \brief Returns a pointer to the assigned ID3v1 tag or nullptr if none is assigned.
* *

View File

@ -259,6 +259,7 @@ public:
operator bool() const; operator bool() const;
MediaFormat &operator+=(const MediaFormat &other); MediaFormat &operator+=(const MediaFormat &other);
bool operator==(GeneralMediaFormat general) const; bool operator==(GeneralMediaFormat general) const;
bool operator!=(GeneralMediaFormat general) const;
GeneralMediaFormat general; GeneralMediaFormat general;
unsigned char sub; unsigned char sub;
@ -299,6 +300,14 @@ inline bool MediaFormat::operator==(GeneralMediaFormat general) const
return this->general == general; 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. * \brief Returns whether the media format is known.
*/ */

View File

@ -235,11 +235,12 @@ void OggContainer::internalParseTags()
break; break;
case GeneralMediaFormat::Opus: case GeneralMediaFormat::Opus:
// skip header (has already been detected by OggStream) // skip header (has already been detected by OggStream)
m_iterator.seekForward(8); m_iterator.ignore(8);
comment->parse(m_iterator, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte); comment->parse(m_iterator, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte);
break; break;
case GeneralMediaFormat::Flac: 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; break;
default: default:
addNotification(NotificationType::Critical, "Stream format not supported.", "parsing tags from OGG streams"); 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> &copyHelper, vector<uint32> &newSegmentSizes, VorbisComment *comment, OggParameter *params) void OggContainer::makeVorbisCommentSegment(stringstream &buffer, CopyHelper<65307> &copyHelper, vector<uint32> &newSegmentSizes, VorbisComment *comment, OggParameter *params)
{ {
auto offset = buffer.tellp(); const auto offset = buffer.tellp();
switch(params->streamFormat) { switch(params->streamFormat) {
case GeneralMediaFormat::Vorbis: case GeneralMediaFormat::Vorbis:
comment->make(buffer); comment->make(buffer);

View File

@ -123,6 +123,7 @@ void OggIterator::previousSegment()
* - Page headers are skipped (this is the whole purpose of this method). * - 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 * \throws Throws a TruncatedDataException if the end of the stream is reached before \a count bytes
* have been read. * have been read.
* \sa readAll()
* \sa currentCharacterOffset() * \sa currentCharacterOffset()
* \sa seekForward() * \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. * \brief Advances the position of the next character to be read from the OGG stream by \a count bytes.
* \remarks * \remarks
@ -159,7 +190,7 @@ void OggIterator::read(char *buffer, size_t count)
* \sa currentCharacterOffset() * \sa currentCharacterOffset()
* \sa read() * \sa read()
*/ */
void OggIterator::seekForward(size_t count) void OggIterator::ignore(size_t count)
{ {
uint32 available = currentSegmentSize() - m_bytesRead; uint32 available = currentSegmentSize() - m_bytesRead;
while(*this) { while(*this) {

View File

@ -3,7 +3,7 @@
#include "./oggpage.h" #include "./oggpage.h"
#include <istream> #include <iosfwd>
#include <vector> #include <vector>
namespace Media { namespace Media {
@ -31,13 +31,15 @@ public:
std::vector<uint32>::size_type currentSegmentIndex() const; std::vector<uint32>::size_type currentSegmentIndex() const;
uint64 currentSegmentOffset() const; uint64 currentSegmentOffset() const;
uint64 currentCharacterOffset() const; uint64 currentCharacterOffset() const;
uint64 tellg() const;
uint32 currentSegmentSize() const; uint32 currentSegmentSize() const;
void setFilter(uint32 streamSerialId); void setFilter(uint32 streamSerialId);
void removeFilter(); void removeFilter();
bool areAllPagesFetched() const; bool areAllPagesFetched() const;
void read(char *buffer, size_t count); void read(char *buffer, std::size_t count);
void seekForward(size_t count); size_t readAll(char *buffer, std::size_t max);
bool bytesRemaining(size_t atLeast) const; void ignore(std::size_t count = 1);
bool bytesRemaining(std::size_t atLeast) const;
operator bool() const; operator bool() const;
OggIterator &operator++(); OggIterator &operator++();
@ -201,6 +203,14 @@ inline uint64 OggIterator::currentCharacterOffset() const
return m_offset + m_bytesRead; 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. * \brief Returns the size of the current segment.
* *

View File

@ -6,7 +6,7 @@
#include <vector> #include <vector>
#include <numeric> #include <numeric>
#include <istream> #include <iosfwd>
namespace Media { namespace Media {

View File

@ -42,6 +42,7 @@ enum Sig48 : uint64
enum Sig32 : uint32 enum Sig32 : uint32
{ {
Elf = 0x7F454C46u, Elf = 0x7F454C46u,
Flac = 0x664C6143u,
JavaClassFile = 0xCAFEBABEu, JavaClassFile = 0xCAFEBABEu,
Ebml = 0x1A45DFA3u, Ebml = 0x1A45DFA3u,
Mp4 = 0x66747970u, Mp4 = 0x66747970u,
@ -150,6 +151,8 @@ ContainerFormat parseSignature(const char *buffer, int bufferSize)
switch(sig >> 32) { // check 32-bit signatures switch(sig >> 32) { // check 32-bit signatures
case Elf: case Elf:
return ContainerFormat::Elf; return ContainerFormat::Elf;
case Flac:
return ContainerFormat::Flac;
case JavaClassFile: case JavaClassFile:
return ContainerFormat::JavaClassFile; return ContainerFormat::JavaClassFile;
case Ebml: case Ebml:
@ -235,6 +238,7 @@ const char *containerFormatAbbreviation(ContainerFormat containerFormat, MediaTy
case ContainerFormat::Ar: return "a"; case ContainerFormat::Ar: return "a";
case ContainerFormat::Asf: return "asf"; case ContainerFormat::Asf: return "asf";
case ContainerFormat::Elf: return "elf"; case ContainerFormat::Elf: return "elf";
case ContainerFormat::Flac: return "flac";
case ContainerFormat::FlashVideo: return "flv"; case ContainerFormat::FlashVideo: return "flv";
case ContainerFormat::Gif87a: case ContainerFormat::Gif87a:
case ContainerFormat::Gif89a: return "gif"; case ContainerFormat::Gif89a: return "gif";
@ -315,6 +319,8 @@ const char *containerFormatName(ContainerFormat containerFormat)
return "Advanced Systems Format"; return "Advanced Systems Format";
case ContainerFormat::Elf: case ContainerFormat::Elf:
return "Executable and Linkable Format"; return "Executable and Linkable Format";
case ContainerFormat::Flac:
return "raw Free Lossless Audio Codec frames";
case ContainerFormat::FlashVideo: case ContainerFormat::FlashVideo:
return "Flash Video"; return "Flash Video";
case ContainerFormat::Gif87a: case ContainerFormat::Gif87a:
@ -417,6 +423,8 @@ const char *containerMimeType(ContainerFormat containerFormat, MediaType mediaTy
switch(containerFormat) { switch(containerFormat) {
case ContainerFormat::Asf: case ContainerFormat::Asf:
return "video/x-ms-asf"; return "video/x-ms-asf";
case ContainerFormat::Flac:
return "audio/flac";
case ContainerFormat::FlashVideo: case ContainerFormat::FlashVideo:
return "video/x-flv"; return "video/x-flv";
case ContainerFormat::Gif87a: case ContainerFormat::Gif87a:

View File

@ -10,6 +10,8 @@ namespace Media {
/*! /*!
* \brief Specifies the container format. * \brief Specifies the container format.
*
* Raw streams like ADTS or raw FLAC count as container format in this context.
*/ */
enum class ContainerFormat enum class ContainerFormat
{ {
@ -19,6 +21,7 @@ enum class ContainerFormat
Asf, /**< Advanced Systems Format */ Asf, /**< Advanced Systems Format */
Bzip2, /** bzip2 compressed file */ Bzip2, /** bzip2 compressed file */
Elf, /**< Executable and Linkable Format */ Elf, /**< Executable and Linkable Format */
Flac, /** < Free Lossless Audio Codec (raw stream) */
FlashVideo, /**< Flash (FLV) */ FlashVideo, /**< Flash (FLV) */
Gif87a, /**< Graphics Interchange Format (1987) */ Gif87a, /**< Graphics Interchange Format (1987) */
Gif89a, /**< Graphics Interchange Format (1989) */ Gif89a, /**< Graphics Interchange Format (1989) */

View File

@ -7,7 +7,7 @@
#include <c++utilities/chrono/timespan.h> #include <c++utilities/chrono/timespan.h>
#include <c++utilities/chrono/datetime.h> #include <c++utilities/chrono/datetime.h>
#include <istream> #include <iosfwd>
#include <string> #include <string>
#include <memory> #include <memory>

View File

@ -100,51 +100,51 @@ KnownField VorbisComment::knownField(const string &id) const
} }
/*! /*!
* \brief Parses tag information using the specified OGG \a iterator. * \brief Internal implementation for parsing.
*
* \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, size_t offset) template<class StreamType>
void VorbisComment::internalParse(StreamType &stream, uint64 maxSize, VorbisCommentFlags flags)
{ {
// prepare parsing // prepare parsing
invalidateStatus(); invalidateStatus();
static const string context("parsing Vorbis comment"); static const string context("parsing Vorbis comment");
auto startOffset = iterator.currentSegmentOffset() + offset; uint64 startOffset = static_cast<uint64>(stream.tellg());
iterator.seekForward(offset);
try { try {
// read signature: 0x3 + "vorbis" // read signature: 0x3 + "vorbis"
char sig[8]; char sig[8];
bool skipSignature = flags & VorbisCommentFlags::NoSignature; bool skipSignature = flags & VorbisCommentFlags::NoSignature;
if(!skipSignature) { if(!skipSignature) {
iterator.read(sig, 7); CHECK_MAX_SIZE(7);
stream.read(sig, 7);
skipSignature = (ConversionUtilities::BE::toUInt64(sig) & 0xffffffffffffff00u) == 0x03766F7262697300u; skipSignature = (ConversionUtilities::BE::toUInt64(sig) & 0xffffffffffffff00u) == 0x03766F7262697300u;
} }
if(skipSignature) { if(skipSignature) {
// read vendor (length prefixed string) // read vendor (length prefixed string)
{ {
iterator.read(sig, 4); CHECK_MAX_SIZE(4);
stream.read(sig, 4);
const auto vendorSize = LE::toUInt32(sig); const auto vendorSize = LE::toUInt32(sig);
if(iterator.currentCharacterOffset() + vendorSize <= iterator.streamSize()) { if(vendorSize <= maxSize) {
auto buff = make_unique<char []>(vendorSize); auto buff = make_unique<char []>(vendorSize);
iterator.read(buff.get(), vendorSize); stream.read(buff.get(), vendorSize);
m_vendor.assignData(move(buff), vendorSize, TagDataType::Text, TagTextEncoding::Utf8); m_vendor.assignData(move(buff), vendorSize, TagDataType::Text, TagTextEncoding::Utf8);
// TODO: Is the vendor string actually UTF-8 (like the field values)? // TODO: Is the vendor string actually UTF-8 (like the field values)?
} else { } else {
addNotification(NotificationType::Critical, "Vendor information is truncated.", context); addNotification(NotificationType::Critical, "Vendor information is truncated.", context);
throw TruncatedDataException(); throw TruncatedDataException();
} }
maxSize -= vendorSize;
} }
// read field count // read field count
iterator.read(sig, 4); CHECK_MAX_SIZE(4);
stream.read(sig, 4);
uint32 fieldCount = LE::toUInt32(sig); uint32 fieldCount = LE::toUInt32(sig);
VorbisCommentField field; VorbisCommentField field;
const string &fieldId = field.id(); const string &fieldId = field.id();
for(uint32 i = 0; i < fieldCount; ++i) { for(uint32 i = 0; i < fieldCount; ++i) {
// read fields // read fields
try { try {
field.parse(iterator); field.parse(stream, maxSize);
fields().insert(pair<fieldType::identifierType, fieldType>(fieldId, field)); fields().insert(pair<fieldType::identifierType, fieldType>(fieldId, field));
} catch(const TruncatedDataException &) { } catch(const TruncatedDataException &) {
addNotifications(field); addNotifications(field);
@ -156,20 +156,44 @@ void VorbisComment::parse(OggIterator &iterator, VorbisCommentFlags flags, size_
field.invalidateNotifications(); field.invalidateNotifications();
} }
if(!(flags & VorbisCommentFlags::NoFramingByte)) { if(!(flags & VorbisCommentFlags::NoFramingByte)) {
iterator.seekForward(1); // skip framing byte stream.ignore(); // skip framing byte
} }
m_size = static_cast<uint32>(static_cast<uint64>(iterator.currentCharacterOffset()) - startOffset); m_size = static_cast<uint32>(static_cast<uint64>(stream.tellg()) - startOffset);
} else { } else {
addNotification(NotificationType::Critical, "Signature is invalid.", context); addNotification(NotificationType::Critical, "Signature is invalid.", context);
throw InvalidDataException(); throw InvalidDataException();
} }
} catch(const TruncatedDataException &) { } catch(const TruncatedDataException &) {
m_size = static_cast<uint32>(static_cast<uint64>(iterator.currentCharacterOffset()) - startOffset); m_size = static_cast<uint32>(static_cast<uint64>(stream.tellg()) - startOffset);
addNotification(NotificationType::Critical, "Vorbis comment is truncated.", context); addNotification(NotificationType::Critical, "Vorbis comment is truncated.", context);
throw; 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. * \brief Writes tag information to the specified \a stream.
* *
@ -197,14 +221,18 @@ void VorbisComment::make(std::ostream &stream, VorbisCommentFlags flags)
// write vendor // write vendor
writer.writeUInt32LE(vendor.size()); writer.writeUInt32LE(vendor.size());
writer.writeString(vendor); writer.writeString(vendor);
// write field count // write field count later
writer.writeUInt32LE(fieldCount()); const auto fieldCountOffset = stream.tellp();
writer.writeUInt32LE(0);
// write fields // write fields
uint32 fieldsWritten = 0;
for(auto i : fields()) { for(auto i : fields()) {
VorbisCommentField &field = i.second; VorbisCommentField &field = i.second;
if(!field.value().isEmpty()) { if(!field.value().isEmpty()) {
try { try {
field.make(writer); if(field.make(writer, flags)) {
++fieldsWritten;
}
} catch(const Failure &) { } catch(const Failure &) {
// nothing to do here since notifications will be added anyways // nothing to do here since notifications will be added anyways
} }
@ -213,6 +241,11 @@ void VorbisComment::make(std::ostream &stream, VorbisCommentFlags flags)
field.invalidateNotifications(); field.invalidateNotifications();
} }
} }
// write field count
const auto framingByteOffset = stream.tellp();
stream.seekp(fieldCountOffset);
writer.writeUInt32LE(fieldsWritten);
stream.seekp(framingByteOffset);
// write framing byte // write framing byte
if(!(flags & VorbisCommentFlags::NoFramingByte)) { if(!(flags & VorbisCommentFlags::NoFramingByte)) {
stream.put(0x01); stream.put(0x01);

View File

@ -12,26 +12,6 @@ namespace Media {
class OggIterator; class OggIterator;
class VorbisComment; 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<byte>(lhs) & static_cast<byte>(rhs);
}
inline VorbisCommentFlags operator |(VorbisCommentFlags lhs, VorbisCommentFlags rhs)
{
return static_cast<VorbisCommentFlags>(static_cast<byte>(lhs) | static_cast<byte>(rhs));
}
class LIB_EXPORT VorbisComment : public FieldMapBasedTag<VorbisCommentField, CaseInsensitiveStringComparer> class LIB_EXPORT VorbisComment : public FieldMapBasedTag<VorbisCommentField, CaseInsensitiveStringComparer>
{ {
public: public:
@ -47,12 +27,17 @@ public:
std::string fieldId(KnownField field) const; std::string fieldId(KnownField field) const;
KnownField knownField(const std::string &id) 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); void make(std::ostream &stream, VorbisCommentFlags flags = VorbisCommentFlags::None);
const TagValue &vendor() const; const TagValue &vendor() const;
void setVendor(const TagValue &vendor); void setVendor(const TagValue &vendor);
private:
template<class StreamType>
void internalParse(StreamType &stream, uint64 maxSize, VorbisCommentFlags flags);
private: private:
TagValue m_vendor; TagValue m_vendor;
}; };

View File

@ -42,25 +42,26 @@ VorbisCommentField::VorbisCommentField(const identifierType &id, const TagValue
{} {}
/*! /*!
* \brief Parses a field from the stream read using the specified \a reader. * \brief Internal implementation for parsing.
*
* 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(OggIterator &iterator) template<class StreamType>
void VorbisCommentField::internalParse(StreamType &stream, uint64 &maxSize)
{ {
static const string context("parsing Vorbis comment field"); static const string context("parsing Vorbis comment field");
char buff[4]; char buff[4];
iterator.read(buff, 4); if(maxSize < 4) {
if(auto size = LE::toUInt32(buff)) { // read size addNotification(NotificationType::Critical, "Field expected.", context);
if(iterator.currentCharacterOffset() + size <= iterator.streamSize()) { 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 // read data
auto data = make_unique<char []>(size); auto data = make_unique<char []>(size);
iterator.read(data.get(), size); stream.read(data.get(), size);
uint32 idSize = 0; uint32 idSize = 0;
for(const char *i = data.get(), *end = data.get() + size; i != end && *i != '='; ++i, ++idSize); for(const char *i = data.get(), *end = data.get() + size; i != end && *i != '='; ++i, ++idSize);
// extract id // extract id
@ -77,13 +78,16 @@ void VorbisCommentField::parse(OggIterator &iterator)
bufferStream.exceptions(ios_base::failbit | ios_base::badbit); bufferStream.exceptions(ios_base::failbit | ios_base::badbit);
bufferStream.rdbuf()->pubsetbuf(reinterpret_cast<char *>(decoded.first.get()), decoded.second); bufferStream.rdbuf()->pubsetbuf(reinterpret_cast<char *>(decoded.first.get()), decoded.second);
FlacMetaDataBlockPicture pictureBlock(value()); FlacMetaDataBlockPicture pictureBlock(value());
pictureBlock.parse(bufferStream); pictureBlock.parse(bufferStream, decoded.second);
setTypeInfo(pictureBlock.pictureType()); 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); addNotification(NotificationType::Critical, "An IO error occured when reading the METADATA_BLOCK_PICTURE struct.", context);
throw Failure(); throw Failure();
} catch (const ConversionException &) { } catch(const TruncatedDataException &) {
addNotification(NotificationType::Critical, "Base64 data from METADATA_BLOCK_PICTURE is invalid.", context); 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(); throw InvalidDataException();
} }
} else if(id().size() + 1 < size) { } 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. * \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 std::ios_base::failure when an IO error occurs.
* \throws Throws Media::Failure or a derived exception when a making * \throws Throws Media::Failure or a derived exception when a making
* error occurs. * 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"); static const string context("making Vorbis comment field");
if(id().empty()) { if(id().empty()) {
@ -114,6 +166,9 @@ void VorbisCommentField::make(BinaryWriter &writer)
// try to convert value to string // try to convert value to string
string valueString; string valueString;
if(id() == VorbisCommentIds::cover()) { if(id() == VorbisCommentIds::cover()) {
if(flags & VorbisCommentFlags::NoCovers) {
return false;
}
// make cover // make cover
if(value().type() != TagDataType::Picture) { if(value().type() != TagDataType::Picture) {
addNotification(NotificationType::Critical, "Assigned value of cover field is not picture data.", context); 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.writeString(id());
writer.writeChar('='); writer.writeChar('=');
writer.writeString(valueString); writer.writeString(valueString);
} catch(ConversionException &) { } catch(const ConversionException &) {
addNotification(NotificationType::Critical, "Assigned value can not be converted appropriately.", context); addNotification(NotificationType::Critical, "Assigned value can not be converted appropriately.", context);
throw InvalidDataException(); throw InvalidDataException();
} }
return true;
} }
} }

View File

@ -11,6 +11,27 @@ class BinaryWriter;
namespace Media { 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<byte>(lhs) & static_cast<byte>(rhs);
}
inline VorbisCommentFlags operator |(VorbisCommentFlags lhs, VorbisCommentFlags rhs)
{
return static_cast<VorbisCommentFlags>(static_cast<byte>(lhs) | static_cast<byte>(rhs));
}
class VorbisCommentField; class VorbisCommentField;
/*! /*!
@ -45,12 +66,18 @@ public:
VorbisCommentField(const identifierType &id, const TagValue &value); VorbisCommentField(const identifierType &id, const TagValue &value);
void parse(OggIterator &iterator); 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 isAdditionalTypeInfoUsed() const;
bool supportsNestedFields() const; bool supportsNestedFields() const;
protected: protected:
void cleared(); void cleared();
private:
template<class StreamType>
void internalParse(StreamType &stream, uint64 &maxSize);
}; };
/*! /*!

View File

@ -1,6 +1,8 @@
#ifndef VORBISPACKAGETYPES_H #ifndef VORBISPACKAGETYPES_H
#define VORBISPACKAGETYPES_H #define VORBISPACKAGETYPES_H
#include <c++utilities/conversion/types.h>
namespace Media { namespace Media {
/*! /*!