From 19cceab211309b58f103608c613359801a9d3c45 Mon Sep 17 00:00:00 2001 From: "Stephen F. Booth" Date: Sat, 21 Oct 2023 07:29:50 +0200 Subject: [PATCH] DSDIFF support, add tests, fix formatting --- taglib/CMakeLists.txt | 12 +- taglib/dsdiff/dsdiffdiintag.cpp | 167 ++++++ taglib/dsdiff/dsdiffdiintag.h | 149 +++++ taglib/dsdiff/dsdifffile.cpp | 929 +++++++++++++++++++++++++++++ taglib/dsdiff/dsdifffile.h | 285 +++++++++ taglib/dsdiff/dsdiffproperties.cpp | 119 ++++ taglib/dsdiff/dsdiffproperties.h | 82 +++ taglib/fileref.cpp | 9 + tests/CMakeLists.txt | 2 + tests/data/empty10ms.dff | Bin 0 -> 7186 bytes tests/test_dsdiff.cpp | 204 +++++++ tests/test_fileref.cpp | 9 + tests/test_sizes.cpp | 4 + 13 files changed, 1970 insertions(+), 1 deletion(-) create mode 100644 taglib/dsdiff/dsdiffdiintag.cpp create mode 100644 taglib/dsdiff/dsdiffdiintag.h create mode 100644 taglib/dsdiff/dsdifffile.cpp create mode 100644 taglib/dsdiff/dsdifffile.h create mode 100644 taglib/dsdiff/dsdiffproperties.cpp create mode 100644 taglib/dsdiff/dsdiffproperties.h create mode 100644 tests/data/empty10ms.dff create mode 100644 tests/test_dsdiff.cpp diff --git a/taglib/CMakeLists.txt b/taglib/CMakeLists.txt index 9f4c2143..911ce058 100644 --- a/taglib/CMakeLists.txt +++ b/taglib/CMakeLists.txt @@ -25,6 +25,7 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/it ${CMAKE_CURRENT_SOURCE_DIR}/xm ${CMAKE_CURRENT_SOURCE_DIR}/dsf + ${CMAKE_CURRENT_SOURCE_DIR}/dsdiff ) set(tag_HDRS @@ -137,6 +138,9 @@ set(tag_HDRS xm/xmproperties.h dsf/dsffile.h dsf/dsfproperties.h + dsdiff/dsdifffile.h + dsdiff/dsdiffproperties.h + dsdiff/dsdiffdiintag.h ) set(mpeg_SRCS @@ -297,6 +301,12 @@ set(dsf_SRCS dsf/dsfproperties.cpp ) +set(dsdiff_SRCS + dsdiff/dsdifffile.cpp + dsdiff/dsdiffproperties.cpp + dsdiff/dsdiffdiintag.cpp +) + set(toolkit_SRCS toolkit/tstring.cpp toolkit/tstringlist.cpp @@ -320,7 +330,7 @@ set(tag_LIB_SRCS ${vorbis_SRCS} ${oggflacs_SRCS} ${mpc_SRCS} ${ape_SRCS} ${toolkit_SRCS} ${flacs_SRCS} ${wavpack_SRCS} ${speex_SRCS} ${trueaudio_SRCS} ${riff_SRCS} ${aiff_SRCS} ${wav_SRCS} ${asf_SRCS} ${mp4_SRCS} ${mod_SRCS} ${s3m_SRCS} ${it_SRCS} ${xm_SRCS} ${opus_SRCS} - ${dsf_SRCS} + ${dsf_SRCS} ${dsdiff_SRCS} tag.cpp tagunion.cpp fileref.cpp diff --git a/taglib/dsdiff/dsdiffdiintag.cpp b/taglib/dsdiff/dsdiffdiintag.cpp new file mode 100644 index 00000000..c67b7a31 --- /dev/null +++ b/taglib/dsdiff/dsdiffdiintag.cpp @@ -0,0 +1,167 @@ +/*************************************************************************** + copyright : (C) 2016 by Damien Plisson, Audirvana + email : damien78@audirvana.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "dsdiffdiintag.h" +#include "tdebug.h" +#include "tstringlist.h" +#include "tpropertymap.h" + +using namespace TagLib; +using namespace DSDIFF::DIIN; + +class DSDIFF::DIIN::Tag::TagPrivate +{ +public: + TagPrivate() + { + } + + String title; + String artist; +}; + +DSDIFF::DIIN::Tag::Tag() : + d(new TagPrivate()) +{ +} + +DSDIFF::DIIN::Tag::~Tag() +{ + delete d; +} + +String DSDIFF::DIIN::Tag::title() const +{ + return d->title; +} + +String DSDIFF::DIIN::Tag::artist() const +{ + return d->artist; +} + +String DSDIFF::DIIN::Tag::album() const +{ + return String(); +} + +String DSDIFF::DIIN::Tag::comment() const +{ + return String(); +} + +String DSDIFF::DIIN::Tag::genre() const +{ + return String(); +} + +unsigned int DSDIFF::DIIN::Tag::year() const +{ + return 0; +} + +unsigned int DSDIFF::DIIN::Tag::track() const +{ + return 0; +} + +void DSDIFF::DIIN::Tag::setTitle(const String &title) +{ + if(title.isEmpty()) + d->title = String(); + else + d->title = title; +} + +void DSDIFF::DIIN::Tag::setArtist(const String &artist) +{ + if(artist.isEmpty()) + d->artist = String(); + else + d->artist = artist; +} + +void DSDIFF::DIIN::Tag::setAlbum(const String &) +{ + debug("DSDIFF::DIIN::Tag::setAlbum() -- Ignoring unsupported tag."); +} + +void DSDIFF::DIIN::Tag::setComment(const String &) +{ + debug("DSDIFF::DIIN::Tag::setComment() -- Ignoring unsupported tag."); +} + +void DSDIFF::DIIN::Tag::setGenre(const String &) +{ + debug("DSDIFF::DIIN::Tag::setGenre() -- Ignoring unsupported tag."); +} + +void DSDIFF::DIIN::Tag::setYear(unsigned int) +{ + debug("DSDIFF::DIIN::Tag::setYear() -- Ignoring unsupported tag."); +} + +void DSDIFF::DIIN::Tag::setTrack(unsigned int) +{ + debug("DSDIFF::DIIN::Tag::setTrack() -- Ignoring unsupported tag."); +} + +PropertyMap DSDIFF::DIIN::Tag::properties() const +{ + PropertyMap properties; + properties["TITLE"] = d->title; + properties["ARTIST"] = d->artist; + return properties; +} + +PropertyMap DSDIFF::DIIN::Tag::setProperties(const PropertyMap &origProps) +{ + PropertyMap properties(origProps); + properties.removeEmpty(); + StringList oneValueSet; + + if(properties.contains("TITLE")) { + d->title = properties["TITLE"].front(); + oneValueSet.append("TITLE"); + } else + d->title = String(); + + if(properties.contains("ARTIST")) { + d->artist = properties["ARTIST"].front(); + oneValueSet.append("ARTIST"); + } else + d->artist = String(); + + // for each tag that has been set above, remove the first entry in the corresponding + // value list. The others will be returned as unsupported by this format. + for(StringList::Iterator it = oneValueSet.begin(); it != oneValueSet.end(); ++it) { + if(properties[*it].size() == 1) + properties.erase(*it); + else + properties[*it].erase(properties[*it].begin()); + } + + return properties; +} diff --git a/taglib/dsdiff/dsdiffdiintag.h b/taglib/dsdiff/dsdiffdiintag.h new file mode 100644 index 00000000..62de949b --- /dev/null +++ b/taglib/dsdiff/dsdiffdiintag.h @@ -0,0 +1,149 @@ +/*************************************************************************** + copyright : (C) 2016 by Damien Plisson, Audirvana + email : damien78@audirvana.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_DSDIFFDIINTAG_H +#define TAGLIB_DSDIFFDIINTAG_H + +#include "tag.h" + +namespace TagLib { + + namespace DSDIFF { + + namespace DIIN { + + /*! + * Tags from the Edited Master Chunk Info + * + * Only Title and Artist tags are supported + */ + class TAGLIB_EXPORT Tag : public TagLib::Tag + { + public: + Tag(); + ~Tag() override; + + /*! + * Returns the track name; if no track name is present in the tag + * String() will be returned. + */ + String title() const override; + + /*! + * Returns the artist name; if no artist name is present in the tag + * String() will be returned. + */ + String artist() const override; + + /*! + * Not supported. Therefore always returns String(). + */ + String album() const override; + + /*! + * Not supported. Therefore always returns String(). + */ + String comment() const override; + + /*! + * Not supported. Therefore always returns String(). + */ + String genre() const override; + + /*! + * Not supported. Therefore always returns 0. + */ + unsigned int year() const override; + + /*! + * Not supported. Therefore always returns 0. + */ + unsigned int track() const override; + + /*! + * Sets the title to \a title. If \a title is String() then this + * value will be cleared. + */ + void setTitle(const String &title) override; + + /*! + * Sets the artist to \a artist. If \a artist is String() then this + * value will be cleared. + */ + void setArtist(const String &artist) override; + + /*! + * Not supported and therefore ignored. + */ + void setAlbum(const String &album) override; + + /*! + * Not supported and therefore ignored. + */ + void setComment(const String &comment) override; + + /*! + * Not supported and therefore ignored. + */ + void setGenre(const String &genre) override; + + /*! + * Not supported and therefore ignored. + */ + void setYear(unsigned int year) override; + + /*! + * Not supported and therefore ignored. + */ + void setTrack(unsigned int track) override; + + /*! + * Implements the unified property interface -- export function. + * Since the DIIN tag is very limited, the exported map is as well. + */ + PropertyMap properties() const override; + + /*! + * Implements the unified property interface -- import function. + * Because of the limitations of the DIIN file tag, any tags besides + * TITLE and ARTIST, will be + * returned. Additionally, if the map contains tags with multiple values, + * all but the first will be contained in the returned map of unsupported + * properties. + */ + PropertyMap setProperties(const PropertyMap &) override; + + private: + Tag(const Tag &) = delete; + Tag &operator=(const Tag &) = delete; + + class TagPrivate; + TagPrivate *d; + }; + } + } +} + +#endif diff --git a/taglib/dsdiff/dsdifffile.cpp b/taglib/dsdiff/dsdifffile.cpp new file mode 100644 index 00000000..0d75ac8d --- /dev/null +++ b/taglib/dsdiff/dsdifffile.cpp @@ -0,0 +1,929 @@ +/*************************************************************************** + copyright : (C) 2016 by Damien Plisson, Audirvana + email : damien78@audirvana.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "tbytevector.h" +#include "tdebug.h" +#include "id3v2tag.h" +#include "tstringlist.h" +#include "tpropertymap.h" +#include "tagutils.h" + +#include "tagunion.h" +#include "dsdifffile.h" + +#include + +using namespace TagLib; + +namespace +{ + struct Chunk64 + { + ByteVector name; + unsigned long long offset; + unsigned long long size; + char padding; + }; + + typedef std::vector ChunkList; + + int chunkIndex(const ChunkList &chunks, const ByteVector &id) + { + for(int i = 0; i < chunks.size(); i++) { + if(chunks[i].name == id) + return i; + } + + return -1; + } + + bool isValidChunkID(const ByteVector &name) + { + if(name.size() != 4) + return false; + + for(int i = 0; i < 4; i++) { + if(name[i] < 32 || name[i] > 127) + return false; + } + + return true; + } + + enum { + ID3v2Index = 0, + DIINIndex = 1 + }; + enum { + PROPChunk = 0, + DIINChunk = 1 + }; +} + +class DSDIFF::File::FilePrivate +{ +public: + FilePrivate() : + endianness(BigEndian), + size(0), + isID3InPropChunk(false), + duplicateID3V2chunkIndex(-1), + properties(0), + id3v2TagChunkID("ID3 "), + hasID3v2(false), + hasDiin(false) + { + childChunkIndex[ID3v2Index] = -1; + childChunkIndex[DIINIndex] = -1; + } + + ~FilePrivate() + { + delete properties; + } + + Endianness endianness; + ByteVector type; + unsigned long long size; + ByteVector format; + ChunkList chunks; + std::array childChunks; + std::array childChunkIndex; + /* + * Two possibilities can be found: ID3V2 chunk inside PROP chunk or at root level + */ + bool isID3InPropChunk; + /* + * ID3 chunks are present. This is then the index of the one in PROP chunk that + * will be removed upon next save to remove duplicates. + */ + int duplicateID3V2chunkIndex; + + Properties *properties; + + TagUnion tag; + + ByteVector id3v2TagChunkID; + + bool hasID3v2; + bool hasDiin; +}; + +//////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool DSDIFF::File::isSupported(IOStream *stream) +{ + // A DSDIFF file has to start with "FRM8????????DSD ". + + const ByteVector id = Utils::readHeader(stream, 16, false); + return (id.startsWith("FRM8") && id.containsAt("DSD ", 12)); +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +DSDIFF::File::File(FileName file, bool readProperties, + Properties::ReadStyle propertiesStyle) : + TagLib::File(file), + d(new FilePrivate()) +{ + d->endianness = BigEndian; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +DSDIFF::File::File(IOStream *stream, bool readProperties, + Properties::ReadStyle propertiesStyle) : + TagLib::File(stream), + d(new FilePrivate()) +{ + d->endianness = BigEndian; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +DSDIFF::File::~File() +{ + delete d; +} + +TagLib::Tag *DSDIFF::File::tag() const +{ + return &d->tag; +} + +ID3v2::Tag *DSDIFF::File::ID3v2Tag(bool create) const +{ + return d->tag.access(ID3v2Index, create); +} + +bool DSDIFF::File::hasID3v2Tag() const +{ + return d->hasID3v2; +} + +DSDIFF::DIIN::Tag *DSDIFF::File::DIINTag(bool create) const +{ + return d->tag.access(DIINIndex, create); +} + +bool DSDIFF::File::hasDIINTag() const +{ + return d->hasDiin; +} + +PropertyMap DSDIFF::File::properties() const +{ + if(d->hasID3v2) + return d->tag.access(ID3v2Index, false)->properties(); + + if(d->hasDiin) + return d->tag.access(DIINIndex, false)->properties(); + + return PropertyMap(); +} + +void DSDIFF::File::removeUnsupportedProperties(const StringList &unsupported) +{ + if(d->hasID3v2) + d->tag.access(ID3v2Index, false)->removeUnsupportedProperties(unsupported); + + if(d->hasDiin) + d->tag.access(DIINIndex, false)->removeUnsupportedProperties(unsupported); +} + +PropertyMap DSDIFF::File::setProperties(const PropertyMap &properties) +{ + return d->tag.access(ID3v2Index, true)->setProperties(properties); +} + +DSDIFF::Properties *DSDIFF::File::audioProperties() const +{ + return d->properties; +} + +bool DSDIFF::File::save() +{ + return save(AllTags); +} + +bool DSDIFF::File::save(int tags, StripTags strip, ID3v2::Version version) +{ + if(readOnly()) { + debug("DSDIFF::File::save() -- File is read only."); + return false; + } + + if(!isValid()) { + debug("DSDIFF::File::save() -- Trying to save invalid file."); + return false; + } + + // TODO: Should duplicate be supported? + + if(strip == StripOthers) + File::strip(~tags); + + // First: save ID3V2 chunk + + ID3v2::Tag *id3v2Tag = d->tag.access(ID3v2Index, false); + + if(tags & ID3v2 && id3v2Tag) { + if(d->isID3InPropChunk) { + if(!id3v2Tag->isEmpty()) { + setChildChunkData(d->id3v2TagChunkID, id3v2Tag->render(version), PROPChunk); + d->hasID3v2 = true; + } + else { + // Empty tag: remove it + setChildChunkData(d->id3v2TagChunkID, ByteVector(), PROPChunk); + d->hasID3v2 = false; + } + } + else { + if(!id3v2Tag->isEmpty()) { + setRootChunkData(d->id3v2TagChunkID, id3v2Tag->render(version)); + d->hasID3v2 = true; + } + else { + // Empty tag: remove it + setRootChunkData(d->id3v2TagChunkID, ByteVector()); + d->hasID3v2 = false; + } + } + } + + // Second: save the DIIN chunk + + DSDIFF::DIIN::Tag *diinTag = d->tag.access(DIINIndex, false); + + if(tags & DIIN && diinTag) { + if(!diinTag->title().isEmpty()) { + ByteVector diinTitle; + diinTitle.append(ByteVector::fromUInt(diinTag->title().size(), d->endianness == BigEndian)); + diinTitle.append(ByteVector::fromCString(diinTag->title().toCString())); + setChildChunkData("DITI", diinTitle, DIINChunk); + } + else + setChildChunkData("DITI", ByteVector(), DIINChunk); + + if(!diinTag->artist().isEmpty()) { + ByteVector diinArtist; + diinArtist.append(ByteVector::fromUInt(diinTag->artist().size(), d->endianness == BigEndian)); + diinArtist.append(ByteVector::fromCString(diinTag->artist().toCString())); + setChildChunkData("DIAR", diinArtist, DIINChunk); + } + else + setChildChunkData("DIAR", ByteVector(), DIINChunk); + } + + // Third: remove the duplicate ID3V2 chunk (inside PROP chunk) if any + if(d->duplicateID3V2chunkIndex>=0) { + setChildChunkData(d->duplicateID3V2chunkIndex, ByteVector(), PROPChunk); + d->duplicateID3V2chunkIndex = -1; + } + + return true; +} + +void DSDIFF::File::strip(int tags) +{ + if(tags & ID3v2) { + removeRootChunk("ID3 "); + removeRootChunk("id3 "); + d->hasID3v2 = false; + d->tag.set(ID3v2Index, new ID3v2::Tag); + + /// TODO: needs to also account for ID3v2 tags under the PROP chunk + } + if(tags & DIIN) { + removeRootChunk("DITI"); + removeRootChunk("DIAR"); + d->hasDiin = false; + d->tag.set(DIINIndex, new DIIN::Tag); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void DSDIFF::File::removeRootChunk(unsigned int i) +{ + unsigned long long chunkSize = d->chunks[i].size + d->chunks[i].padding + 12; + + d->size -= chunkSize; + insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8); + + removeBlock(d->chunks[i].offset - 12, chunkSize); + + // Update the internal offsets + + for(unsigned long r = i + 1; r < d->chunks.size(); r++) + d->chunks[r].offset = d->chunks[r - 1].offset + 12 + + d->chunks[r - 1].size + d->chunks[r - 1].padding; + + d->chunks.erase(d->chunks.begin() + i); +} + +void DSDIFF::File::removeRootChunk(const ByteVector &id) +{ + int i = chunkIndex(d->chunks, id); + + if(i >= 0) + removeRootChunk(i); +} + +void DSDIFF::File::setRootChunkData(unsigned int i, const ByteVector &data) +{ + if(data.isEmpty()) { + removeRootChunk(i); + return; + } + + // Non null data: update chunk + // First we update the global size + + d->size += ((data.size() + 1) & ~1) - (d->chunks[i].size + d->chunks[i].padding); + insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8); + + // Now update the specific chunk + + writeChunk(d->chunks[i].name, + data, + d->chunks[i].offset - 12, + d->chunks[i].size + d->chunks[i].padding + 12); + + d->chunks[i].size = data.size(); + d->chunks[i].padding = (data.size() & 0x01) ? 1 : 0; + + // Finally update the internal offsets + + updateRootChunksStructure(i + 1); +} + +void DSDIFF::File::setRootChunkData(const ByteVector &name, const ByteVector &data) +{ + if(d->chunks.size() == 0) { + debug("DSDIFF::File::setRootChunkData('" + name + "') - No valid chunks found."); + return; + } + + int i = chunkIndex(d->chunks, name); + + if(i >= 0) { + setRootChunkData(i, data); + return; + } + + // Couldn't find an existing chunk, so let's create a new one. + i = d->chunks.size() - 1; + unsigned long offset = d->chunks[i].offset + d->chunks[i].size + d->chunks[i].padding; + + // First we update the global size + d->size += (offset & 1) + ((data.size() + 1) & ~1) + 12; + insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8); + + // Now add the chunk to the file + writeChunk(name, + data, + offset, + std::max(0, length() - offset), + (offset & 1) ? 1 : 0); + + Chunk64 chunk; + chunk.name = name; + chunk.size = data.size(); + chunk.offset = offset + 12; + chunk.padding = (data.size() & 0x01) ? 1 : 0; + + d->chunks.push_back(chunk); +} + +void DSDIFF::File::removeChildChunk(unsigned int i, unsigned int childChunkNum) +{ + ChunkList &childChunks = d->childChunks[childChunkNum]; + + // Update global size + + unsigned long long removedChunkTotalSize = childChunks[i].size + childChunks[i].padding + 12; + d->size -= removedChunkTotalSize; + insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8); + + // Update child chunk size + + d->chunks[d->childChunkIndex[childChunkNum]].size -= removedChunkTotalSize; + insert(ByteVector::fromLongLong(d->chunks[d->childChunkIndex[childChunkNum]].size, + d->endianness == BigEndian), + d->chunks[d->childChunkIndex[childChunkNum]].offset - 8, 8); + // Remove the chunk + + removeBlock(childChunks[i].offset - 12, removedChunkTotalSize); + + // Update the internal offsets + // For child chunks + + if((i + 1) < childChunks.size()) { + childChunks[i + 1].offset = childChunks[i].offset; + i++; + for(i++; i < childChunks.size(); i++) + childChunks[i].offset = childChunks[i - 1].offset + 12 + + childChunks[i - 1].size + childChunks[i - 1].padding; + } + + // And for root chunks + + for(i = d->childChunkIndex[childChunkNum] + 1; i < d->chunks.size(); i++) + d->chunks[i].offset = d->chunks[i - 1].offset + 12 + + d->chunks[i - 1].size + d->chunks[i - 1].padding; + + childChunks.erase(childChunks.begin() + i); +} + +void DSDIFF::File::setChildChunkData(unsigned int i, + const ByteVector &data, + unsigned int childChunkNum) +{ + ChunkList &childChunks = d->childChunks[childChunkNum]; + + if(data.isEmpty()) { + removeChildChunk(i, childChunkNum); + return; + } + + // Non null data: update chunk + // First we update the global size + + d->size += ((data.size() + 1) & ~1) - (childChunks[i].size + childChunks[i].padding); + + insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8); + + // And the PROP chunk size + + d->chunks[d->childChunkIndex[childChunkNum]].size += + ((data.size() + 1) & ~1) - (childChunks[i].size + childChunks[i].padding); + insert(ByteVector::fromLongLong(d->chunks[d->childChunkIndex[childChunkNum]].size, + d->endianness == BigEndian), + d->chunks[d->childChunkIndex[childChunkNum]].offset - 8, 8); + + // Now update the specific chunk + + writeChunk(childChunks[i].name, + data, + childChunks[i].offset - 12, + childChunks[i].size + childChunks[i].padding + 12); + + childChunks[i].size = data.size(); + childChunks[i].padding = (data.size() & 0x01) ? 1 : 0; + + // Now update the internal offsets + // For child Chunks + for(i++; i < childChunks.size(); i++) + childChunks[i].offset = childChunks[i - 1].offset + 12 + + childChunks[i - 1].size + childChunks[i - 1].padding; + + // And for root chunks + updateRootChunksStructure(d->childChunkIndex[childChunkNum] + 1); +} + +void DSDIFF::File::setChildChunkData(const ByteVector &name, + const ByteVector &data, + unsigned int childChunkNum) +{ + ChunkList &childChunks = d->childChunks[childChunkNum]; + + if(childChunks.size() == 0) { + debug("DSDIFF::File::setChildChunkData - No valid chunks found."); + return; + } + + for(unsigned int i = 0; i < childChunks.size(); i++) { + if(childChunks[i].name == name) { + setChildChunkData(i, data, childChunkNum); + return; + } + } + + // Do not attempt to remove a non existing chunk + + if(data.isEmpty()) + return; + + // Couldn't find an existing chunk, so let's create a new one. + + unsigned int i = childChunks.size() - 1; + unsigned long offset = childChunks[i].offset + childChunks[i].size + childChunks[i].padding; + + // First we update the global size + + d->size += (offset & 1) + ((data.size() + 1) & ~1) + 12; + insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8); + + // And the child chunk size + + d->chunks[d->childChunkIndex[childChunkNum]].size += (offset & 1) + + ((data.size() + 1) & ~1) + 12; + insert(ByteVector::fromLongLong(d->chunks[d->childChunkIndex[childChunkNum]].size, + d->endianness == BigEndian), + d->chunks[d->childChunkIndex[childChunkNum]].offset - 8, 8); + + // Now add the chunk to the file + + unsigned long long nextRootChunkIdx = length(); + if((d->childChunkIndex[childChunkNum] + 1) < static_cast(d->chunks.size())) + nextRootChunkIdx = d->chunks[d->childChunkIndex[childChunkNum] + 1].offset - 12; + + writeChunk(name, data, offset, + std::max(0, nextRootChunkIdx - offset), + (offset & 1) ? 1 : 0); + + // For root chunks + + updateRootChunksStructure(d->childChunkIndex[childChunkNum] + 1); + + Chunk64 chunk; + chunk.name = name; + chunk.size = data.size(); + chunk.offset = offset + 12; + chunk.padding = (data.size() & 0x01) ? 1 : 0; + + childChunks.push_back(chunk); +} + +void DSDIFF::File::updateRootChunksStructure(unsigned int startingChunk) +{ + for(unsigned int i = startingChunk; i < d->chunks.size(); i++) + d->chunks[i].offset = d->chunks[i - 1].offset + 12 + + d->chunks[i - 1].size + d->chunks[i - 1].padding; + + // Update childchunks structure as well + + if(d->childChunkIndex[PROPChunk] >= static_cast(startingChunk)) { + ChunkList &childChunksToUpdate = d->childChunks[PROPChunk]; + if(childChunksToUpdate.size() > 0) { + childChunksToUpdate[0].offset = d->chunks[d->childChunkIndex[PROPChunk]].offset + 12; + for(unsigned int i = 1; i < childChunksToUpdate.size(); i++) + childChunksToUpdate[i].offset = childChunksToUpdate[i - 1].offset + 12 + + childChunksToUpdate[i - 1].size + childChunksToUpdate[i - 1].padding; + } + + } + if(d->childChunkIndex[DIINChunk] >= static_cast(startingChunk)) { + ChunkList &childChunksToUpdate = d->childChunks[DIINChunk]; + if(childChunksToUpdate.size() > 0) { + childChunksToUpdate[0].offset = d->chunks[d->childChunkIndex[DIINChunk]].offset + 12; + for(unsigned int i = 1; i < childChunksToUpdate.size(); i++) + childChunksToUpdate[i].offset = childChunksToUpdate[i - 1].offset + 12 + + childChunksToUpdate[i - 1].size + childChunksToUpdate[i - 1].padding; + } + } +} + +void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesStyle) +{ + bool bigEndian = (d->endianness == BigEndian); + + d->type = readBlock(4); + d->size = readBlock(8).toLongLong(bigEndian); + d->format = readBlock(4); + + // + 12: chunk header at least, fix for additional junk bytes + + while(tell() + 12 <= length()) { + ByteVector chunkName = readBlock(4); + unsigned long long chunkSize = readBlock(8).toLongLong(bigEndian); + + if(!isValidChunkID(chunkName)) { + debug("DSDIFF::File::read() -- Chunk '" + chunkName + "' has invalid ID"); + setValid(false); + break; + } + + if(static_cast(tell()) + chunkSize > + static_cast(length())) { + debug("DSDIFF::File::read() -- Chunk '" + chunkName + + "' has invalid size (larger than the file size)"); + setValid(false); + break; + } + + Chunk64 chunk; + chunk.name = chunkName; + chunk.size = chunkSize; + chunk.offset = tell(); + + seek(chunk.size, Current); + + // Check padding + + chunk.padding = 0; + long uPosNotPadded = tell(); + if((uPosNotPadded & 0x01) != 0) { + ByteVector iByte = readBlock(1); + if((iByte.size() != 1) || (iByte[0] != 0)) + // Not well formed, re-seek + seek(uPosNotPadded, Beginning); + else + chunk.padding = 1; + } + d->chunks.push_back(chunk); + } + + // For DSD uncompressed + unsigned long long lengthDSDSamplesTimeChannels = 0; + // For computing bitrate + unsigned long long audioDataSizeinBytes = 0; + // For DST compressed frames + unsigned long dstNumFrames = 0; + // For DST compressed frames + unsigned short dstFrameRate = 0; + + for(unsigned int i = 0; i < d->chunks.size(); i++) { + if(d->chunks[i].name == "DSD ") { + lengthDSDSamplesTimeChannels = d->chunks[i].size * 8; + audioDataSizeinBytes = d->chunks[i].size; + } + else if(d->chunks[i].name == "DST ") { + // Now decode the chunks inside the DST chunk to read the DST Frame Information one + long long dstChunkEnd = d->chunks[i].offset + d->chunks[i].size; + seek(d->chunks[i].offset); + + audioDataSizeinBytes = d->chunks[i].size; + + while(tell() + 12 <= dstChunkEnd) { + ByteVector dstChunkName = readBlock(4); + long long dstChunkSize = readBlock(8).toLongLong(bigEndian); + + if(!isValidChunkID(dstChunkName)) { + debug("DSDIFF::File::read() -- DST Chunk '" + dstChunkName + "' has invalid ID"); + setValid(false); + break; + } + + if(static_cast(tell()) + dstChunkSize > dstChunkEnd) { + debug("DSDIFF::File::read() -- DST Chunk '" + dstChunkName + + "' has invalid size (larger than the DST chunk)"); + setValid(false); + break; + } + + if(dstChunkName == "FRTE") { + // Found the DST frame information chunk + dstNumFrames = readBlock(4).toUInt(bigEndian); + dstFrameRate = readBlock(2).toUShort(bigEndian); + // Found the wanted one, no need to look at the others + break; + } + + seek(dstChunkSize, Current); + + // Check padding + long uPosNotPadded = tell(); + if((uPosNotPadded & 0x01) != 0) { + ByteVector iByte = readBlock(1); + if((iByte.size() != 1) || (iByte[0] != 0)) + // Not well formed, re-seek + seek(uPosNotPadded, Beginning); + } + } + } + else if(d->chunks[i].name == "PROP") { + d->childChunkIndex[PROPChunk] = i; + // Now decodes the chunks inside the PROP chunk + long long propChunkEnd = d->chunks[i].offset + d->chunks[i].size; + // +4 to remove the 'SND ' marker at beginning of 'PROP' chunk + seek(d->chunks[i].offset + 4); + while(tell() + 12 <= propChunkEnd) { + ByteVector propChunkName = readBlock(4); + long long propChunkSize = readBlock(8).toLongLong(bigEndian); + + if(!isValidChunkID(propChunkName)) { + debug("DSDIFF::File::read() -- PROP Chunk '" + propChunkName + "' has invalid ID"); + setValid(false); + break; + } + + if(static_cast(tell()) + propChunkSize > propChunkEnd) { + debug("DSDIFF::File::read() -- PROP Chunk '" + propChunkName + + "' has invalid size (larger than the PROP chunk)"); + setValid(false); + break; + } + + Chunk64 chunk; + chunk.name = propChunkName; + chunk.size = propChunkSize; + chunk.offset = tell(); + + seek(chunk.size, Current); + + // Check padding + chunk.padding = 0; + long uPosNotPadded = tell(); + if((uPosNotPadded & 0x01) != 0) { + ByteVector iByte = readBlock(1); + if((iByte.size() != 1) || (iByte[0] != 0)) + // Not well formed, re-seek + seek(uPosNotPadded, Beginning); + else + chunk.padding = 1; + } + d->childChunks[PROPChunk].push_back(chunk); + } + } + else if(d->chunks[i].name == "DIIN") { + d->childChunkIndex[DIINChunk] = i; + d->hasDiin = true; + + // Now decode the chunks inside the DIIN chunk + + long long diinChunkEnd = d->chunks[i].offset + d->chunks[i].size; + seek(d->chunks[i].offset); + + while(tell() + 12 <= diinChunkEnd) { + ByteVector diinChunkName = readBlock(4); + long long diinChunkSize = readBlock(8).toLongLong(bigEndian); + + if(!isValidChunkID(diinChunkName)) { + debug("DSDIFF::File::read() -- DIIN Chunk '" + diinChunkName + "' has invalid ID"); + setValid(false); + break; + } + + if(static_cast(tell()) + diinChunkSize > diinChunkEnd) { + debug("DSDIFF::File::read() -- DIIN Chunk '" + diinChunkName + + "' has invalid size (larger than the DIIN chunk)"); + setValid(false); + break; + } + + Chunk64 chunk; + chunk.name = diinChunkName; + chunk.size = diinChunkSize; + chunk.offset = tell(); + + seek(chunk.size, Current); + + // Check padding + + chunk.padding = 0; + long uPosNotPadded = tell(); + + if((uPosNotPadded & 0x01) != 0) { + ByteVector iByte = readBlock(1); + if((iByte.size() != 1) || (iByte[0] != 0)) + // Not well formed, re-seek + seek(uPosNotPadded, Beginning); + else + chunk.padding = 1; + } + d->childChunks[DIINChunk].push_back(chunk); + } + } + else if(d->chunks[i].name == "ID3 " || d->chunks[i].name == "id3 ") { + d->id3v2TagChunkID = d->chunks[i].name; + d->tag.set(ID3v2Index, new ID3v2::Tag(this, d->chunks[i].offset)); + d->isID3InPropChunk = false; + d->hasID3v2 = true; + } + } + + if(!isValid()) + return; + + if(d->childChunkIndex[PROPChunk] < 0) { + debug("DSDIFF::File::read() -- no PROP chunk found"); + setValid(false); + return; + } + + // Read properties + + unsigned int sampleRate=0; + unsigned short channels=0; + + for(unsigned int i = 0; i < d->childChunks[PROPChunk].size(); i++) { + if(d->childChunks[PROPChunk][i].name == "ID3 " || + d->childChunks[PROPChunk][i].name == "id3 ") { + if(d->hasID3v2) { + d->duplicateID3V2chunkIndex = i; + // ID3V2 tag has already been found at root level + continue; + } + d->id3v2TagChunkID = d->childChunks[PROPChunk][i].name; + d->tag.set(ID3v2Index, new ID3v2::Tag(this, d->childChunks[PROPChunk][i].offset)); + d->isID3InPropChunk = true; + d->hasID3v2 = true; + } + else if(d->childChunks[PROPChunk][i].name == "FS ") { + // Sample rate + seek(d->childChunks[PROPChunk][i].offset); + sampleRate = readBlock(4).toUInt(0, 4, bigEndian); + } + else if(d->childChunks[PROPChunk][i].name == "CHNL") { + // Channels + seek(d->childChunks[PROPChunk][i].offset); + channels = readBlock(2).toShort(0, bigEndian); + } + } + + // Read title & artist from DIIN chunk + + d->tag.access(DIINIndex, true); + + if(d->hasDiin) { + for(unsigned int i = 0; i < d->childChunks[DIINChunk].size(); i++) { + if(d->childChunks[DIINChunk][i].name == "DITI") { + seek(d->childChunks[DIINChunk][i].offset); + unsigned int titleStrLength = readBlock(4).toUInt(0, 4, bigEndian); + if(titleStrLength <= d->childChunks[DIINChunk][i].size) { + ByteVector titleStr = readBlock(titleStrLength); + d->tag.access(DIINIndex, false)->setTitle(titleStr); + } + } + else if(d->childChunks[DIINChunk][i].name == "DIAR") { + seek(d->childChunks[DIINChunk][i].offset); + unsigned int artistStrLength = readBlock(4).toUInt(0, 4, bigEndian); + if(artistStrLength <= d->childChunks[DIINChunk][i].size) { + ByteVector artistStr = readBlock(artistStrLength); + d->tag.access(DIINIndex, false)->setArtist(artistStr); + } + } + } + } + + if(readProperties) { + if(lengthDSDSamplesTimeChannels == 0) { + // DST compressed signal : need to compute length of DSD uncompressed frames + if(dstFrameRate > 0) + lengthDSDSamplesTimeChannels = (unsigned long long) dstNumFrames * + (unsigned long long) sampleRate / + (unsigned long long) dstFrameRate; + else + lengthDSDSamplesTimeChannels = 0; + } + else { + // In DSD uncompressed files, the read number of samples is the total for each channel + if(channels > 0) + lengthDSDSamplesTimeChannels /= channels; + } + int bitrate = 0; + if(lengthDSDSamplesTimeChannels > 0) + bitrate = (audioDataSizeinBytes * 8 * sampleRate) / lengthDSDSamplesTimeChannels / 1000; + + d->properties = new Properties(sampleRate, + channels, + lengthDSDSamplesTimeChannels, + bitrate, + propertiesStyle); + } + + if(!ID3v2Tag()) { + d->tag.access(ID3v2Index, true); + // By default, ID3 chunk is at root level + d->isID3InPropChunk = false; + d->hasID3v2 = false; + } +} + +void DSDIFF::File::writeChunk(const ByteVector &name, const ByteVector &data, + unsigned long long offset, unsigned long replace, + unsigned int leadingPadding) +{ + ByteVector combined; + if(leadingPadding) + combined.append(ByteVector(leadingPadding, '\x00')); + + combined.append(name); + combined.append(ByteVector::fromLongLong(data.size(), d->endianness == BigEndian)); + combined.append(data); + if((data.size() & 0x01) != 0) + combined.append('\x00'); + + insert(combined, offset, replace); +} diff --git a/taglib/dsdiff/dsdifffile.h b/taglib/dsdiff/dsdifffile.h new file mode 100644 index 00000000..d1cb8248 --- /dev/null +++ b/taglib/dsdiff/dsdifffile.h @@ -0,0 +1,285 @@ +/*************************************************************************** + copyright : (C) 2016 by Damien Plisson, Audirvana + email : damien78@audirvana.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_DSDIFFFILE_H +#define TAGLIB_DSDIFFFILE_H + +#include "rifffile.h" +#include "id3v2tag.h" +#include "dsdiffproperties.h" +#include "dsdiffdiintag.h" + +namespace TagLib { + + //! An implementation of DSDIFF metadata + + /*! + * This is implementation of DSDIFF metadata. + * + * This supports an ID3v2 tag as well as reading stream from the ID3 RIFF + * chunk as well as properties from the file. + * Description of the DSDIFF format is available + * at http://dsd-guide.com/sites/default/files/white-papers/DSDIFF_1.5_Spec.pdf + * DSDIFF standard does not explicitly specify the ID3V2 chunk + * It can be found at the root level, but also sometimes inside the PROP chunk + * In addition, title and artist info are stored as part of the standard + */ + + namespace DSDIFF { + + //! An implementation of TagLib::File with DSDIFF specific methods + + /*! + * This implements and provides an interface for DSDIFF files to the + * TagLib::Tag and TagLib::AudioProperties interfaces by way of implementing + * the abstract TagLib::File API as well as providing some additional + * information specific to DSDIFF files. + */ + + class TAGLIB_EXPORT File : public TagLib::File + { + public: + + /*! + * This set of flags is used for various operations and is suitable for + * being OR-ed together. + */ + enum TagTypes { + //! Empty set. Matches no tag types. + NoTags = 0x0000, + //! Matches DIIN tags. + DIIN = 0x0002, + //! Matches ID3v1 tags. + ID3v2 = 0x0002, + //! Matches all tag types. + AllTags = 0xffff + }; + + /*! + * Constructs an DSDIFF file from \a file. If \a readProperties is true + * the file's audio properties will also be read. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs an DSDIFF file from \a stream. If \a readProperties is true + * the file's audio properties will also be read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(IOStream *stream, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + ~File() override; + + /*! + * Returns a pointer to a tag that is the union of the ID3v2 and DIIN + * tags. The ID3v2 tag is given priority in reading the information -- if + * requested information exists in both the ID3v2 tag and the ID3v1 tag, + * the information from the ID3v2 tag will be returned. + * + * If you would like more granular control over the content of the tags, + * with the concession of generality, use the tag-type specific calls. + * + * \note As this tag is not implemented as an ID3v2 tag or a DIIN tag, + * but a union of the two this pointer may not be cast to the specific + * tag types. + * + * \see ID3v2Tag() + * \see DIINTag() + */ + Tag *tag() const override; + + /*! + * Returns the ID3V2 Tag for this file. + * + * \note This always returns a valid pointer regardless of whether or not + * the file on disk has an ID3v2 tag. Use hasID3v2Tag() to check if the + * file on disk actually has an ID3v2 tag. + * + * \see hasID3v2Tag() + */ + ID3v2::Tag *ID3v2Tag(bool create = false) const; + + /*! + * Returns the DSDIFF DIIN Tag for this file + * + */ + DSDIFF::DIIN::Tag *DIINTag(bool create = false) const; + + /*! + * Implements the unified property interface -- export function. + * This method forwards to ID3v2::Tag::properties(). + */ + PropertyMap properties() const override; + + void removeUnsupportedProperties(const StringList &properties) override; + + /*! + * Implements the unified property interface -- import function. + * This method forwards to ID3v2::Tag::setProperties(). + */ + PropertyMap setProperties(const PropertyMap &) override; + + /*! + * Returns the AIFF::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + Properties *audioProperties() const override; + + /*! + * Save the file. If at least one tag -- ID3v1 or DIIN -- exists this + * will duplicate its content into the other tag. This returns true + * if saving was successful. + * + * If neither exists or if both tags are empty, this will strip the tags + * from the file. + * + * This is the same as calling save(AllTags); + * + * If you would like more granular control over the content of the tags, + * with the concession of generality, use paramaterized save call below. + * + * \see save(int tags) + */ + bool save() override; + + /*! + * Save the file. If \a strip is specified, it is possible to choose if + * tags not specified in \a tags should be stripped from the file or + * retained. With \a version, it is possible to specify whether ID3v2.4 + * or ID3v2.3 should be used. + */ + bool save(int tags, StripTags strip = StripOthers, ID3v2::Version version = ID3v2::v4); + + /*! + * This will strip the tags that match the OR-ed together TagTypes from the + * file. By default it strips all tags. It returns true if the tags are + * successfully stripped. + * + * \note This will update the file immediately. + */ + void strip(int tags = AllTags); + + /*! + * Returns whether or not the file on disk actually has an ID3v2 tag. + * + * \see ID3v2Tag() + */ + bool hasID3v2Tag() const; + + /*! + * Returns whether or not the file on disk actually has the DSDIFF + * title and artist tags. + * + * \see DIINTag() + */ + bool hasDIINTag() const; + + /*! + * Returns whether or not the given \a stream can be opened as a DSDIFF + * file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + + protected: + enum Endianness { BigEndian, LittleEndian }; + + File(FileName file, Endianness endianness); + File(IOStream *stream, Endianness endianness); + + private: + File(const File &) = delete; + File &operator=(const File &) = delete; + + void removeRootChunk(const ByteVector &id); + void removeRootChunk(unsigned int chunk); + void removeChildChunk(unsigned int i, unsigned int chunk); + + /*! + * Sets the data for the the specified chunk at root level to \a data. + * + * \warning This will update the file immediately. + */ + void setRootChunkData(unsigned int i, const ByteVector &data); + + /*! + * Sets the data for the root-level chunk \a name to \a data. + * If a root-level chunk with the given name already exists + * it will be overwritten, otherwise it will be + * created after the existing chunks. + * + * \warning This will update the file immediately. + */ + void setRootChunkData(const ByteVector &name, const ByteVector &data); + + /*! + * Sets the data for the the specified child chunk to \a data. + * + * If data is null, then remove the chunk + * + * \warning This will update the file immediately. + */ + void setChildChunkData(unsigned int i, const ByteVector &data, + unsigned int childChunkNum); + + /*! + * Sets the data for the child chunk \a name to \a data. If a chunk with + * the given name already exists it will be overwritten, otherwise it will + * be created after the existing chunks inside child chunk. + * + * If data is null, then remove the chunks with \a name name + * + * \warning This will update the file immediately. + */ + void setChildChunkData(const ByteVector &name, const ByteVector &data, + unsigned int childChunkNum); + + void updateRootChunksStructure(unsigned int startingChunk); + + void read(bool readProperties, Properties::ReadStyle propertiesStyle); + void writeChunk(const ByteVector &name, const ByteVector &data, + unsigned long long offset, unsigned long replace = 0, + unsigned int leadingPadding = 0); + + class FilePrivate; + FilePrivate *d; + }; + } +} + +#endif diff --git a/taglib/dsdiff/dsdiffproperties.cpp b/taglib/dsdiff/dsdiffproperties.cpp new file mode 100644 index 00000000..afa12f3a --- /dev/null +++ b/taglib/dsdiff/dsdiffproperties.cpp @@ -0,0 +1,119 @@ +/*************************************************************************** + copyright : (C) 2016 by Damien Plisson, Audirvana + email : damien78@audirvana.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "tstring.h" +#include "tdebug.h" + +#include "dsdiffproperties.h" + +using namespace TagLib; + +class DSDIFF::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate() : + length(0), + bitrate(0), + sampleRate(0), + channels(0), + sampleWidth(0), + sampleCount(0) + { + } + + int length; + int bitrate; + int sampleRate; + int channels; + int sampleWidth; + unsigned long long sampleCount; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +DSDIFF::Properties::Properties(const unsigned int sampleRate, + const unsigned short channels, + const unsigned long long samplesCount, + const int bitrate, + ReadStyle style) : + AudioProperties(style), + d(new PropertiesPrivate()) +{ + d->channels = channels; + d->sampleCount = samplesCount; + d->sampleWidth = 1; + d->sampleRate = sampleRate; + d->bitrate = bitrate; + d->length = d->sampleRate > 0 + ? static_cast((d->sampleCount * 1000.0) / d->sampleRate + 0.5) + : 0; +} + +DSDIFF::Properties::~Properties() +{ + delete d; +} + +int DSDIFF::Properties::length() const +{ + return lengthInSeconds(); +} + +int DSDIFF::Properties::lengthInSeconds() const +{ + return d->length / 1000; +} + +int DSDIFF::Properties::lengthInMilliseconds() const +{ + return d->length; +} + +int DSDIFF::Properties::bitrate() const +{ + return d->bitrate; +} + +int DSDIFF::Properties::sampleRate() const +{ + return d->sampleRate; +} + +int DSDIFF::Properties::channels() const +{ + return d->channels; +} + +int DSDIFF::Properties::bitsPerSample() const +{ + return d->sampleWidth; +} + +long long DSDIFF::Properties::sampleCount() const +{ + return d->sampleCount; +} diff --git a/taglib/dsdiff/dsdiffproperties.h b/taglib/dsdiff/dsdiffproperties.h new file mode 100644 index 00000000..577afedb --- /dev/null +++ b/taglib/dsdiff/dsdiffproperties.h @@ -0,0 +1,82 @@ +/*************************************************************************** + copyright : (C) 2016 by Damien Plisson, Audirvana + email : damien78@audirvana.com +***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_DSDIFFPROPERTIES_H +#define TAGLIB_DSDIFFPROPERTIES_H + +#include "audioproperties.h" + +namespace TagLib { + + namespace DSDIFF { + + class File; + + //! An implementation of audio property reading for DSDIFF + + /*! + * This reads the data from an DSDIFF stream found in the AudioProperties + * API. + */ + + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + /*! + * Create an instance of DSDIFF::Properties with the data read from the + * ByteVector \a data. + */ + Properties(const unsigned int sampleRate, const unsigned short channels, + const unsigned long long samplesCount, const int bitrate, + ReadStyle style); + + /*! + * Destroys this DSDIFF::Properties instance. + */ + virtual ~Properties(); + + // Reimplementations. + + virtual int length() const; + virtual int lengthInSeconds() const; + virtual int lengthInMilliseconds() const; + virtual int bitrate() const; + virtual int sampleRate() const; + virtual int channels() const; + + int bitsPerSample() const; + long long sampleCount() const; + + private: + Properties(const Properties &); + Properties &operator=(const Properties &); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } +} + +#endif diff --git a/taglib/fileref.cpp b/taglib/fileref.cpp index 32ee212a..5cbf15d8 100644 --- a/taglib/fileref.cpp +++ b/taglib/fileref.cpp @@ -53,6 +53,7 @@ #include "wavpackfile.h" #include "xmfile.h" #include "dsffile.h" +#include "dsdifffile.h" using namespace TagLib; @@ -167,6 +168,8 @@ namespace file = new XM::File(stream, readAudioProperties, audioPropertiesStyle); else if(ext == "DSF") file = new DSF::File(stream, readAudioProperties, audioPropertiesStyle); + else if(ext == "DFF" || ext == "DSDIFF") + file = new DSDIFF::File(stream, readAudioProperties, audioPropertiesStyle); // if file is not valid, leave it to content-based detection. @@ -216,6 +219,8 @@ namespace file = new APE::File(stream, readAudioProperties, audioPropertiesStyle); else if(DSF::File::isSupported(stream)) file = new DSF::File(stream, readAudioProperties, audioPropertiesStyle); + else if(DSDIFF::File::isSupported(stream)) + file = new DSDIFF::File(stream, readAudioProperties, audioPropertiesStyle); // isSupported() only does a quick check, so double check the file here. @@ -297,6 +302,8 @@ namespace return new XM::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "DSF") return new DSF::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "DFF" || ext == "DSDIFF") + return new DSDIFF::File(fileName, readAudioProperties, audioPropertiesStyle); return nullptr; } @@ -430,6 +437,8 @@ StringList FileRef::defaultFileExtensions() l.append("it"); l.append("xm"); l.append("dsf"); + l.append("dff"); + l.append("dsdiff"); // alias for "dff" return l; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 472c3b63..fbd6eb83 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,6 +26,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/it ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/xm ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/dsf + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/dsdiff ) SET(test_runner_SRCS @@ -71,6 +72,7 @@ SET(test_runner_SRCS test_opus.cpp test_speex.cpp test_dsf.cpp + test_dsdiff.cpp test_sizes.cpp test_versionnumber.cpp test_tag_c.cpp diff --git a/tests/data/empty10ms.dff b/tests/data/empty10ms.dff new file mode 100644 index 0000000000000000000000000000000000000000..9dc0b9ecdfd67c6e5cf8cb776aeac93c84200dc6 GIT binary patch literal 7186 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 n!DH@pWX@G)8Y5 +#include +#include +#include +#include +#include +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestDSDIFF : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestDSDIFF); + CPPUNIT_TEST(testProperties); + CPPUNIT_TEST(testTags); + CPPUNIT_TEST(testSaveID3v2); + CPPUNIT_TEST(testSaveID3v23); + CPPUNIT_TEST(testStrip); + CPPUNIT_TEST(testRepeatedSave); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testProperties() + { + DSDIFF::File f(TEST_FILE_PATH_C("empty10ms.dff")); + CPPUNIT_ASSERT(f.audioProperties()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->lengthInSeconds()); + CPPUNIT_ASSERT_EQUAL(10, f.audioProperties()->lengthInMilliseconds()); + CPPUNIT_ASSERT_EQUAL(5644, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(2822400, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->bitsPerSample()); + CPPUNIT_ASSERT_EQUAL(static_cast(28224), f.audioProperties()->sampleCount()); + } + + void testTags() + { + ScopedFileCopy copy("empty10ms", ".dff"); + string newname = copy.fileName(); + + DSDIFF::File *f = new DSDIFF::File(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(String(""), f->tag()->artist()); + f->tag()->setArtist("The Artist"); + f->save(); + delete f; + + f = new DSDIFF::File(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(String("The Artist"), f->tag()->artist()); + delete f; + } + + void testSaveID3v2() + { + ScopedFileCopy copy("empty10ms", ".dff"); + string newname = copy.fileName(); + + { + DSDIFF::File f(newname.c_str()); + CPPUNIT_ASSERT(!f.hasID3v2Tag()); + + f.tag()->setTitle(L"TitleXXX"); + f.save(); + CPPUNIT_ASSERT(f.hasID3v2Tag()); + } + { + DSDIFF::File f(newname.c_str()); + CPPUNIT_ASSERT(f.hasID3v2Tag()); + CPPUNIT_ASSERT_EQUAL(String(L"TitleXXX"), f.tag()->title()); + + f.tag()->setTitle(""); + f.save(); + CPPUNIT_ASSERT(!f.hasID3v2Tag()); + } + { + DSDIFF::File f(newname.c_str()); + CPPUNIT_ASSERT(!f.hasID3v2Tag()); + f.tag()->setTitle(L"TitleXXX"); + f.save(); + CPPUNIT_ASSERT(f.hasID3v2Tag()); + } + { + DSDIFF::File f(newname.c_str()); + CPPUNIT_ASSERT(f.hasID3v2Tag()); + CPPUNIT_ASSERT_EQUAL(String(L"TitleXXX"), f.tag()->title()); + } + } + + void testSaveID3v23() + { + ScopedFileCopy copy("empty10ms", ".dff"); + string newname = copy.fileName(); + + String xxx = ByteVector(254, 'X'); + { + DSDIFF::File f(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(false, f.hasID3v2Tag()); + + f.tag()->setTitle(xxx); + f.tag()->setArtist("Artist A"); + f.save(DSDIFF::File::AllTags, File::StripOthers, ID3v2::v3); + CPPUNIT_ASSERT_EQUAL(true, f.hasID3v2Tag()); + } + { + DSDIFF::File f2(newname.c_str()); + CPPUNIT_ASSERT_EQUAL((unsigned int)3, f2.ID3v2Tag()->header()->majorVersion()); + CPPUNIT_ASSERT_EQUAL(String("Artist A"), f2.tag()->artist()); + CPPUNIT_ASSERT_EQUAL(xxx, f2.tag()->title()); + } + } + + void testStrip() + { + { + ScopedFileCopy copy("empty10ms", ".dff"); + { + DSDIFF::File f(copy.fileName().c_str()); + f.tag()->setArtist("X"); + f.save(); + } + { + DSDIFF::File f(copy.fileName().c_str()); + CPPUNIT_ASSERT(f.hasID3v2Tag()); + CPPUNIT_ASSERT(f.hasDIINTag()); + f.strip(); + } + { + DSDIFF::File f(copy.fileName().c_str()); + CPPUNIT_ASSERT(!f.hasID3v2Tag()); + CPPUNIT_ASSERT(!f.hasDIINTag()); + } + } + + { + ScopedFileCopy copy("empty10ms", ".dff"); + { + DSDIFF::File f(copy.fileName().c_str()); + f.ID3v2Tag(true); + f.DIINTag(true); + f.tag()->setArtist("X"); + f.save(); + } + { + DSDIFF::File f(copy.fileName().c_str()); + CPPUNIT_ASSERT(f.hasID3v2Tag()); + CPPUNIT_ASSERT(f.hasDIINTag()); + f.strip(DSDIFF::File::ID3v2); + } + { + DSDIFF::File f(copy.fileName().c_str()); + CPPUNIT_ASSERT(!f.hasID3v2Tag()); + CPPUNIT_ASSERT(f.hasDIINTag()); + } + } + + { + ScopedFileCopy copy("empty10ms", ".dff"); + { + DSDIFF::File f(copy.fileName().c_str()); + f.tag()->setArtist("X"); + f.save(); + } + { + DSDIFF::File f(copy.fileName().c_str()); + CPPUNIT_ASSERT(f.hasID3v2Tag()); + CPPUNIT_ASSERT(f.hasDIINTag()); + f.strip(DSDIFF::File::DIIN); + } + { + DSDIFF::File f(copy.fileName().c_str()); + CPPUNIT_ASSERT(f.hasID3v2Tag()); + CPPUNIT_ASSERT(!f.hasDIINTag()); + } + } + } + + void testRepeatedSave() + { + ScopedFileCopy copy("empty10ms", ".dff"); + string newname = copy.fileName(); + + { + DSDIFF::File f(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(String(""), f.tag()->title()); + f.tag()->setTitle("NEW TITLE"); + f.save(); + CPPUNIT_ASSERT_EQUAL(String("NEW TITLE"), f.tag()->title()); + f.tag()->setTitle("NEW TITLE 2"); + f.save(); + CPPUNIT_ASSERT_EQUAL(String("NEW TITLE 2"), f.tag()->title()); + CPPUNIT_ASSERT_EQUAL(static_cast(8252), f.length()); + f.save(); + CPPUNIT_ASSERT_EQUAL(static_cast(8252), f.length()); + } + { + DSDIFF::File f(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(String("NEW TITLE 2"), f.tag()->title()); + } + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestDSDIFF); diff --git a/tests/test_fileref.cpp b/tests/test_fileref.cpp index 8c6928a2..024ea1a4 100644 --- a/tests/test_fileref.cpp +++ b/tests/test_fileref.cpp @@ -46,6 +46,7 @@ #include "opusfile.h" #include "xmfile.h" #include "dsffile.h" +#include "dsdifffile.h" #include #include "utils.h" @@ -101,6 +102,7 @@ class TestFileRef : public CppUnit::TestFixture CPPUNIT_TEST(testWavPack); CPPUNIT_TEST(testOpus); CPPUNIT_TEST(testDSF); + CPPUNIT_TEST(testDSDIFF); CPPUNIT_TEST(testUnsupported); CPPUNIT_TEST(testCreate); CPPUNIT_TEST(testAudioProperties); @@ -338,6 +340,11 @@ public: fileRefSave("empty10ms",".dsf"); } + void testDSDIFF() + { + fileRefSave("empty10ms",".dff"); + } + void testUnsupported() { FileRef f1(TEST_FILE_PATH_C("no-extension")); @@ -394,6 +401,8 @@ public: CPPUNIT_ASSERT(extensions.contains("opus")); CPPUNIT_ASSERT(extensions.contains("xm")); CPPUNIT_ASSERT(extensions.contains("dsf")); + CPPUNIT_ASSERT(extensions.contains("dff")); + CPPUNIT_ASSERT(extensions.contains("dsdiff")); } void testFileResolver() diff --git a/tests/test_sizes.cpp b/tests/test_sizes.cpp index 4a88ba1c..4ee00cfe 100644 --- a/tests/test_sizes.cpp +++ b/tests/test_sizes.cpp @@ -43,6 +43,8 @@ #include "commentsframe.h" #include "dsffile.h" #include "dsfproperties.h" +#include "dsdifffile.h" +#include "dsdiffproperties.h" #include "eventtimingcodesframe.h" #include "fileref.h" #include "flacfile.h" @@ -160,6 +162,8 @@ public: CPPUNIT_ASSERT_EQUAL(classSize(2, false), sizeof(TagLib::ByteVectorList)); CPPUNIT_ASSERT_EQUAL(classSize(1, true), sizeof(TagLib::ByteVectorStream)); CPPUNIT_ASSERT_EQUAL(classSize(0, true), sizeof(TagLib::DebugListener)); + CPPUNIT_ASSERT_EQUAL(classSize(1, true), sizeof(TagLib::DSDIFF::File)); + CPPUNIT_ASSERT_EQUAL(classSize(1, true), sizeof(TagLib::DSDIFF::Properties)); CPPUNIT_ASSERT_EQUAL(classSize(1, true), sizeof(TagLib::FLAC::File)); CPPUNIT_ASSERT_EQUAL(classSize(1, true), sizeof(TagLib::DSF::File)); CPPUNIT_ASSERT_EQUAL(classSize(1, true), sizeof(TagLib::DSF::Properties));