Add support for raw FLAC streams
This commit is contained in:
parent
bbafd16dcc
commit
a84ac37dbe
|
@ -36,6 +36,7 @@ set(HEADER_FILES
|
|||
opus/opusidentificationheader.h
|
||||
flac/flactooggmappingheader.h
|
||||
flac/flacmetadata.h
|
||||
flac/flacstream.h
|
||||
positioninset.h
|
||||
signature.h
|
||||
size.h
|
||||
|
@ -109,6 +110,7 @@ set(SRC_FILES
|
|||
opus/opusidentificationheader.cpp
|
||||
flac/flactooggmappingheader.cpp
|
||||
flac/flacmetadata.cpp
|
||||
flac/flacstream.cpp
|
||||
signature.cpp
|
||||
statusprovider.cpp
|
||||
tag.cpp
|
||||
|
|
10
README.md
10
README.md
|
@ -1,13 +1,14 @@
|
|||
# Tag Parser
|
||||
C++ library for reading and writing MP4 (iTunes), ID3, Vorbis and Matroska tags.
|
||||
C++ library for reading and writing MP4 (iTunes), ID3, Vorbis, Opus, FLAC and Matroska tags.
|
||||
|
||||
## Supported formats
|
||||
The tag library can read and write the following tag formats:
|
||||
- iTunes-style MP4 tags (MP4-DASH is supported)
|
||||
- iTunes-style MP4/M4A tags (MP4-DASH is supported)
|
||||
- ID3v1 and ID3v2 tags
|
||||
- conversion between ID3v1 and different versions of ID3v2
|
||||
- conversion between ID3v1 and different versions of ID3v2 is possible
|
||||
- Vorbis, Opus and FLAC comments in Ogg streams
|
||||
- cover art via "METADATA_BLOCK_PICTURE" is supported
|
||||
- Vorbis comments and "METADATA_BLOCK_PICTURE" in raw FLAC streams
|
||||
- Matroska/WebM tags and attachments
|
||||
|
||||
## File layout options
|
||||
|
@ -23,7 +24,8 @@ or appending the tag. Usage of padding can be configured:
|
|||
|
||||
However, it is also possible to force rewriting the entire file.
|
||||
|
||||
Taking advantage of padding is currently not supported when dealing with Ogg streams.
|
||||
Taking advantage of padding is currently not supported when dealing with Ogg streams (it is supported when
|
||||
dealing with raw FLAC streams).
|
||||
|
||||
## Additional features
|
||||
The library can also display technical information such as the ID, format, language, bitrate,
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
#include <c++utilities/chrono/datetime.h>
|
||||
#include <c++utilities/chrono/timespan.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
|
||||
namespace Media {
|
||||
|
@ -35,7 +35,8 @@ enum class TrackType
|
|||
Mp4Track, /**< The track is a Media::Mp4Track. */
|
||||
WaveAudioStream, /**< The track is a Media::WaveAudioStream. */
|
||||
OggStream, /**< The track is a Media::OggStream. */
|
||||
AdtsStream /**< The track is a Media::AdtsStream. */
|
||||
AdtsStream, /**< The track is a Media::AdtsStream. */
|
||||
FlacStream, /**< The track is a Media::FlacStream. */
|
||||
};
|
||||
|
||||
class LIB_EXPORT AbstractTrack : public StatusProvider
|
||||
|
|
10
exceptions.h
10
exceptions.h
|
@ -64,6 +64,16 @@ public:
|
|||
virtual const char *what() const USE_NOTHROW;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Throws TruncatedDataException() if the specified \a sizeDenotation exceeds maxSize; otherwise maxSize is reduced by \a sizeDenotation.
|
||||
*/
|
||||
#define CHECK_MAX_SIZE(sizeDenotation) \
|
||||
if(maxSize < sizeDenotation) { \
|
||||
throw TruncatedDataException(); \
|
||||
} else { \
|
||||
maxSize -= sizeDenotation; \
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif // MEDIA_EXCEPTIONS_H
|
||||
|
|
|
@ -54,7 +54,7 @@ void FlacMetaDataBlockHeader::makeHeader(std::ostream &outputStream)
|
|||
|
||||
/*!
|
||||
* \brief Parses the FLAC "METADATA_BLOCK_STREAMINFO" which is read using the specified \a iterator.
|
||||
* \remarks The specified \a buffer must be at least 34 bytes long.
|
||||
* \remarks The specified \a buffer must be at least 0x22 bytes long.
|
||||
*/
|
||||
void FlacMetaDataBlockStreamInfo::parse(const char *buffer)
|
||||
{
|
||||
|
@ -78,18 +78,24 @@ void FlacMetaDataBlockStreamInfo::parse(const char *buffer)
|
|||
|
||||
/*!
|
||||
* \brief Parses the FLAC "METADATA_BLOCK_PICTURE".
|
||||
*
|
||||
* \a maxSize specifies the maximum size of the structure.
|
||||
*/
|
||||
void FlacMetaDataBlockPicture::parse(istream &inputStream)
|
||||
void FlacMetaDataBlockPicture::parse(istream &inputStream, uint32 maxSize)
|
||||
{
|
||||
CHECK_MAX_SIZE(32);
|
||||
BinaryReader reader(&inputStream);
|
||||
m_pictureType = reader.readUInt32BE();
|
||||
auto size = reader.readUInt32BE();
|
||||
uint32 size = reader.readUInt32BE();
|
||||
CHECK_MAX_SIZE(size);
|
||||
m_value.setMimeType(reader.readString(size));
|
||||
size = reader.readUInt32BE();
|
||||
CHECK_MAX_SIZE(size);
|
||||
m_value.setDescription(reader.readString(size));
|
||||
// skip width, height, color depth, number of colors used
|
||||
inputStream.seekg(4 * 4, ios_base::cur);
|
||||
size = reader.readUInt32BE();
|
||||
CHECK_MAX_SIZE(size);
|
||||
auto data = make_unique<char[]>(size);
|
||||
inputStream.read(data.get(), size);
|
||||
m_value.assignData(move(data), size, TagDataType::Picture);
|
||||
|
|
|
@ -66,6 +66,7 @@ inline FlacMetaDataBlockHeader::FlacMetaDataBlockHeader() :
|
|||
|
||||
/*!
|
||||
* \brief Returns whether this is the last metadata block before the audio blocks.
|
||||
* \remarks The default value is 0/false.
|
||||
*/
|
||||
inline byte FlacMetaDataBlockHeader::isLast() const
|
||||
{
|
||||
|
@ -258,7 +259,7 @@ class LIB_EXPORT FlacMetaDataBlockPicture
|
|||
public:
|
||||
FlacMetaDataBlockPicture(TagValue &tagValue);
|
||||
|
||||
void parse(std::istream &inputStream);
|
||||
void parse(std::istream &inputStream, uint32 maxSize);
|
||||
uint32 requiredSize() const;
|
||||
void make(std::ostream &outputStream);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
|
@ -12,8 +12,7 @@
|
|||
#include <c++utilities/conversion/stringconversion.h>
|
||||
|
||||
#include <string>
|
||||
#include <istream>
|
||||
#include <ostream>
|
||||
#include <iosfwd>
|
||||
#include <vector>
|
||||
|
||||
namespace Media
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
#include "../exceptions.h"
|
||||
#include "../backuphelper.h"
|
||||
|
||||
// include configuration from separate header file when building with CMake
|
||||
#include "resources/config.h"
|
||||
|
||||
#include <c++utilities/conversion/stringconversion.h>
|
||||
|
|
|
@ -27,6 +27,9 @@
|
|||
|
||||
#include "./ogg/oggcontainer.h"
|
||||
|
||||
#include "./flac/flacstream.h"
|
||||
#include "./flac/flacmetadata.h"
|
||||
|
||||
#include <c++utilities/conversion/stringconversion.h>
|
||||
#include <c++utilities/chrono/timespan.h>
|
||||
#include <c++utilities/misc/memory.h>
|
||||
|
@ -306,19 +309,31 @@ void MediaFileInfo::parseTracks()
|
|||
m_container->parseTracks();
|
||||
} else {
|
||||
switch(m_containerFormat) {
|
||||
case ContainerFormat::RiffWave:
|
||||
m_singleTrack = make_unique<WaveAudioStream>(stream(), m_containerOffset);
|
||||
case ContainerFormat::Adts:
|
||||
m_singleTrack = make_unique<AdtsStream>(stream(), m_containerOffset);
|
||||
break;
|
||||
case ContainerFormat::Flac:
|
||||
m_singleTrack = make_unique<FlacStream>(*this, m_containerOffset);
|
||||
break;
|
||||
case ContainerFormat::MpegAudioFrames:
|
||||
m_singleTrack = make_unique<MpegAudioFrameStream>(stream(), m_containerOffset);
|
||||
break;
|
||||
case ContainerFormat::Adts:
|
||||
m_singleTrack = make_unique<AdtsStream>(stream(), m_containerOffset);
|
||||
case ContainerFormat::RiffWave:
|
||||
m_singleTrack = make_unique<WaveAudioStream>(stream(), m_containerOffset);
|
||||
break;
|
||||
default:
|
||||
throw NotImplementedException();
|
||||
}
|
||||
m_singleTrack->parseHeader();
|
||||
|
||||
switch(m_containerFormat) {
|
||||
case ContainerFormat::Flac:
|
||||
// FLAC streams might container padding
|
||||
m_paddingSize += static_cast<FlacStream *>(m_singleTrack.get())->paddingSize();
|
||||
break;
|
||||
default:
|
||||
;
|
||||
}
|
||||
}
|
||||
m_tracksParsingStatus = ParsingStatus::Ok;
|
||||
} catch(const NotImplementedException &) {
|
||||
|
@ -539,27 +554,34 @@ bool MediaFileInfo::createAppropriateTags(bool treatUnknownFilesAsMp3Files, TagU
|
|||
m_container->createTag();
|
||||
}
|
||||
} else {
|
||||
// no container object present; creation of ID3 tag is possible
|
||||
if(!hasAnyTag() && !treatUnknownFilesAsMp3Files) {
|
||||
switch(containerFormat()) {
|
||||
case ContainerFormat::MpegAudioFrames:
|
||||
case ContainerFormat::Adts:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// create ID3 tags according to id3v2usage and id3v2usage
|
||||
if(id3v1usage == TagUsage::Always) {
|
||||
// always create ID3v1 tag -> ensure there is one
|
||||
createId3v1Tag();
|
||||
}
|
||||
if(id3v2usage == TagUsage::Always) {
|
||||
// always create ID3v2 tag -> ensure there is one and set version
|
||||
if(!hasId3v2Tag()) {
|
||||
createId3v2Tag()->setVersion(id3v2version, 0);
|
||||
// no container object present
|
||||
if(m_containerFormat == ContainerFormat::Flac) {
|
||||
// creation of Vorbis comment is possible
|
||||
static_cast<FlacStream *>(m_singleTrack.get())->createVorbisComment();
|
||||
} else {
|
||||
// creation of ID3 tag is possible
|
||||
if(!hasAnyTag() && !treatUnknownFilesAsMp3Files) {
|
||||
switch(containerFormat()) {
|
||||
case ContainerFormat::MpegAudioFrames:
|
||||
case ContainerFormat::Adts:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// create ID3 tags according to id3v2usage and id3v2usage
|
||||
if(id3v1usage == TagUsage::Always) {
|
||||
// always create ID3v1 tag -> ensure there is one
|
||||
createId3v1Tag();
|
||||
}
|
||||
if(id3v2usage == TagUsage::Always) {
|
||||
// always create ID3v2 tag -> ensure there is one and set version
|
||||
if(!hasId3v2Tag()) {
|
||||
createId3v2Tag()->setVersion(id3v2version, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(mergeMultipleSuccessiveId3v2Tags) {
|
||||
mergeId3v2Tags();
|
||||
}
|
||||
|
@ -1031,12 +1053,13 @@ bool MediaFileInfo::areTracksSupported() const
|
|||
bool MediaFileInfo::areTagsSupported() const
|
||||
{
|
||||
switch(m_containerFormat) {
|
||||
case ContainerFormat::Mp4:
|
||||
case ContainerFormat::MpegAudioFrames:
|
||||
case ContainerFormat::Ogg:
|
||||
case ContainerFormat::Matroska:
|
||||
case ContainerFormat::Webm:
|
||||
case ContainerFormat::Adts:
|
||||
case ContainerFormat::Flac:
|
||||
case ContainerFormat::Matroska:
|
||||
case ContainerFormat::MpegAudioFrames:
|
||||
case ContainerFormat::Mp4:
|
||||
case ContainerFormat::Ogg:
|
||||
case ContainerFormat::Webm:
|
||||
// these container formats are supported
|
||||
return true;
|
||||
default:
|
||||
|
@ -1081,7 +1104,11 @@ const vector<unique_ptr<MatroskaTag> > &MediaFileInfo::matroskaTags() 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
|
||||
{
|
||||
if(hasId3v1Tag())
|
||||
if(hasId3v1Tag()) {
|
||||
tags.push_back(m_id3v1Tag.get());
|
||||
}
|
||||
for(const unique_ptr<Id3v2Tag> &tag : m_id3v2Tags) {
|
||||
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) {
|
||||
for(size_t i = 0, count = m_container->tagCount(); i < count; ++i) {
|
||||
tags.push_back(m_container->tag(i));
|
||||
|
@ -1342,6 +1375,17 @@ void MediaFileInfo::tags(vector<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.
|
||||
*
|
||||
|
@ -1371,9 +1415,9 @@ void MediaFileInfo::invalidated()
|
|||
*/
|
||||
void MediaFileInfo::makeMp3File()
|
||||
{
|
||||
const string context("making MP3 file");
|
||||
static const string context("making MP3 file");
|
||||
// there's no need to rewrite the complete file if there are no ID3v2 tags present or to be written
|
||||
if(!isForcingRewrite() && m_id3v2Tags.empty() && m_actualId3v2TagOffsets.empty() && m_saveFilePath.empty()) {
|
||||
if(!isForcingRewrite() && m_id3v2Tags.empty() && m_actualId3v2TagOffsets.empty() && m_saveFilePath.empty() && m_containerFormat != ContainerFormat::Flac) {
|
||||
if(m_actualExistingId3v1Tag) {
|
||||
// there is currently an ID3v1 tag at the end of the file
|
||||
if(m_id3v1Tag) {
|
||||
|
@ -1434,30 +1478,57 @@ void MediaFileInfo::makeMp3File()
|
|||
addNotifications(*tag);
|
||||
}
|
||||
|
||||
// check whether it is a raw FLAC stream
|
||||
FlacStream *flacStream = (m_containerFormat == ContainerFormat::Flac ? static_cast<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
|
||||
bool rewriteRequired = isForcingRewrite() || !m_saveFilePath.empty() || (tagsSize > static_cast<uint32>(m_containerOffset));
|
||||
bool rewriteRequired = isForcingRewrite() || !m_saveFilePath.empty() || (tagsSize > streamOffset);
|
||||
uint32 padding = 0;
|
||||
if(!rewriteRequired) {
|
||||
// rewriting is not forced and new tag is not too big for available space
|
||||
// -> calculate new padding
|
||||
padding = static_cast<uint32>(m_containerOffset) - tagsSize;
|
||||
padding = streamOffset - tagsSize;
|
||||
// -> check whether the new padding matches specifications
|
||||
if(padding < minPadding() || padding > maxPadding()) {
|
||||
rewriteRequired = true;
|
||||
}
|
||||
}
|
||||
if(makers.empty()) {
|
||||
// an ID3v2 tag shouldn't be written
|
||||
if(makers.empty() && !flacStream) {
|
||||
// an ID3v2 tag is not written and it is not a FLAC stream
|
||||
// -> can't include padding
|
||||
if(padding) {
|
||||
// but padding would be present -> need to rewrite
|
||||
padding = 0;
|
||||
padding = 0; // can't write the preferred padding despite rewriting
|
||||
rewriteRequired = true;
|
||||
}
|
||||
} else if(rewriteRequired) {
|
||||
// rewriting is forced or new ID3v2 tag is too big for available space
|
||||
// -> use preferred padding when rewriting anyways
|
||||
padding = preferredPadding();
|
||||
} else if(makers.empty() && flacStream && padding && padding < 4) {
|
||||
// no ID3v2 tag -> must include padding in FLAC stream
|
||||
// but padding of 1, 2, and 3 byte isn't possible -> need to rewrite
|
||||
padding = preferredPadding();
|
||||
rewriteRequired = true;
|
||||
}
|
||||
if(rewriteRequired && flacStream && makers.empty() && padding) {
|
||||
// the first 4 byte of FLAC padding actually don't count because these
|
||||
// can not be used for additional meta data
|
||||
padding += 4;
|
||||
}
|
||||
updateStatus(rewriteRequired ? "Preparing streams for rewriting ..." : "Preparing streams for updating ...");
|
||||
|
||||
|
@ -1490,7 +1561,6 @@ void MediaFileInfo::makeMp3File()
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
} else { // !rewriteRequired
|
||||
// reopen original file to ensure it is opened for writing
|
||||
try {
|
||||
|
@ -1511,30 +1581,56 @@ void MediaFileInfo::makeMp3File()
|
|||
i->make(outputStream, 0);
|
||||
}
|
||||
// include padding into the last ID3v2 tag
|
||||
makers.back().make(outputStream, padding);
|
||||
} else {
|
||||
// just write padding
|
||||
makers.back().make(outputStream, (flacStream && padding && padding < 4) ? 0 : padding);
|
||||
}
|
||||
|
||||
if(flacStream) {
|
||||
if(padding && startOfLastMetaDataBlock) {
|
||||
// if appending padding, ensure the last flag of the last "METADATA_BLOCK_HEADER" is not set
|
||||
flacMetaData.seekg(startOfLastMetaDataBlock);
|
||||
flacMetaData.seekp(startOfLastMetaDataBlock);
|
||||
flacMetaData.put(static_cast<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) {
|
||||
outputStream.put(0);
|
||||
}
|
||||
}
|
||||
|
||||
// copy / skip media data
|
||||
// copy / skip actual stream data
|
||||
// -> determine media data size
|
||||
uint64 mediaDataSize = size() - m_containerOffset;
|
||||
uint64 mediaDataSize = size() - streamOffset;
|
||||
if(m_actualExistingId3v1Tag) {
|
||||
mediaDataSize -= 128;
|
||||
}
|
||||
|
||||
if(rewriteRequired) {
|
||||
// copy data from original file
|
||||
updateStatus("Writing MPEG audio frames ...");
|
||||
backupStream.seekg(m_containerOffset);
|
||||
switch(m_containerFormat) {
|
||||
case ContainerFormat::MpegAudioFrames:
|
||||
updateStatus("Writing MPEG audio frames ...");
|
||||
break;
|
||||
default:
|
||||
updateStatus("Writing frames ...");
|
||||
}
|
||||
backupStream.seekg(streamOffset);
|
||||
CopyHelper<0x4000> copyHelper;
|
||||
copyHelper.callbackCopy(backupStream, stream(), mediaDataSize, bind(&StatusProvider::isAborted, this), bind(&StatusProvider::updatePercentage, this, _1));
|
||||
updatePercentage(100.0);
|
||||
} else {
|
||||
// just skip media data
|
||||
// just skip actual stream data
|
||||
outputStream.seekp(mediaDataSize, ios_base::cur);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
#include "./basicfileinfo.h"
|
||||
#include "./abstractcontainer.h"
|
||||
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
|
@ -24,8 +22,6 @@ class OggContainer;
|
|||
class EbmlElement;
|
||||
class MatroskaTag;
|
||||
class AbstractTrack;
|
||||
class WaveAudioStream;
|
||||
class MpegAudioFrameStream;
|
||||
class VorbisComment;
|
||||
|
||||
enum class MediaType;
|
||||
|
@ -335,14 +331,6 @@ inline bool MediaFileInfo::hasId3v2Tag() const
|
|||
return m_id3v2Tags.size();
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an indication whether a tag of any format is assigned.
|
||||
*/
|
||||
inline bool MediaFileInfo::hasAnyTag() const
|
||||
{
|
||||
return hasId3v1Tag() || hasId3v2Tag() || (m_container && m_container->tagCount());
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns a pointer to the assigned ID3v1 tag or nullptr if none is assigned.
|
||||
*
|
||||
|
|
|
@ -259,6 +259,7 @@ public:
|
|||
operator bool() const;
|
||||
MediaFormat &operator+=(const MediaFormat &other);
|
||||
bool operator==(GeneralMediaFormat general) const;
|
||||
bool operator!=(GeneralMediaFormat general) const;
|
||||
|
||||
GeneralMediaFormat general;
|
||||
unsigned char sub;
|
||||
|
@ -299,6 +300,14 @@ inline bool MediaFormat::operator==(GeneralMediaFormat general) const
|
|||
return this->general == general;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns whether the media format is not the specified general media format.
|
||||
*/
|
||||
inline bool MediaFormat::operator!=(GeneralMediaFormat general) const
|
||||
{
|
||||
return this->general != general;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns whether the media format is known.
|
||||
*/
|
||||
|
|
|
@ -235,11 +235,12 @@ void OggContainer::internalParseTags()
|
|||
break;
|
||||
case GeneralMediaFormat::Opus:
|
||||
// skip header (has already been detected by OggStream)
|
||||
m_iterator.seekForward(8);
|
||||
m_iterator.ignore(8);
|
||||
comment->parse(m_iterator, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte);
|
||||
break;
|
||||
case GeneralMediaFormat::Flac:
|
||||
comment->parse(m_iterator, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, 4);
|
||||
m_iterator.ignore(4);
|
||||
comment->parse(m_iterator, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte);
|
||||
break;
|
||||
default:
|
||||
addNotification(NotificationType::Critical, "Stream format not supported.", "parsing tags from OGG streams");
|
||||
|
@ -287,7 +288,7 @@ void OggContainer::internalParseTracks()
|
|||
*/
|
||||
void OggContainer::makeVorbisCommentSegment(stringstream &buffer, CopyHelper<65307> ©Helper, vector<uint32> &newSegmentSizes, VorbisComment *comment, OggParameter *params)
|
||||
{
|
||||
auto offset = buffer.tellp();
|
||||
const auto offset = buffer.tellp();
|
||||
switch(params->streamFormat) {
|
||||
case GeneralMediaFormat::Vorbis:
|
||||
comment->make(buffer);
|
||||
|
|
|
@ -123,6 +123,7 @@ void OggIterator::previousSegment()
|
|||
* - Page headers are skipped (this is the whole purpose of this method).
|
||||
* \throws Throws a TruncatedDataException if the end of the stream is reached before \a count bytes
|
||||
* have been read.
|
||||
* \sa readAll()
|
||||
* \sa currentCharacterOffset()
|
||||
* \sa seekForward()
|
||||
*/
|
||||
|
@ -149,6 +150,36 @@ void OggIterator::read(char *buffer, size_t count)
|
|||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Reads all bytes from the OGG stream and writes it to the specified \a buffer.
|
||||
* \remarks
|
||||
* - Might increase the current page index and/or the current segment index.
|
||||
* - Page headers are skipped (this is the whole purpose of this method).
|
||||
* - Does not read more than \a max bytes from the buffer.
|
||||
* \sa read()
|
||||
* \sa currentCharacterOffset()
|
||||
* \sa seekForward()
|
||||
*/
|
||||
size_t OggIterator::readAll(char *buffer, size_t max)
|
||||
{
|
||||
size_t bytesRead = 0;
|
||||
while(*this && max) {
|
||||
uint32 available = currentSegmentSize() - m_bytesRead;
|
||||
stream().seekg(currentCharacterOffset());
|
||||
if(max <= available) {
|
||||
stream().read(buffer + bytesRead, max);
|
||||
m_bytesRead += max;
|
||||
return bytesRead + max;
|
||||
} else {
|
||||
stream().read(buffer + bytesRead, available);
|
||||
nextSegment();
|
||||
bytesRead += available;
|
||||
max -= available;
|
||||
}
|
||||
}
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Advances the position of the next character to be read from the OGG stream by \a count bytes.
|
||||
* \remarks
|
||||
|
@ -159,7 +190,7 @@ void OggIterator::read(char *buffer, size_t count)
|
|||
* \sa currentCharacterOffset()
|
||||
* \sa read()
|
||||
*/
|
||||
void OggIterator::seekForward(size_t count)
|
||||
void OggIterator::ignore(size_t count)
|
||||
{
|
||||
uint32 available = currentSegmentSize() - m_bytesRead;
|
||||
while(*this) {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
#include "./oggpage.h"
|
||||
|
||||
#include <istream>
|
||||
#include <iosfwd>
|
||||
#include <vector>
|
||||
|
||||
namespace Media {
|
||||
|
@ -31,13 +31,15 @@ public:
|
|||
std::vector<uint32>::size_type currentSegmentIndex() const;
|
||||
uint64 currentSegmentOffset() const;
|
||||
uint64 currentCharacterOffset() const;
|
||||
uint64 tellg() const;
|
||||
uint32 currentSegmentSize() const;
|
||||
void setFilter(uint32 streamSerialId);
|
||||
void removeFilter();
|
||||
bool areAllPagesFetched() const;
|
||||
void read(char *buffer, size_t count);
|
||||
void seekForward(size_t count);
|
||||
bool bytesRemaining(size_t atLeast) const;
|
||||
void read(char *buffer, std::size_t count);
|
||||
size_t readAll(char *buffer, std::size_t max);
|
||||
void ignore(std::size_t count = 1);
|
||||
bool bytesRemaining(std::size_t atLeast) const;
|
||||
|
||||
operator bool() const;
|
||||
OggIterator &operator++();
|
||||
|
@ -201,6 +203,14 @@ inline uint64 OggIterator::currentCharacterOffset() const
|
|||
return m_offset + m_bytesRead;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Same as currentCharacterOffset(); only provided for compliance with std::istream.
|
||||
*/
|
||||
inline uint64 OggIterator::tellg() const
|
||||
{
|
||||
return currentCharacterOffset();
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the size of the current segment.
|
||||
*
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
#include <vector>
|
||||
#include <numeric>
|
||||
#include <istream>
|
||||
#include <iosfwd>
|
||||
|
||||
namespace Media {
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ enum Sig48 : uint64
|
|||
enum Sig32 : uint32
|
||||
{
|
||||
Elf = 0x7F454C46u,
|
||||
Flac = 0x664C6143u,
|
||||
JavaClassFile = 0xCAFEBABEu,
|
||||
Ebml = 0x1A45DFA3u,
|
||||
Mp4 = 0x66747970u,
|
||||
|
@ -150,6 +151,8 @@ ContainerFormat parseSignature(const char *buffer, int bufferSize)
|
|||
switch(sig >> 32) { // check 32-bit signatures
|
||||
case Elf:
|
||||
return ContainerFormat::Elf;
|
||||
case Flac:
|
||||
return ContainerFormat::Flac;
|
||||
case JavaClassFile:
|
||||
return ContainerFormat::JavaClassFile;
|
||||
case Ebml:
|
||||
|
@ -235,6 +238,7 @@ const char *containerFormatAbbreviation(ContainerFormat containerFormat, MediaTy
|
|||
case ContainerFormat::Ar: return "a";
|
||||
case ContainerFormat::Asf: return "asf";
|
||||
case ContainerFormat::Elf: return "elf";
|
||||
case ContainerFormat::Flac: return "flac";
|
||||
case ContainerFormat::FlashVideo: return "flv";
|
||||
case ContainerFormat::Gif87a:
|
||||
case ContainerFormat::Gif89a: return "gif";
|
||||
|
@ -315,6 +319,8 @@ const char *containerFormatName(ContainerFormat containerFormat)
|
|||
return "Advanced Systems Format";
|
||||
case ContainerFormat::Elf:
|
||||
return "Executable and Linkable Format";
|
||||
case ContainerFormat::Flac:
|
||||
return "raw Free Lossless Audio Codec frames";
|
||||
case ContainerFormat::FlashVideo:
|
||||
return "Flash Video";
|
||||
case ContainerFormat::Gif87a:
|
||||
|
@ -417,6 +423,8 @@ const char *containerMimeType(ContainerFormat containerFormat, MediaType mediaTy
|
|||
switch(containerFormat) {
|
||||
case ContainerFormat::Asf:
|
||||
return "video/x-ms-asf";
|
||||
case ContainerFormat::Flac:
|
||||
return "audio/flac";
|
||||
case ContainerFormat::FlashVideo:
|
||||
return "video/x-flv";
|
||||
case ContainerFormat::Gif87a:
|
||||
|
|
|
@ -10,6 +10,8 @@ namespace Media {
|
|||
|
||||
/*!
|
||||
* \brief Specifies the container format.
|
||||
*
|
||||
* Raw streams like ADTS or raw FLAC count as container format in this context.
|
||||
*/
|
||||
enum class ContainerFormat
|
||||
{
|
||||
|
@ -19,6 +21,7 @@ enum class ContainerFormat
|
|||
Asf, /**< Advanced Systems Format */
|
||||
Bzip2, /** bzip2 compressed file */
|
||||
Elf, /**< Executable and Linkable Format */
|
||||
Flac, /** < Free Lossless Audio Codec (raw stream) */
|
||||
FlashVideo, /**< Flash (FLV) */
|
||||
Gif87a, /**< Graphics Interchange Format (1987) */
|
||||
Gif89a, /**< Graphics Interchange Format (1989) */
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
#include <c++utilities/chrono/timespan.h>
|
||||
#include <c++utilities/chrono/datetime.h>
|
||||
|
||||
#include <istream>
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
|
|
|
@ -100,51 +100,51 @@ KnownField VorbisComment::knownField(const string &id) const
|
|||
}
|
||||
|
||||
/*!
|
||||
* \brief Parses tag information using the specified OGG \a iterator.
|
||||
*
|
||||
* \throws Throws std::ios_base::failure when an IO error occurs.
|
||||
* \throws Throws Media::Failure or a derived exception when a parsing
|
||||
* error occurs.
|
||||
* \brief Internal implementation for parsing.
|
||||
*/
|
||||
void VorbisComment::parse(OggIterator &iterator, VorbisCommentFlags flags, size_t offset)
|
||||
template<class StreamType>
|
||||
void VorbisComment::internalParse(StreamType &stream, uint64 maxSize, VorbisCommentFlags flags)
|
||||
{
|
||||
// prepare parsing
|
||||
invalidateStatus();
|
||||
static const string context("parsing Vorbis comment");
|
||||
auto startOffset = iterator.currentSegmentOffset() + offset;
|
||||
iterator.seekForward(offset);
|
||||
uint64 startOffset = static_cast<uint64>(stream.tellg());
|
||||
try {
|
||||
// read signature: 0x3 + "vorbis"
|
||||
char sig[8];
|
||||
bool skipSignature = flags & VorbisCommentFlags::NoSignature;
|
||||
if(!skipSignature) {
|
||||
iterator.read(sig, 7);
|
||||
CHECK_MAX_SIZE(7);
|
||||
stream.read(sig, 7);
|
||||
skipSignature = (ConversionUtilities::BE::toUInt64(sig) & 0xffffffffffffff00u) == 0x03766F7262697300u;
|
||||
}
|
||||
if(skipSignature) {
|
||||
// read vendor (length prefixed string)
|
||||
{
|
||||
iterator.read(sig, 4);
|
||||
CHECK_MAX_SIZE(4);
|
||||
stream.read(sig, 4);
|
||||
const auto vendorSize = LE::toUInt32(sig);
|
||||
if(iterator.currentCharacterOffset() + vendorSize <= iterator.streamSize()) {
|
||||
if(vendorSize <= maxSize) {
|
||||
auto buff = make_unique<char []>(vendorSize);
|
||||
iterator.read(buff.get(), vendorSize);
|
||||
stream.read(buff.get(), vendorSize);
|
||||
m_vendor.assignData(move(buff), vendorSize, TagDataType::Text, TagTextEncoding::Utf8);
|
||||
// TODO: Is the vendor string actually UTF-8 (like the field values)?
|
||||
} else {
|
||||
addNotification(NotificationType::Critical, "Vendor information is truncated.", context);
|
||||
throw TruncatedDataException();
|
||||
}
|
||||
maxSize -= vendorSize;
|
||||
}
|
||||
// read field count
|
||||
iterator.read(sig, 4);
|
||||
CHECK_MAX_SIZE(4);
|
||||
stream.read(sig, 4);
|
||||
uint32 fieldCount = LE::toUInt32(sig);
|
||||
VorbisCommentField field;
|
||||
const string &fieldId = field.id();
|
||||
for(uint32 i = 0; i < fieldCount; ++i) {
|
||||
// read fields
|
||||
try {
|
||||
field.parse(iterator);
|
||||
field.parse(stream, maxSize);
|
||||
fields().insert(pair<fieldType::identifierType, fieldType>(fieldId, field));
|
||||
} catch(const TruncatedDataException &) {
|
||||
addNotifications(field);
|
||||
|
@ -156,20 +156,44 @@ void VorbisComment::parse(OggIterator &iterator, VorbisCommentFlags flags, size_
|
|||
field.invalidateNotifications();
|
||||
}
|
||||
if(!(flags & VorbisCommentFlags::NoFramingByte)) {
|
||||
iterator.seekForward(1); // skip framing byte
|
||||
stream.ignore(); // skip framing byte
|
||||
}
|
||||
m_size = static_cast<uint32>(static_cast<uint64>(iterator.currentCharacterOffset()) - startOffset);
|
||||
m_size = static_cast<uint32>(static_cast<uint64>(stream.tellg()) - startOffset);
|
||||
} else {
|
||||
addNotification(NotificationType::Critical, "Signature is invalid.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
} 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);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Parses tag information using the specified OGG \a iterator.
|
||||
*
|
||||
* \throws Throws std::ios_base::failure when an IO error occurs.
|
||||
* \throws Throws Media::Failure or a derived exception when a parsing
|
||||
* error occurs.
|
||||
*/
|
||||
void VorbisComment::parse(OggIterator &iterator, VorbisCommentFlags flags)
|
||||
{
|
||||
internalParse(iterator, iterator.streamSize(), flags);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Parses tag information using the specified OGG \a iterator.
|
||||
*
|
||||
* \throws Throws std::ios_base::failure when an IO error occurs.
|
||||
* \throws Throws Media::Failure or a derived exception when a parsing
|
||||
* error occurs.
|
||||
*/
|
||||
void VorbisComment::parse(istream &stream, uint64 maxSize, VorbisCommentFlags flags)
|
||||
{
|
||||
internalParse(stream, maxSize, flags);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Writes tag information to the specified \a stream.
|
||||
*
|
||||
|
@ -197,14 +221,18 @@ void VorbisComment::make(std::ostream &stream, VorbisCommentFlags flags)
|
|||
// write vendor
|
||||
writer.writeUInt32LE(vendor.size());
|
||||
writer.writeString(vendor);
|
||||
// write field count
|
||||
writer.writeUInt32LE(fieldCount());
|
||||
// write field count later
|
||||
const auto fieldCountOffset = stream.tellp();
|
||||
writer.writeUInt32LE(0);
|
||||
// write fields
|
||||
uint32 fieldsWritten = 0;
|
||||
for(auto i : fields()) {
|
||||
VorbisCommentField &field = i.second;
|
||||
if(!field.value().isEmpty()) {
|
||||
try {
|
||||
field.make(writer);
|
||||
if(field.make(writer, flags)) {
|
||||
++fieldsWritten;
|
||||
}
|
||||
} catch(const Failure &) {
|
||||
// nothing to do here since notifications will be added anyways
|
||||
}
|
||||
|
@ -213,6 +241,11 @@ void VorbisComment::make(std::ostream &stream, VorbisCommentFlags flags)
|
|||
field.invalidateNotifications();
|
||||
}
|
||||
}
|
||||
// write field count
|
||||
const auto framingByteOffset = stream.tellp();
|
||||
stream.seekp(fieldCountOffset);
|
||||
writer.writeUInt32LE(fieldsWritten);
|
||||
stream.seekp(framingByteOffset);
|
||||
// write framing byte
|
||||
if(!(flags & VorbisCommentFlags::NoFramingByte)) {
|
||||
stream.put(0x01);
|
||||
|
|
|
@ -12,26 +12,6 @@ namespace Media {
|
|||
class OggIterator;
|
||||
class VorbisComment;
|
||||
|
||||
/*!
|
||||
* \brief The VorbisCommentFlags enum specifies flags which controls parsing and making of Vorbis comments.
|
||||
*/
|
||||
enum class VorbisCommentFlags : byte
|
||||
{
|
||||
None = 0x0, /**< Regular parsing/making. */
|
||||
NoSignature = 0x1, /**< Skips the signature when parsing; does not make the signature when making. */
|
||||
NoFramingByte = 0x2 /**< Doesn't expect the framing bit to be present when parsing; does not make the framing bit when making. */
|
||||
};
|
||||
|
||||
inline bool operator &(VorbisCommentFlags lhs, VorbisCommentFlags rhs)
|
||||
{
|
||||
return static_cast<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>
|
||||
{
|
||||
public:
|
||||
|
@ -47,12 +27,17 @@ public:
|
|||
std::string fieldId(KnownField field) const;
|
||||
KnownField knownField(const std::string &id) const;
|
||||
|
||||
void parse(OggIterator &iterator, VorbisCommentFlags flags = VorbisCommentFlags::None, std::size_t offset = 0);
|
||||
void parse(OggIterator &iterator, VorbisCommentFlags flags = VorbisCommentFlags::None);
|
||||
void parse(std::istream &stream, uint64 maxSize, VorbisCommentFlags flags = VorbisCommentFlags::None);
|
||||
void make(std::ostream &stream, VorbisCommentFlags flags = VorbisCommentFlags::None);
|
||||
|
||||
const TagValue &vendor() const;
|
||||
void setVendor(const TagValue &vendor);
|
||||
|
||||
private:
|
||||
template<class StreamType>
|
||||
void internalParse(StreamType &stream, uint64 maxSize, VorbisCommentFlags flags);
|
||||
|
||||
private:
|
||||
TagValue m_vendor;
|
||||
};
|
||||
|
|
|
@ -42,25 +42,26 @@ VorbisCommentField::VorbisCommentField(const identifierType &id, const TagValue
|
|||
{}
|
||||
|
||||
/*!
|
||||
* \brief Parses a field from the stream read using the specified \a reader.
|
||||
*
|
||||
* The position of the current character in the input stream is expected to be
|
||||
* at the beginning of the field to be parsed.
|
||||
*
|
||||
* \throws Throws std::ios_base::failure when an IO error occurs.
|
||||
* \throws Throws Media::Failure or a derived exception when a parsing
|
||||
* error occurs.
|
||||
* \brief Internal implementation for parsing.
|
||||
*/
|
||||
void VorbisCommentField::parse(OggIterator &iterator)
|
||||
template<class StreamType>
|
||||
void VorbisCommentField::internalParse(StreamType &stream, uint64 &maxSize)
|
||||
{
|
||||
static const string context("parsing Vorbis comment field");
|
||||
char buff[4];
|
||||
iterator.read(buff, 4);
|
||||
if(auto size = LE::toUInt32(buff)) { // read size
|
||||
if(iterator.currentCharacterOffset() + size <= iterator.streamSize()) {
|
||||
if(maxSize < 4) {
|
||||
addNotification(NotificationType::Critical, "Field expected.", context);
|
||||
throw TruncatedDataException();
|
||||
} else {
|
||||
maxSize -= 4;
|
||||
}
|
||||
stream.read(buff, 4);
|
||||
if(const auto size = LE::toUInt32(buff)) { // read size
|
||||
if(size <= maxSize) {
|
||||
maxSize -= size;
|
||||
// read data
|
||||
auto data = make_unique<char []>(size);
|
||||
iterator.read(data.get(), size);
|
||||
stream.read(data.get(), size);
|
||||
uint32 idSize = 0;
|
||||
for(const char *i = data.get(), *end = data.get() + size; i != end && *i != '='; ++i, ++idSize);
|
||||
// extract id
|
||||
|
@ -77,13 +78,16 @@ void VorbisCommentField::parse(OggIterator &iterator)
|
|||
bufferStream.exceptions(ios_base::failbit | ios_base::badbit);
|
||||
bufferStream.rdbuf()->pubsetbuf(reinterpret_cast<char *>(decoded.first.get()), decoded.second);
|
||||
FlacMetaDataBlockPicture pictureBlock(value());
|
||||
pictureBlock.parse(bufferStream);
|
||||
pictureBlock.parse(bufferStream, decoded.second);
|
||||
setTypeInfo(pictureBlock.pictureType());
|
||||
} catch (const ios_base::failure &) {
|
||||
} catch(const ios_base::failure &) {
|
||||
addNotification(NotificationType::Critical, "An IO error occured when reading the METADATA_BLOCK_PICTURE struct.", context);
|
||||
throw Failure();
|
||||
} catch (const ConversionException &) {
|
||||
addNotification(NotificationType::Critical, "Base64 data from METADATA_BLOCK_PICTURE is invalid.", context);
|
||||
} catch(const TruncatedDataException &) {
|
||||
addNotification(NotificationType::Critical, "METADATA_BLOCK_PICTURE is truncated.", context);
|
||||
throw;
|
||||
} catch(const ConversionException &) {
|
||||
addNotification(NotificationType::Critical, "Base64 coding of METADATA_BLOCK_PICTURE is invalid.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
} else if(id().size() + 1 < size) {
|
||||
|
@ -97,14 +101,62 @@ void VorbisCommentField::parse(OggIterator &iterator)
|
|||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Parses a field using the specified \a iterator.
|
||||
*
|
||||
* The currentCharacterOffset() of the iterator is expected to be
|
||||
* at the beginning of the field to be parsed.
|
||||
*
|
||||
* \throws Throws std::ios_base::failure when an IO error occurs.
|
||||
* \throws Throws Media::Failure or a derived exception when a parsing
|
||||
* error occurs.
|
||||
*/
|
||||
void VorbisCommentField::parse(OggIterator &iterator)
|
||||
{
|
||||
uint64 maxSize = iterator.streamSize() - iterator.currentCharacterOffset();
|
||||
internalParse(iterator, maxSize);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Parses a field using the specified \a iterator.
|
||||
*
|
||||
* The currentCharacterOffset() of the iterator is expected to be
|
||||
* at the beginning of the field to be parsed.
|
||||
*
|
||||
* \throws Throws std::ios_base::failure when an IO error occurs.
|
||||
* \throws Throws Media::Failure or a derived exception when a parsing
|
||||
* error occurs.
|
||||
*/
|
||||
void VorbisCommentField::parse(OggIterator &iterator, uint64 &maxSize)
|
||||
{
|
||||
internalParse(iterator, maxSize);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Parses a field from the specified \a stream.
|
||||
*
|
||||
* The position of the current character in the input stream is expected to be
|
||||
* at the beginning of the field to be parsed.
|
||||
*
|
||||
* \throws Throws std::ios_base::failure when an IO error occurs.
|
||||
* \throws Throws Media::Failure or a derived exception when a parsing
|
||||
* error occurs.
|
||||
*/
|
||||
void VorbisCommentField::parse(istream &stream, uint64 &maxSize)
|
||||
{
|
||||
internalParse(stream, maxSize);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Writes the field to a stream using the specified \a writer.
|
||||
*
|
||||
* \throws Throws std::ios_base::failure when an IO error occurs.
|
||||
* \throws Throws Media::Failure or a derived exception when a making
|
||||
* error occurs.
|
||||
* \returns Returns whether the field has been written. (Some fields might be skipped
|
||||
* when specific \a flags are set.)
|
||||
*/
|
||||
void VorbisCommentField::make(BinaryWriter &writer)
|
||||
bool VorbisCommentField::make(BinaryWriter &writer, VorbisCommentFlags flags)
|
||||
{
|
||||
static const string context("making Vorbis comment field");
|
||||
if(id().empty()) {
|
||||
|
@ -114,6 +166,9 @@ void VorbisCommentField::make(BinaryWriter &writer)
|
|||
// try to convert value to string
|
||||
string valueString;
|
||||
if(id() == VorbisCommentIds::cover()) {
|
||||
if(flags & VorbisCommentFlags::NoCovers) {
|
||||
return false;
|
||||
}
|
||||
// make cover
|
||||
if(value().type() != TagDataType::Picture) {
|
||||
addNotification(NotificationType::Critical, "Assigned value of cover field is not picture data.", context);
|
||||
|
@ -143,10 +198,11 @@ void VorbisCommentField::make(BinaryWriter &writer)
|
|||
writer.writeString(id());
|
||||
writer.writeChar('=');
|
||||
writer.writeString(valueString);
|
||||
} catch(ConversionException &) {
|
||||
} catch(const ConversionException &) {
|
||||
addNotification(NotificationType::Critical, "Assigned value can not be converted appropriately.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,6 +11,27 @@ class BinaryWriter;
|
|||
|
||||
namespace Media {
|
||||
|
||||
/*!
|
||||
* \brief The VorbisCommentFlags enum specifies flags which controls parsing and making of Vorbis comments.
|
||||
*/
|
||||
enum class VorbisCommentFlags : byte
|
||||
{
|
||||
None = 0x0, /**< Regular parsing/making. */
|
||||
NoSignature = 0x1, /**< Skips the signature when parsing and making. */
|
||||
NoFramingByte = 0x2, /**< Doesn't expect the framing bit to be present when parsing; does not make the framing bit when making. */
|
||||
NoCovers = 0x4 /**< Skips all covers when making. */
|
||||
};
|
||||
|
||||
inline bool operator &(VorbisCommentFlags lhs, VorbisCommentFlags rhs)
|
||||
{
|
||||
return static_cast<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;
|
||||
|
||||
/*!
|
||||
|
@ -45,12 +66,18 @@ public:
|
|||
VorbisCommentField(const identifierType &id, const TagValue &value);
|
||||
|
||||
void parse(OggIterator &iterator);
|
||||
void make(IoUtilities::BinaryWriter &writer);
|
||||
void parse(OggIterator &iterator, uint64 &maxSize);
|
||||
void parse(std::istream &stream, uint64 &maxSize);
|
||||
bool make(IoUtilities::BinaryWriter &writer, VorbisCommentFlags flags = VorbisCommentFlags::None);
|
||||
bool isAdditionalTypeInfoUsed() const;
|
||||
bool supportsNestedFields() const;
|
||||
|
||||
protected:
|
||||
void cleared();
|
||||
|
||||
private:
|
||||
template<class StreamType>
|
||||
void internalParse(StreamType &stream, uint64 &maxSize);
|
||||
};
|
||||
|
||||
/*!
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#ifndef VORBISPACKAGETYPES_H
|
||||
#define VORBISPACKAGETYPES_H
|
||||
|
||||
#include <c++utilities/conversion/types.h>
|
||||
|
||||
namespace Media {
|
||||
|
||||
/*!
|
||||
|
|
Loading…
Reference in New Issue