diff --git a/plugins/editor/src/editor/Editor.cpp b/plugins/editor/src/editor/Editor.cpp index b11c9e4c2..a31c7c733 100644 --- a/plugins/editor/src/editor/Editor.cpp +++ b/plugins/editor/src/editor/Editor.cpp @@ -1078,6 +1078,8 @@ void Editor::Impl::chooseSfzFile() fs->addFileExtension(CFileExtension("AIF", "aif")); fs->addFileExtension(CFileExtension("AIFF", "aiff")); fs->addFileExtension(CFileExtension("AIFC", "aifc")); + // Decent samples + fs->addFileExtension(CFileExtension("DSPRESET", "dspreset")); std::string initialDir = getFileChooserInitialDir(currentSfzFile_); if (!initialDir.empty()) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5c9e34164..66fcb9582 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -245,11 +245,13 @@ endif() # Import library set(SFIZZ_IMPORT_HEADERS sfizz/import/ForeignInstrument.h - sfizz/import/foreign_instruments/AudioFile.h) + sfizz/import/foreign_instruments/AudioFile.h + sfizz/import/foreign_instruments/DecentSampler.h) set(SFIZZ_IMPORT_SOURCES sfizz/import/ForeignInstrument.cpp - sfizz/import/foreign_instruments/AudioFile.cpp) + sfizz/import/foreign_instruments/AudioFile.cpp + sfizz/import/foreign_instruments/DecentSampler.cpp) add_library(sfizz_import STATIC) add_library(sfizz::import ALIAS sfizz_import) @@ -258,7 +260,7 @@ target_sources(sfizz_import PRIVATE target_include_directories(sfizz_import PUBLIC ".") target_link_libraries(sfizz_import PUBLIC absl::strings absl::memory sfizz::filesystem - PRIVATE sfizz::pugixml) + PRIVATE sfizz::internal sfizz::pugixml) # Sfizz spinlock mutex add_library(sfizz_spin_mutex STATIC diff --git a/src/sfizz/import/ForeignInstrument.cpp b/src/sfizz/import/ForeignInstrument.cpp index 0508010e5..7853ff3ae 100644 --- a/src/sfizz/import/ForeignInstrument.cpp +++ b/src/sfizz/import/ForeignInstrument.cpp @@ -6,12 +6,14 @@ #include "ForeignInstrument.h" #include "foreign_instruments/AudioFile.h" +#include "foreign_instruments/DecentSampler.h" namespace sfz { InstrumentFormatRegistry::InstrumentFormatRegistry() : formats_ { &AudioFileInstrumentFormat::getInstance(), + &DecentSamplerInstrumentFormat::getInstance(), } { } diff --git a/src/sfizz/import/foreign_instruments/DecentSampler.cpp b/src/sfizz/import/foreign_instruments/DecentSampler.cpp new file mode 100644 index 000000000..a5785eb10 --- /dev/null +++ b/src/sfizz/import/foreign_instruments/DecentSampler.cpp @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "DecentSampler.h" +#include "sfizz/Opcode.h" +#include +#include +#include +#include +#include +#include +#include + +namespace sfz { + +DecentSamplerInstrumentFormat& DecentSamplerInstrumentFormat::getInstance() +{ + static DecentSamplerInstrumentFormat format; + return format; +} + +const char* DecentSamplerInstrumentFormat::name() const noexcept +{ + return "DecentSampler instrument"; +} + +bool DecentSamplerInstrumentFormat::matchesFilePath(const fs::path& path) const +{ + const std::string ext = path.extension().u8string(); + return absl::EqualsIgnoreCase(ext, ".dspreset"); +} + +std::unique_ptr DecentSamplerInstrumentFormat::createImporter() const +{ + return absl::make_unique(); +} + +/// +std::string DecentSamplerInstrumentImporter::convertToSfz(const fs::path& path) const +{ + std::ostringstream os; + os.imbue(std::locale::classic()); + + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(path.c_str()); + + if (!result) { + std::cerr << "[sfizz] dspreset: cannot load xml\n"; + return {}; + } + + pugi::xml_node rootNode(doc.child("DecentSampler")); + if (!rootNode) { + std::cerr << "[sfizz] dspreset: missing element\n"; + return {}; + } + + pugi::xml_node globalNode(rootNode.child("groups")); + os << "\n"; + emitRegionalOpcodes(os, globalNode); + + for (pugi::xml_node groupNode: globalNode.children("group")) + { + os << "\n"; + emitRegionalOpcodes(os, groupNode); + + for (pugi::xml_node sampleNode: groupNode.children("sample")) + { + os << "\n"; + emitRegionalOpcodes(os, sampleNode); + } + } + + // TODO effects + + return os.str(); +} + +void DecentSamplerInstrumentImporter::emitRegionalOpcodes(std::ostream& os, pugi::xml_node node) const +{ + std::vector xmlOpcodes; + xmlOpcodes.reserve(64); + + for (pugi::xml_attribute attr : node.attributes()) + xmlOpcodes.emplace_back(attr.name(), attr.value()); + + // a number of opcodes must respect a particular processing order + // sort opcodes by computing a score to each (lower is first) + + std::sort( + xmlOpcodes.begin(), xmlOpcodes.end(), + [](const Opcode& a, const Opcode& b) { + auto score = [](const Opcode& x) -> int { + switch (x.lettersOnlyHash) { + case hash("rootNote"): + // rootNote before loNote/hiNote + return -1; + default: + return 0; + } + }; + return score(a) < score(b); + }); + + + for (const Opcode& xmlOpcode : xmlOpcodes) { + auto convertToInt = [&os, &xmlOpcode](absl::string_view name) { + int64_t value; + if (readLeadingInt(xmlOpcode.value, &value)) + os << name << '=' << value << '\n'; + }; + auto convertToReal = [&os, &xmlOpcode](absl::string_view name) { + double value; + if (readLeadingFloat(xmlOpcode.value, &value)) + os << name << '=' << value << '\n'; + }; + auto convertToRealEx = [&os, &xmlOpcode](absl::string_view name, double(*conv)(double)) { + double value; + if (readLeadingFloat(xmlOpcode.value, &value)) + os << name << '=' << conv(value) << '\n'; + }; + + /// + switch (xmlOpcode.lettersOnlyHash) { + case hash("volume"): + { + absl::string_view unit; + double value; + if (readLeadingFloat(xmlOpcode.value, &value, &unit)) { + os << ((unit == "dB") ? "volume" : "amplitude") + << '=' << value << "\n"; + } + } + break; + case hash("ampVeltrack"): + convertToReal("amp_veltrack"); + break; + case hash("path"): + os << "sample=" << xmlOpcode.value << '\n'; + break; + case hash("rootNote"): + convertToInt("key"); + break; + case hash("loNote"): + convertToInt("lokey"); + break; + case hash("hiNote"): + convertToInt("hikey"); + break; + case hash("loVel"): + convertToInt("lovel"); + break; + case hash("hiVel"): + convertToInt("hivel"); + break; + case hash("start"): + convertToInt("offset"); + break; + case hash("end"): + convertToInt("end"); + break; + case hash("tuning"): + convertToReal("transpose"); + break; + case hash("pan"): + convertToReal("pan"); + break; + case hash("trigger"): + os << "trigger=" << xmlOpcode.value << '\n'; + break; + case hash("onLoCC&"): + convertToInt("on_locc" + std::to_string(xmlOpcode.parameters[0])); + break; + case hash("onHiCC&"): + convertToInt("on_hicc" + std::to_string(xmlOpcode.parameters[0])); + break; + case hash("loopStart"): + convertToInt("loop_start"); + break; + case hash("loopEnd"): + convertToInt("loop_end"); + break; + case hash("loopCrossfade"): + convertToRealEx( + "loop_crossfade", + [](double xfadeInFrames) -> double { + // transform this value to seconds + // TODO: probably should get sample rate from the audio file? + return xfadeInFrames / 44100.0; + }); + break; + case hash("loopCrossfadeMode"): + // TODO + break; + case hash("loopEnabled"): + os << "loop_mode=" + << ((xmlOpcode.value == "true") ? "loop_continuous" : "one_shot") << "\n"; + break; + case hash("attack"): + convertToReal("ampeg_attack"); + break; + case hash("decay"): + convertToReal("ampeg_decay"); + break; + case hash("sustain"): + convertToRealEx("ampeg_sustain", [](double x) -> double { return 100.0 * x; }); + break; + case hash("release"): + convertToReal("ampeg_release"); + break; + case hash("seqMode"): + // TODO + break; + case hash("seqPosition"): + convertToInt("seq_position"); + break; + } + } +} + +const InstrumentFormat* DecentSamplerInstrumentImporter::getFormat() const noexcept +{ + return &DecentSamplerInstrumentFormat::getInstance(); +} + +} // namespace sfz diff --git a/src/sfizz/import/foreign_instruments/DecentSampler.h b/src/sfizz/import/foreign_instruments/DecentSampler.h new file mode 100644 index 000000000..a15952925 --- /dev/null +++ b/src/sfizz/import/foreign_instruments/DecentSampler.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "../ForeignInstrument.h" +#include +#include + +namespace sfz { + +class DecentSamplerInstrumentFormat : public InstrumentFormat { +private: + DecentSamplerInstrumentFormat() noexcept = default; + +public: + static DecentSamplerInstrumentFormat& getInstance(); + const char* name() const noexcept override; + bool matchesFilePath(const fs::path& path) const override; + std::unique_ptr createImporter() const override; +}; + +/// +class DecentSamplerInstrumentImporter : public InstrumentImporter { +public: + std::string convertToSfz(const fs::path& path) const override; + const InstrumentFormat* getFormat() const noexcept override; + +private: + void emitRegionalOpcodes(std::ostream& os, pugi::xml_node node) const; +}; + +} // namespace sfz