#include "./flacstream.h" #include "./flacmetadata.h" #include "../vorbis/vorbiscomment.h" #include "../exceptions.h" #include "../mediafileinfo.h" #include "../mediaformat.h" #include "resources/config.h" #include #include using namespace std; using namespace IoUtilities; using namespace ConversionUtilities; using namespace ChronoUtilities; namespace TagParser { /*! * \class TagParser::FlacStream * \brief Implementation of TagParser::AbstractTrack for raw FLAC streams. */ /*! * \brief Constructs a new track for the specified \a mediaFileInfo at the specified \a startOffset. * * The stream of the \a mediaFileInfo instance is used as input stream. */ FlacStream::FlacStream(MediaFileInfo &mediaFileInfo, uint64 startOffset) : AbstractTrack(mediaFileInfo.stream(), startOffset) , m_mediaFileInfo(mediaFileInfo) , m_paddingSize(0) , m_streamOffset(0) { m_mediaType = MediaType::Audio; } /*! * \brief Creates a new Vorbis comment for the stream. * \remarks Just returns the current Vorbis comment if already present. */ VorbisComment *FlacStream::createVorbisComment() { if (!m_vorbisComment) { m_vorbisComment = make_unique(); } return m_vorbisComment.get(); } /*! * \brief Removes the assigned Vorbis comment if one is assigned; does nothing otherwise. * \returns Returns whether there were a Vorbis comment assigned. */ bool FlacStream::removeVorbisComment() { if (!m_vorbisComment) { return false; } m_vorbisComment.reset(); return true; } void FlacStream::internalParseHeader(Diagnostics &diag) { static const string context("parsing raw FLAC header"); if (!m_istream) { throw NoDataFoundException(); } m_istream->seekg(static_cast(m_startOffset), ios_base::beg); char buffer[0x22]; // check signature if (m_reader.readUInt32BE() != 0x664C6143) { diag.emplace_back(DiagLevel::Critical, "Signature (fLaC) not found.", context); throw InvalidDataException(); } m_format = GeneralMediaFormat::Flac; // parse meta data blocks for (FlacMetaDataBlockHeader header; !header.isLast();) { // parse block header m_istream->read(buffer, 4); header.parseHeader(buffer); // remember start offset const auto startOffset = m_istream->tellg(); // parse relevant meta data switch (static_cast(header.type())) { case FlacMetaDataBlockType::StreamInfo: if (header.dataSize() >= 0x22) { m_istream->read(buffer, 0x22); FlacMetaDataBlockStreamInfo streamInfo; streamInfo.parse(buffer); m_channelCount = streamInfo.channelCount(); m_samplingFrequency = streamInfo.samplingFrequency(); m_sampleCount = streamInfo.totalSampleCount(); m_bitsPerSample = streamInfo.bitsPerSample(); m_duration = TimeSpan::fromSeconds(static_cast(m_sampleCount) / m_samplingFrequency); } else { diag.emplace_back(DiagLevel::Critical, "\"METADATA_BLOCK_STREAMINFO\" is truncated and will be ignored.", context); } break; case FlacMetaDataBlockType::VorbisComment: // parse Vorbis comment // if more than one comment exist, simply thread those comments as one if (!m_vorbisComment) { m_vorbisComment = make_unique(); } try { m_vorbisComment->parse(*m_istream, header.dataSize(), VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, diag); } 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()); if (coverField.value().isEmpty()) { diag.emplace_back(DiagLevel::Warning, "\"METADATA_BLOCK_PICTURE\" contains no picture.", context); } else { // add the cover to the Vorbis comment if (!m_vorbisComment) { // create one if none exists yet m_vorbisComment = make_unique(); m_vorbisComment->setVendor(TagValue(APP_NAME " v" APP_VERSION, TagTextEncoding::Utf8)); } m_vorbisComment->fields().insert(make_pair(coverField.id(), move(coverField))); } } catch (const TruncatedDataException &) { diag.emplace_back(DiagLevel::Critical, "\"METADATA_BLOCK_PICTURE\" is truncated and will be ignored.", context); } break; case FlacMetaDataBlockType::Padding: m_paddingSize += 4 + header.dataSize(); break; default:; } // seek to next block m_istream->seekg(startOffset + static_cast(header.dataSize())); // TODO: check first FLAC frame } m_streamOffset = static_cast(m_istream->tellg()); } /*! * \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" within \a outputStream. */ std::streamoff FlacStream::makeHeader(ostream &outputStream, Diagnostics &diag) { istream &originalStream = m_mediaFileInfo.stream(); originalStream.seekg(static_cast(m_startOffset + 4)); CopyHelper<512> copy; // write signature BE::getBytes(static_cast(0x664C6143u), copy.buffer()); outputStream.write(copy.buffer(), 4); std::streamoff lastStartOffset = -1; // write meta data blocks which don't need to be adjusted FlacMetaDataBlockHeader header; FlacMetaDataBlockHeader lastActuallyWrittenHeader; do { // parse block header originalStream.read(copy.buffer(), 4); header.parseHeader(copy.buffer()); // skip/copy block switch (static_cast(header.type())) { case FlacMetaDataBlockType::VorbisComment: case FlacMetaDataBlockType::Picture: case FlacMetaDataBlockType::Padding: // skip separately written block originalStream.seekg(header.dataSize(), ios_base::cur); break; default: // copy block which doesn't need to be adjusted originalStream.seekg(-4, ios_base::cur); lastStartOffset = outputStream.tellp(); copy.copy(originalStream, outputStream, 4 + header.dataSize()); lastActuallyWrittenHeader = header; } } while (!header.isLast()); // adjust "isLast" flag if neccassary if (lastStartOffset >= 4 && ((!m_vorbisComment && !lastActuallyWrittenHeader.isLast()) || (m_vorbisComment && lastActuallyWrittenHeader.isLast()))) { outputStream.seekp(lastStartOffset); lastActuallyWrittenHeader.setLast(!m_vorbisComment); lastActuallyWrittenHeader.makeHeader(outputStream); originalStream.seekg(lastActuallyWrittenHeader.dataSize(), ios_base::cur); } // write Vorbis comment if (!m_vorbisComment) { return lastStartOffset >= 0 ? lastStartOffset : 0; } // 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, diag); // write "METADATA_BLOCK_HEADER" const auto endOffset = outputStream.tellp(); header.setType(FlacMetaDataBlockType::VorbisComment); auto dataSize(static_cast(endOffset) - static_cast(lastStartOffset) - 4); if (dataSize > 0xFFFFFF) { dataSize = 0xFFFFFF; diag.emplace_back(DiagLevel::Critical, "Vorbis Comment is too big and will be truncated.", "write Vorbis Comment to FLAC stream"); } header.setDataSize(static_cast(dataSize)); header.setLast(!m_vorbisComment->hasField(coverId)); outputStream.seekp(lastStartOffset); header.makeHeader(outputStream); outputStream.seekp(static_cast(dataSize), ios_base::cur); lastActuallyWrittenHeader = header; // write cover fields separately as "METADATA_BLOCK_PICTURE" if (header.isLast()) { return lastStartOffset; } header.setType(FlacMetaDataBlockType::Picture); const auto coverFields = m_vorbisComment->fields().equal_range(coverId); for (auto i = coverFields.first; i != coverFields.second;) { const auto lastCoverStartOffset = outputStream.tellp(); try { // write the structure 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); // update variables to handle the "isLast" flag lastStartOffset = lastCoverStartOffset; lastActuallyWrittenHeader = header; } catch (const Failure &) { // we can expect nothing is written in the error case except the FLAC header, so // -> just add an error message diag.emplace_back(DiagLevel::Critical, "Unable to serialize \"METADATA_BLOCK_PICTURE\" from assigned value.", "write \"METADATA_BLOCK_PICTURE\" to FLAC stream"); // -> and to recover, go back to where we were before outputStream.seekp(lastCoverStartOffset); } } // adjust "isLast" flag if neccassary if (!lastActuallyWrittenHeader.isLast()) { outputStream.seekp(lastStartOffset); lastActuallyWrittenHeader.setLast(true); lastActuallyWrittenHeader.makeHeader(outputStream); outputStream.seekp(lastActuallyWrittenHeader.dataSize()); } 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, Diagnostics &diag) { VAR_UNUSED(diag) // 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); } } } // namespace TagParser