//------------------------------------------------------------------------ // Flags : clang-format SMTGSequencer // Project : VST3 SDK // Filename : public.sdk/source/vst/utility/vst2persistence.cpp // Created by : Steinberg, 12/2019 // Description : vst2 persistence helper // //----------------------------------------------------------------------------- // This file is part of a Steinberg SDK. It is subject to the license terms // in the LICENSE file found in the top-level directory of this distribution // and at www.steinberg.net/sdklicenses. // No part of the SDK, including this file, may be copied, modified, propagated, // or distributed except according to the terms contained in the LICENSE file. //----------------------------------------------------------------------------- #include "public.sdk/source/vst/utility/vst2persistence.h" #include "pluginterfaces/base/fplatform.h" #include //------------------------------------------------------------------------ namespace VST3 { namespace { namespace IO { //------------------------------------------------------------------------ enum class Error { NoError, Unknown, EndOfFile, BufferToBig, NotAllowed, InvalidArgument, }; //------------------------------------------------------------------------ enum class SeekMode { Set, End, Current }; //------------------------------------------------------------------------ struct Result { Error error {Error::Unknown}; uint64_t bytes {0u}; Result () noexcept = default; Result (Error error, uint64_t bytes = 0) noexcept : error (error), bytes (bytes) {} operator bool () const noexcept { return error == Error::NoError; } }; //------------------------------------------------------------------------ struct ReadBufferDesc { const uint64_t bytes; void* ptr; }; //------------------------------------------------------------------------ struct WriteBufferDesc { const uint64_t bytes; const void* ptr; }; //------------------------------------------------------------------------ template class ByteOrderStream { public: //------------------------------------------------------------------------ ByteOrderStream (Steinberg::IBStream& stream) noexcept : stream (stream) {} ByteOrderStream (ByteOrderStream&&) noexcept = delete; ByteOrderStream& operator= (ByteOrderStream&&) noexcept = delete; ByteOrderStream (const ByteOrderStream&) noexcept = delete; ByteOrderStream& operator= (const ByteOrderStream&) noexcept = delete; inline Result operator<< (const std::string& input) noexcept; inline Result operator>> (std::string& output) noexcept; template inline Result operator<< (const T& input) noexcept; template inline Result operator>> (T& output) const noexcept; inline Result read (const ReadBufferDesc& buffer) const noexcept; inline Result write (const WriteBufferDesc& buffer) noexcept; inline Result seek (SeekMode mode, int64_t bytes) const noexcept; inline Result tell () const noexcept; //------------------------------------------------------------------------ private: template inline Result swapAndWrite (const uint8_t* buffer) noexcept; inline void swap (uint8_t* buffer, uint64_t size) const noexcept; Steinberg::IBStream& stream; }; //------------------------------------------------------------------------ using LittleEndianStream = ByteOrderStream; using BigEndianStream = ByteOrderStream; using NativeEndianStream = ByteOrderStream; //------------------------------------------------------------------------ template inline Result ByteOrderStream::read (const ReadBufferDesc& buffer) const noexcept { if (buffer.bytes > static_cast (std::numeric_limits::max ())) return Result (Error::BufferToBig); Steinberg::int32 readBytes = 0; auto tres = stream.read (buffer.ptr, static_cast (buffer.bytes), &readBytes); if (tres != Steinberg::kResultTrue) return Result (Error::Unknown); assert (readBytes >= 0); return Result {Error::NoError, static_cast (readBytes)}; } //------------------------------------------------------------------------ template inline Result ByteOrderStream::write (const WriteBufferDesc& buffer) noexcept { if (buffer.bytes > static_cast (std::numeric_limits::max ())) return Result (Error::BufferToBig); Steinberg::int32 writtenBytes = 0; auto tres = stream.write (const_cast (buffer.ptr), static_cast (buffer.bytes), &writtenBytes); if (tres != Steinberg::kResultTrue) return Result (Error::Unknown); assert (writtenBytes >= 0); return Result {Error::NoError, static_cast (writtenBytes)}; } //------------------------------------------------------------------------ template inline Result ByteOrderStream::seek (SeekMode mode, int64_t bytes) const noexcept { Steinberg::int32 seekMode = 0; switch (mode) { case SeekMode::Set: seekMode = Steinberg::IBStream::kIBSeekSet; break; case SeekMode::Current: seekMode = Steinberg::IBStream::kIBSeekCur; break; case SeekMode::End: seekMode = Steinberg::IBStream::kIBSeekEnd; break; } Steinberg::int64 seekRes = 0; auto tres = stream.seek (static_cast (bytes), seekMode, &seekRes); if (tres != Steinberg::kResultTrue || seekRes < 0) return Result {Error::Unknown}; return Result (Error::NoError, static_cast (seekRes)); } //------------------------------------------------------------------------ template inline Result ByteOrderStream::tell () const noexcept { Steinberg::int64 tellRes = 0; auto tres = stream.tell (&tellRes); if (tres != Steinberg::kResultTrue || tellRes < 0) return Result {Error::Unknown}; return Result {Error::NoError, static_cast (tellRes)}; } //------------------------------------------------------------------------ template inline Result ByteOrderStream::operator<< (const std::string& input) noexcept { auto res = *this << static_cast (input.length ()); if (!res) return res; res = stream.write (const_cast (static_cast (input.data ())), static_cast (input.length ())); res.bytes += sizeof (uint64_t); return res; } //------------------------------------------------------------------------ template inline Result ByteOrderStream::operator>> (std::string& output) noexcept { uint64_t length; auto res = *this >> length; if (!res) return res; output.resize (length); if (length > 0) { res = stream.read (&output.front (), static_cast (length)); res.bytes += sizeof (uint64_t); } return res; } //------------------------------------------------------------------------ template template inline Result ByteOrderStream::operator<< (const T& input) noexcept { static_assert (std::is_standard_layout::value, "Only standard layout types allowed"); // with C++17: if constexpr (StreamByteOrder == BYTEORDER) if (constexpr bool tmp = (StreamByteOrder == BYTEORDER)) return write (WriteBufferDesc {sizeof (T), static_cast (&input)}); return swapAndWrite (reinterpret_cast (&input)); } //------------------------------------------------------------------------ template template inline Result ByteOrderStream::operator>> (T& output) const noexcept { static_assert (std::is_standard_layout::value, "Only standard layout types allowed"); auto res = read (ReadBufferDesc {sizeof (T), &output}); // with C++17: if constexpr (StreamByteOrder == BYTEORDER) if (constexpr bool tmp = (StreamByteOrder == BYTEORDER)) return res; swap (reinterpret_cast (&output), res.bytes); return res; } //------------------------------------------------------------------------ template template inline Result ByteOrderStream::swapAndWrite (const uint8_t* buffer) noexcept { // with C++17: if constexpr (_size > 1) if (constexpr bool tmp2 = (_size > 1)) { int8_t tmp[_size]; constexpr auto halfSize = _size / 2; auto size = _size; auto low = buffer; auto high = buffer + size - 1; while (size > halfSize) { tmp[size - 2] = buffer[(_size - size) + 1]; tmp[(_size - size) + 1] = buffer[size - 2]; tmp[_size - size] = *high; tmp[size - 1] = *low; low += 2; high -= 2; size -= 2; } return write (WriteBufferDesc {_size, tmp}); } return write (WriteBufferDesc {1, buffer}); } //------------------------------------------------------------------------ template inline void ByteOrderStream::swap (uint8_t* buffer, uint64_t size) const noexcept { if (size < 2) return; auto low = buffer; auto high = buffer + size - 1; while (size >= 2) { auto tmp = *low; *low = *high; *high = tmp; low += 2; high -= 2; size -= 2; } } //------------------------------------------------------------------------ } // IO //------------------------------------------------------------------------ constexpr int32_t cMagic = 'CcnK'; constexpr int32_t bankMagic = 'FxBk'; constexpr int32_t privateChunkID = 'VstW'; constexpr int32_t chunkBankMagic = 'FBCh'; constexpr int32_t programMagic = 'FxCk'; constexpr int32_t chunkProgramMagic = 'FPCh'; //------------------------------------------------------------------------ Optional loadProgram (const IO::BigEndianStream& state, const Optional& vst2xUniqueID) { Vst2xProgram program; int32_t id; if (!(state >> id)) return {}; if (id != cMagic) return {}; int32_t bankSize; if (!(state >> bankSize)) return {}; int32_t fxMagic; if (!(state >> fxMagic)) return {}; if (!(fxMagic == programMagic || fxMagic == chunkProgramMagic)) return {}; int32_t formatVersion; if (!(state >> formatVersion)) return {}; int32_t fxId; if (!(state >> fxId)) return {}; if (vst2xUniqueID && fxId != *vst2xUniqueID) return {}; int32_t fxVersion; if (!(state >> fxVersion)) return {}; int32_t numParams; if (!(state >> numParams)) return {}; if (numParams < 0) return {}; char name[29]; if (!state.read ({28, name})) return {}; name[28] = 0; program.name = name; program.fxUniqueID = fxId; program.fxVersion = fxVersion; if (fxMagic == chunkProgramMagic) { uint32_t chunkSize; if (!(state >> chunkSize)) return {}; program.chunk.resize (chunkSize); if (!state.read ({chunkSize, program.chunk.data ()})) return {}; } else { program.values.resize (numParams); float paramValue; for (int32_t i = 0; i < numParams; ++i) { if (!(state >> paramValue)) return {}; program.values[i] = paramValue; } } return {std::move (program)}; } //------------------------------------------------------------------------ bool loadPrograms (Steinberg::IBStream& stream, Vst2xState::Programs& programs, const Optional& vst2xUniqueID) { IO::BigEndianStream state (stream); for (auto& program : programs) { if (auto prg = loadProgram (state, vst2xUniqueID)) std::swap (program, *prg); else return false; } return true; } //------------------------------------------------------------------------ template IO::Error streamSizeWriter (StreamT& stream, Proc proc) { auto startPos = stream.tell (); if (startPos.error != IO::Error::NoError) return startPos.error; auto res = stream << static_cast (0); // placeholder if (!res) return res.error; auto procRes = proc (); if (procRes != IO::Error::NoError) return procRes; auto endPos = stream.tell (); if (endPos.error != IO::Error::NoError) return endPos.error; auto size = (endPos.bytes - startPos.bytes) - 4; auto typeSize = static_cast (size); if (size != static_cast (typeSize)) return IO::Error::Unknown; res = stream.seek (IO::SeekMode::Set, startPos.bytes); if (!res) return res.error; res = (stream << typeSize); if (!res) return res.error; res = stream.seek (IO::SeekMode::Set, endPos.bytes); return res.error; } //------------------------------------------------------------------------ template IO::Error writePrograms (StreamT& stream, const Vst2xState::Programs& programs) { for (const auto& program : programs) { auto res = stream << cMagic; if (!res) return res.error; res = streamSizeWriter (stream, [&] () { bool writeChunk = !program.chunk.empty (); if (!(res = stream << (writeChunk ? chunkProgramMagic : programMagic))) return res.error; int32_t version = 1; if (!(res = stream << version)) return res.error; if (!(res = stream << program.fxUniqueID)) return res.error; int32_t fxVersion = program.fxVersion; if (!(res = stream << fxVersion)) return res.error; uint32_t numParams = static_cast (program.values.size ()); if (!(res = stream << numParams)) return res.error; auto programName = program.name; programName.resize (28); for (auto c : programName) { if (!(res = stream << c)) return res.error; } if (writeChunk) { if (!(res = stream << static_cast (program.chunk.size ()))) return res.error; if (!(res = stream.write ({program.chunk.size (), program.chunk.data ()}))) return res.error; } else { for (auto value : program.values) { if (!(res = stream << value)) return res.error; } } return IO::Error::NoError; }); if (res.error != IO::Error::NoError) return res.error; } return IO::Error::NoError; } //------------------------------------------------------------------------ } // anonymous //------------------------------------------------------------------------ Optional tryVst2StateLoad (Steinberg::IBStream& stream, Optional vst2xUniqueID) noexcept { Vst2xState result; IO::BigEndianStream state (stream); int32_t version; int32_t size; int32_t id; if (!(state >> id)) return {}; if (id == privateChunkID) { if (!(state >> size)) return {}; if (!(state >> version)) return {}; int32_t bypass; if (!(state >> bypass)) return {}; result.isBypassed = bypass ? true : false; if (!(state >> id)) return {}; } if (id != cMagic) return {}; int32_t bankSize; if (!(state >> bankSize)) return {}; int32_t fxMagic; if (!(state >> fxMagic)) return {}; if (!(fxMagic == bankMagic || fxMagic == chunkBankMagic)) return {}; int32_t bankVersion; if (!(state >> bankVersion)) return {}; int32_t fxId; if (!(state >> fxId)) return {}; if (vst2xUniqueID && fxId != *vst2xUniqueID) return {}; result.fxUniqueID = fxId; int32_t fxVersion; if (!(state >> fxVersion)) return {}; result.fxVersion = fxVersion; int32_t numPrograms; if (!(state >> numPrograms)) return {}; if (numPrograms < 1 && fxMagic == bankMagic) return {}; int32_t currentProgram = 0; if (bankVersion >= 1) { if (!(state >> currentProgram)) return {}; state.seek (IO::SeekMode::Current, 124); // future } result.currentProgram = currentProgram; if (fxMagic == bankMagic) { result.programs.resize (numPrograms); if (!loadPrograms (stream, result.programs, vst2xUniqueID)) return {}; assert (static_cast (result.programs.size ()) > currentProgram); } else { uint32_t chunkSize; if (!(state >> chunkSize)) return {}; if (chunkSize == 0) return {}; result.chunk.resize (chunkSize); if (!state.read ({chunkSize, result.chunk.data ()})) return {}; } return {std::move (result)}; } //------------------------------------------------------------------------ bool writeVst2State (const Vst2xState& state, Steinberg::IBStream& _stream, bool writeBypassState) noexcept { IO::BigEndianStream stream (_stream); if (writeBypassState) { if (!(stream << privateChunkID)) return false; if (streamSizeWriter (stream, [&] () { uint32_t version = 1; auto res = (stream << version); if (!res) return res.error; int32_t bypass = state.isBypassed ? 1 : 0; return (stream << bypass).error; }) != IO::Error::NoError) { return false; } } if (!(stream << cMagic)) { return false; } if (streamSizeWriter (stream, [&] () { bool writeChunk = !state.chunk.empty (); IO::Result res; if (!(res = (stream << (writeChunk ? chunkBankMagic : bankMagic)))) return res.error; int32_t bankVersion = 2; if (!(res = (stream << bankVersion))) return res.error; if (!(res = (stream << state.fxUniqueID))) return res.error; if (!(res = (stream << state.fxVersion))) return res.error; int32_t numPrograms = writeChunk ? 1 : static_cast (state.programs.size ()); if (!(res = (stream << numPrograms))) return res.error; if (bankVersion > 1) { if (!(res = (stream << state.currentProgram))) return res.error; // write 124 zero bytes uint8_t byte = 0; for (uint32_t i = 0; i < 124; ++i) if (!(res = (stream << byte))) return res.error; } if (writeChunk) { auto chunkSize = static_cast (state.chunk.size ()); if (!(res = (stream << chunkSize))) return res.error; stream.write ({state.chunk.size (), state.chunk.data ()}); } else { writePrograms (stream, state.programs); } return IO::Error::NoError; }) != IO::Error::NoError) { return false; } return true; } //------------------------------------------------------------------------ Optional tryVst2ProgramLoad (Steinberg::IBStream& stream, Optional vst2xUniqueID) noexcept { IO::BigEndianStream state (stream); return loadProgram (state, vst2xUniqueID); } //------------------------------------------------------------------------ } // VST3