diff --git a/dpf.mk b/dpf.mk index d9700993..58e5721d 100644 --- a/dpf.mk +++ b/dpf.mk @@ -76,6 +76,7 @@ SFIZZ_SOURCES = \ src/sfizz/effects/Width.cpp \ src/sfizz/EQPool.cpp \ src/sfizz/FileId.cpp \ + src/sfizz/FileInstrument.cpp \ src/sfizz/FilePool.cpp \ src/sfizz/FilterPool.cpp \ src/sfizz/FloatEnvelopes.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 03f976d2..6b950336 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,6 +7,7 @@ set (SFIZZ_SOURCES sfizz/Synth.cpp sfizz/FileId.cpp sfizz/FilePool.cpp + sfizz/FileInstrument.cpp sfizz/FilterPool.cpp sfizz/EQPool.cpp sfizz/Region.cpp diff --git a/src/sfizz/FileInstrument.cpp b/src/sfizz/FileInstrument.cpp new file mode 100644 index 00000000..7ba11144 --- /dev/null +++ b/src/sfizz/FileInstrument.cpp @@ -0,0 +1,143 @@ +// 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 "FileInstrument.h" +#include "absl/types/span.h" +#include +#include +#include + +namespace sfz { + +// Utility: file cleanup + +struct FILE_deleter { + void operator()(FILE* x) const noexcept { fclose(x); } +}; +typedef std::unique_ptr FILE_u; + +// Utility: binary file IO + +static bool fread_u32le(FILE* stream, uint32_t& value) +{ + uint8_t bytes[4]; + if (fread(bytes, 4, 1, stream) != 1) + return false; + value = bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); + return true; +} + +static bool fread_u32be(FILE* stream, uint32_t& value) +{ + uint8_t bytes[4]; + if (fread(bytes, 4, 1, stream) != 1) + return false; + value = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]; + return true; +} + +/** + * @brief Extract the instrument data from the RIFF sampler block + * + * @param data sampler block data, except the 8 leading bytes 'smpl' + size + * @param ins destination instrument + */ +static bool extractSamplerChunkInstrument( + absl::Span data, SF_INSTRUMENT& ins) +{ + auto extractU32 = [&data](const uint32_t offset) -> uint32_t { + const uint8_t* bytes = &data[offset]; + if (bytes + 4 > data.end()) + return 0; + return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); + }; + + ins.gain = 1; + ins.basenote = extractU32(0x14 - 8); + ins.detune = static_cast( // Q0,32 semitones to cents + (static_cast(extractU32(0x18 - 8)) * 100) >> 32); + ins.velocity_lo = 0; + ins.velocity_hi = 127; + ins.key_lo = 0; + ins.key_hi = 127; + + const uint32_t numLoops = std::min(16u, extractU32(0x24 - 8)); + ins.loop_count = numLoops; + + for (uint32_t i = 0; i < numLoops; ++i) { + const uint32_t loopOffset = 0x2c - 8 + i * 24; + + switch (extractU32(loopOffset + 0x04)) { + default: + ins.loops[i].mode = SF_LOOP_NONE; + break; + case 0: + ins.loops[i].mode = SF_LOOP_FORWARD; + break; + case 1: + ins.loops[i].mode = SF_LOOP_ALTERNATING; + break; + case 2: + ins.loops[i].mode = SF_LOOP_BACKWARD; + break; + } + + ins.loops[i].start = extractU32(loopOffset + 0x08); + ins.loops[i].end = extractU32(loopOffset + 0x0c) + 1; + ins.loops[i].count = extractU32(loopOffset + 0x14); + } + + return true; +} + +bool FileInstruments::extractFromFlac(const fs::path& path, SF_INSTRUMENT& ins) +{ + memset(&ins, 0, sizeof(SF_INSTRUMENT)); + +#if !defined(_WIN32) + FILE_u stream(fopen(path.native().c_str(), "rb")); +#else + FILE_u stream(_wfopen(path.wstring().c_str(), L"rb")); +#endif + + char magic[4]; + if (fread(magic, 4, 1, stream.get()) != 1 || memcmp(magic, "fLaC", 4) != 0) + return false; + + uint32_t header = 0; + while (((header >> 31) & 1) != 1) { + if (!fread_u32be(stream.get(), header)) + return false; + + const uint32_t block_type = (header >> 24) & 0x7f; + const uint32_t block_size = header & ((1 << 24) - 1); + + const off_t off_start_block = ftell(stream.get()); + const off_t off_next_block = off_start_block + block_size; + + if (block_type == 2) { // APPLICATION block + char blockId[4]; + char riffId[4]; + uint32_t riffChunkSize; + if (fread(blockId, 4, 1, stream.get()) == 1 && memcmp(blockId, "riff", 4) == 0 && + fread(riffId, 4, 1, stream.get()) == 1 && memcmp(riffId, "smpl", 4) == 0 && + fread_u32le(stream.get(), riffChunkSize) && riffChunkSize <= block_size - 12) + { + std::unique_ptr chunk { new uint8_t[riffChunkSize] }; + if (fread(chunk.get(), riffChunkSize, 1, stream.get()) == 1) + return extractSamplerChunkInstrument( + absl::MakeConstSpan(chunk.get(), riffChunkSize), ins); + } + } + + if (fseek(stream.get(), off_next_block, SEEK_SET) != 0) + return false; + } + + return false; +} + +} // namespace sfz diff --git a/src/sfizz/FileInstrument.h b/src/sfizz/FileInstrument.h new file mode 100644 index 00000000..1c75e988 --- /dev/null +++ b/src/sfizz/FileInstrument.h @@ -0,0 +1,24 @@ +// 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 "ghc/fs_std.hpp" +#include + +namespace sfz { + +class FileInstruments { +public: +/** + * @brief Extract the loop information of a FLAC file, using RIFF foreign data. + * + * This feature lacks support in libsndfile (as of version 1.0.28). + * see https://github.com/erikd/libsndfile/issues/59 + */ +static bool extractFromFlac(const fs::path& path, SF_INSTRUMENT& ins); +}; + +} // namespace sfz diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index b8e9ade4..5e61b2b7 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -24,6 +24,7 @@ // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "FilePool.h" +#include "FileInstrument.h" #include "Buffer.h" #include "AudioBuffer.h" #include "Config.h" @@ -217,9 +218,15 @@ absl::optional sfz::FilePool::getFileInformation(const Fil returnedValue.sampleRate = static_cast(sndFile.samplerate()); returnedValue.numChannels = sndFile.channels(); - if (!fileId.isReverse()) { - SF_INSTRUMENT instrumentInfo; + SF_INSTRUMENT instrumentInfo {}; + + const int sndFormat = sndFile.format(); + if ((sndFormat & SF_FORMAT_TYPEMASK) == SF_FORMAT_FLAC) + sfz::FileInstruments::extractFromFlac(file, instrumentInfo); + else sndFile.command(SFC_GET_INSTRUMENT, &instrumentInfo, sizeof(instrumentInfo)); + + if (!fileId.isReverse()) { if (instrumentInfo.loop_count > 0) { returnedValue.loopBegin = instrumentInfo.loops[0].start; returnedValue.loopEnd = min(returnedValue.end, instrumentInfo.loops[0].end - 1); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 345a052b..e0d54bc4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -76,4 +76,7 @@ target_link_libraries(sfizz_plot_curve PRIVATE sfizz::sfizz) add_executable(sfizz_plot_wavetables PlotWavetables.cpp) target_link_libraries(sfizz_plot_wavetables PRIVATE sfizz::sfizz) +add_executable(sfizz_file_instrument FileInstrument.cpp) +target_link_libraries(sfizz_file_instrument PRIVATE sfizz::sfizz) + file(COPY "." DESTINATION ${CMAKE_BINARY_DIR}/tests) diff --git a/tests/FileInstrument.cpp b/tests/FileInstrument.cpp new file mode 100644 index 00000000..b6e21c3d --- /dev/null +++ b/tests/FileInstrument.cpp @@ -0,0 +1,109 @@ +// 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 "sfizz/FileInstrument.h" +#include "absl/strings/string_view.h" +#include +#include + +static const char* modeString(int mode, const char* valueFallback = nullptr) +{ + switch (mode) { + case SF_LOOP_NONE: + return "none"; + case SF_LOOP_FORWARD: + return "forward"; + case SF_LOOP_BACKWARD: + return "backward"; + case SF_LOOP_ALTERNATING: + return "alternating"; + default: + return valueFallback; + } +} + +static void printInstrument(const SF_INSTRUMENT& ins) +{ + printf("Gain: %d\n", ins.gain); + printf("Base note: %d\n", ins.basenote); + printf("Detune: %d\n", ins.detune); + printf("Velocity: %d:%d\n", ins.velocity_lo, ins.velocity_hi); + printf("Key: %d:%d\n", ins.key_lo, ins.key_hi); + printf("Loop count: %d\n", ins.loop_count); + + for (int i = 0; i < ins.loop_count; ++i) { + printf("\nLoop %d:\n", i + 1); + printf("\tMode: %s\n", modeString(ins.loops[i].mode, "(unknown)")); + printf("\tStart: %u\n", ins.loops[i].start); + printf("\tEnd: %u\n", ins.loops[i].end); + printf("\tCount: %u\n", ins.loops[i].count); + } +} + +static void usage(const char* argv0) +{ + fprintf( + stderr, + "Usage: %s [-s|-f] \n" + " -s: extract the instrument using libsndfile\n" + " -f: extract the instrument using FLAC metadata\n", + argv0); +} + +enum FileMethod { + kMethodSndfile, + kMethodFlac, +}; + +int main(int argc, char *argv[]) +{ + fs::path path; + FileMethod method = kMethodSndfile; + + if (argc == 2) { + path = argv[1]; + } + else if (argc == 3) { + absl::string_view flag = argv[1]; + if (flag == "-s") + method = kMethodSndfile; + else if (flag == "-f") + method = kMethodFlac; + else { + usage(argv[0]); + return 1; + } + path = argv[2]; + } + else { + usage(argv[0]); + return 1; + } + + SF_INSTRUMENT ins {}; + + if (method == kMethodFlac) { + if (!sfz::FileInstruments::extractFromFlac(path, ins)) { + fprintf(stderr, "Cannot get instrument\n"); + return 1; + } + } + else { + SndfileHandle sndFile(path); + if (!sndFile) { + fprintf(stderr, "Cannot open file\n"); + return 1; + } + if (sndFile.command(SFC_GET_INSTRUMENT, &ins, sizeof(ins)) != 1) { + fprintf(stderr, "Cannot get instrument\n"); + return 1; + } + } + + printInstrument(ins); + + return 0; +}