diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ae4f493..8ed8a21 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -32,7 +32,7 @@ jobs: run: sudo apt-get install -y lcov - name: Configure - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -DVELOCILOOPS_COVERAGE=ON + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -DVELOCILOOPS_ENABLE_COVERAGE=ON - name: Build run: cmake --build build --parallel $(nproc) @@ -44,7 +44,7 @@ jobs: run: | lcov -c -d build \ --ignore-errors mismatch,unused,inconsistent,unsupported \ - --include "*/src/*" \ + --include "*/src/*.cpp" \ -o build/coverage.info - name: Check Coverage diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index fd3b05d..58a9ad4 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -34,7 +34,7 @@ jobs: -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \ -DCMAKE_C_COMPILER=clang \ -DCMAKE_CXX_COMPILER=clang++ \ - -DVELOCILOOPS_SANITIZERS=ON \ + -DVELOCILOOPS_ENABLE_SANITIZERS=ON \ -DVELOCILOOPS_ENABLE_FUZZING=ON - name: Build @@ -42,11 +42,12 @@ jobs: - name: Fuzz run: | - ./build-fuzz/tests/velociloops_fuzzer \ - tests/fuzz/ \ + ./build-fuzz/fuzz/velociloops_fuzzer \ -max_total_time=60 \ -max_len=980128 \ -rss_limit_mb=512 \ + -entropic=0 \ + fuzz/data/ \ tests/data/ env: ASAN_OPTIONS: halt_on_error=1:detect_leaks=0 diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml index 928a243..1fc66c3 100644 --- a/.github/workflows/sanitizers.yml +++ b/.github/workflows/sanitizers.yml @@ -30,13 +30,13 @@ jobs: run: | cmake -S . -B build \ -DCMAKE_BUILD_TYPE=Debug \ - -DVELOCILOOPS_SANITIZERS=ON + -DVELOCILOOPS_ENABLE_SANITIZERS=ON - name: Build - run: cmake --build build --parallel + run: cmake --build build --parallel $(nproc) - name: Test - run: ctest --test-dir build --output-on-failure + run: ctest --test-dir build --parallel $(nproc) --output-on-failure env: ASAN_OPTIONS: halt_on_error=1:detect_leaks=1 UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 23825a0..2c2b8c4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,7 +41,7 @@ jobs: run: cmake -S . -B build -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} - name: Build - run: cmake --build build --config ${{ env.BUILD_TYPE }} + run: cmake --build build --config ${{ env.BUILD_TYPE }} --parallel $(nproc) - name: Test - run: ctest --test-dir build --build-config ${{ env.BUILD_TYPE }} --output-on-failure + run: ctest --test-dir build --build-config ${{ env.BUILD_TYPE }} --parallel $(nproc) --output-on-failure diff --git a/.gitignore b/.gitignore index af5cd45..5259ca7 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ # Folders .venv/ +.gitnexus/ _build/ build*/ parking/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 891c952..69e67b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,14 +12,25 @@ option(VELOCILOOPS_ENABLE_SHARED "Build shared library" ON) option(VELOCILOOPS_ENABLE_TESTING "Build tests" ON) option(VELOCILOOPS_ENABLE_DEMO "Build demo" ON) +cmake_dependent_option(VELOCILOOPS_ENABLE_COVERAGE "Enable coverage" OFF + "VELOCILOOPS_ENABLE_TESTING;LCOV_EXECUTABLE;GENHTML_EXECUTABLE" OFF) + +cmake_dependent_option(VELOCILOOPS_ENABLE_FUZZING + "Build libFuzzer target (requires Clang with -fsanitize=fuzzer)" OFF + "VELOCILOOPS_ENABLE_TESTING" OFF) + +cmake_dependent_option(VELOCILOOPS_ENABLE_SANITIZERS + "Enable AddressSanitizer + UBSan (requires Clang or GCC)" OFF + "VELOCILOOPS_ENABLE_TESTING" OFF) + if (VELOCILOOPS_ENABLE_STATIC) - add_library(velociloops_static STATIC src/velociloops.cpp) + add_library(velociloops_static STATIC src/velociloops.cpp src/fft8g.h) target_include_directories(velociloops_static PUBLIC include) target_compile_features(velociloops_static PRIVATE cxx_std_17) endif() if (VELOCILOOPS_ENABLE_SHARED) - add_library(velociloops_shared SHARED src/velociloops.cpp) + add_library(velociloops_shared SHARED src/velociloops.cpp src/fft8g.h) target_include_directories(velociloops_shared PUBLIC include) target_compile_features(velociloops_shared PRIVATE cxx_std_17) endif() @@ -27,32 +38,25 @@ endif() if (PROJECT_IS_TOP_LEVEL) include(CTest) if (VELOCILOOPS_ENABLE_TESTING) - cmake_dependent_option(VELOCILOOPS_COVERAGE "Enable coverage" OFF - "VELOCILOOPS_ENABLE_TESTING;LCOV_EXECUTABLE;GENHTML_EXECUTABLE" OFF) - - if (VELOCILOOPS_COVERAGE) + if (VELOCILOOPS_ENABLE_COVERAGE) target_compile_options(velociloops_static PRIVATE --coverage) target_link_options(velociloops_static INTERFACE --coverage) endif() - cmake_dependent_option(VELOCILOOPS_SANITIZERS - "Enable AddressSanitizer + UBSan (requires Clang or GCC)" OFF - "VELOCILOOPS_ENABLE_TESTING" OFF) - - if (VELOCILOOPS_SANITIZERS) + if (VELOCILOOPS_ENABLE_SANITIZERS) target_compile_options(velociloops_static PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer -g) target_link_options(velociloops_static INTERFACE -fsanitize=address,undefined) endif() - cmake_dependent_option(VELOCILOOPS_ENABLE_FUZZING - "Build libFuzzer target (requires Clang with -fsanitize=fuzzer)" OFF - "VELOCILOOPS_ENABLE_TESTING" OFF) - add_subdirectory(tests) endif() + if (VELOCILOOPS_ENABLE_FUZZING) + add_subdirectory(fuzz) + endif() + if (VELOCILOOPS_ENABLE_DEMO) add_subdirectory(demo) endif() diff --git a/README.md b/README.md index 040962a..905a9d4 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ exposes per-slice float audio, and supports a full read/write round-trip. ### Authoring and mutation - **Create new RX2 files** — Build mono or stereo loops from caller-supplied float slices. +- **SuperFlux auto-slicing** — Build a save-ready loop from full-loop mono or stereo float PCM using onset detection. - **Set file and creator metadata before writing** — Configure tempo, original tempo, time signature, bit depth, transient settings, and creator fields before adding audio. - **Append and remove slices** — Add slices by PPQ position and remove existing slices by index. - **Round-trip by re-encoding** — Decode existing slices to float audio, mutate metadata/slices, and save a fresh RX2 file. @@ -80,6 +81,12 @@ executable. Extracts every slice as a WAV file and performs a save/reload round-trip check. +```sh +./build/demo/velociloops tests/data/120Stereo.wav out/120Stereo_auto.rx2 120 +``` + +Auto-slices a WAV file with SuperFlux onset detection and writes a REX2 file. + ### Embed in your project ```cmake diff --git a/demo/amen.wav b/demo/amen.wav new file mode 100644 index 0000000..696a8ee Binary files /dev/null and b/demo/amen.wav differ diff --git a/demo/main.cpp b/demo/main.cpp index 2330c52..82c76f3 100644 --- a/demo/main.cpp +++ b/demo/main.cpp @@ -1,34 +1,236 @@ #include "velociloops.h" -#include +#include +#include +#include #include +#include +#include #include +#include #include -#include +#include #include +#include /* ----------------------------------------------------------------------- - Minimal WAV writer + Minimal WAV reader/writer ----------------------------------------------------------------------- */ -static bool writeWav(const char* path, const float* left, const float* right, - int32_t frames, int32_t sampleRate) { - const int channels = right ? 2 : 1; +struct WavData +{ + int32_t channels = 0; + int32_t sampleRate = 0; + std::vector left; + std::vector right; +}; + +static uint16_t readLE16(const uint8_t* p) +{ + return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); +} + +static uint32_t readLE32(const uint8_t* p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +static int32_t readSignedLE(const uint8_t* p, uint16_t bits) +{ + if (bits == 8) + return (int32_t)p[0] - 128; + if (bits == 16) + return (int16_t)readLE16(p); + if (bits == 24) + { + int32_t v = (int32_t)p[0] | ((int32_t)p[1] << 8) | ((int32_t)p[2] << 16); + if (v & 0x800000) + v |= ~0x00ffffff; + return v; + } + if (bits == 32) + return (int32_t)readLE32(p); + return 0; +} + +static bool readWav(const char* path, WavData& out, std::string& error) +{ + std::ifstream in(path, std::ios::binary); + if (!in) + { + error = "failed to open file"; + return false; + } + + std::vector bytes((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + if (bytes.size() < 12 || std::memcmp(bytes.data(), "RIFF", 4) != 0 || std::memcmp(bytes.data() + 8, "WAVE", 4) != 0) + { + error = "not a RIFF/WAVE file"; + return false; + } + + uint16_t format = 0; + uint16_t channels = 0; + uint32_t sampleRate = 0; + uint16_t blockAlign = 0; + uint16_t bits = 0; + const uint8_t* data = nullptr; + uint32_t dataBytes = 0; + bool sawFmt = false; + + size_t off = 12; + while (off + 8 <= bytes.size()) + { + const uint8_t* chunk = bytes.data() + off; + const uint32_t size = readLE32(chunk + 4); + off += 8; + if (off + size > bytes.size()) + { + error = "truncated chunk"; + return false; + } + + if (std::memcmp(chunk, "fmt ", 4) == 0) + { + if (size < 16) + { + error = "short fmt chunk"; + return false; + } + + format = readLE16(bytes.data() + off); + channels = readLE16(bytes.data() + off + 2); + sampleRate = readLE32(bytes.data() + off + 4); + blockAlign = readLE16(bytes.data() + off + 12); + bits = readLE16(bytes.data() + off + 14); + + if (format == 0xfffe && size >= 40) + { + const uint16_t validBits = readLE16(bytes.data() + off + 18); + const uint16_t subFormat = readLE16(bytes.data() + off + 24); + format = subFormat; + if (validBits != 0) + bits = validBits; + } + sawFmt = true; + } + else if (std::memcmp(chunk, "data", 4) == 0) + { + data = bytes.data() + off; + dataBytes = size; + } + + off += size + (size & 1u); + } + + if (!sawFmt || !data) + { + error = "missing fmt or data chunk"; + return false; + } + if (channels != 1 && channels != 2) + { + error = "only mono and stereo WAV files are supported"; + return false; + } + if (sampleRate < 8000 || sampleRate > 192000) + { + error = "unsupported sample rate"; + return false; + } + if (format != 1 && format != 3) + { + error = "only PCM and IEEE float WAV files are supported"; + return false; + } + if (format == 1 && bits != 8 && bits != 16 && bits != 24 && bits != 32) + { + error = "unsupported PCM bit depth"; + return false; + } + if (format == 3 && bits != 32) + { + error = "only 32-bit float WAV files are supported"; + return false; + } + + const uint16_t sampleBytes = (uint16_t)((bits + 7) / 8); + const uint16_t expectedAlign = (uint16_t)(channels * sampleBytes); + if (blockAlign < expectedAlign || blockAlign == 0) + { + error = "invalid block alignment"; + return false; + } + + const uint32_t frames = dataBytes / blockAlign; + if (frames == 0) + { + error = "empty WAV data"; + return false; + } + + out.channels = channels; + out.sampleRate = (int32_t)sampleRate; + out.left.assign(frames, 0.0f); + out.right.assign(channels == 2 ? frames : 0, 0.0f); + + for (uint32_t frame = 0; frame < frames; ++frame) + { + const uint8_t* src = data + (size_t)frame * blockAlign; + for (uint16_t ch = 0; ch < channels; ++ch) + { + const uint8_t* samplePtr = src + (size_t)ch * sampleBytes; + float sample = 0.0f; + if (format == 3) + { + float value = 0.0f; + std::memcpy(&value, samplePtr, sizeof(value)); + sample = std::isfinite(value) ? value : 0.0f; + } + else + { + const int32_t value = readSignedLE(samplePtr, bits); + const float scale = bits == 8 ? 128.0f : (float)(1u << (bits - 1)); + sample = (float)value / scale; + } + + sample = std::clamp(sample, -1.0f, 1.0f); + if (ch == 0) + out.left[frame] = sample; + else + out.right[frame] = sample; + } + } + + return true; +} + +static bool writeWav(const char* path, const float* left, const float* right, int32_t frames, int32_t sampleRate) +{ + const int channels = right ? 2 : 1; const int byteDepth = 2; const int dataBytes = frames * channels * byteDepth; FILE* f = fopen(path, "wb"); - if (!f) return false; + if (!f) + return false; + + auto w16 = [&](uint16_t v) + { + fwrite(&v, 2, 1, f); + }; - auto w16 = [&](uint16_t v){ fwrite(&v, 2, 1, f); }; - auto w32 = [&](uint32_t v){ fwrite(&v, 4, 1, f); }; + auto w32 = [&](uint32_t v) + { + fwrite(&v, 4, 1, f); + }; fwrite("RIFF", 1, 4, f); w32((uint32_t)(36 + dataBytes)); fwrite("WAVE", 1, 4, f); fwrite("fmt ", 1, 4, f); w32(16); - w16(1); /* PCM */ + w16(1); /* PCM */ w16((uint16_t)channels); w32((uint32_t)sampleRate); w32((uint32_t)(sampleRate * channels * byteDepth)); @@ -37,14 +239,18 @@ static bool writeWav(const char* path, const float* left, const float* right, fwrite("data", 1, 4, f); w32((uint32_t)dataBytes); - for (int32_t i = 0; i < frames; ++i) { - auto toS16 = [](float s) -> int16_t { + for (int32_t i = 0; i < frames; ++i) + { + auto toS16 = [](float s) -> int16_t + { const float c = s < -1.f ? -1.f : (s > 1.f ? 1.f : s); return (int16_t)(c * 32767.f); }; + int16_t l = toS16(left[i]); fwrite(&l, 2, 1, f); - if (right) { + if (right) + { int16_t r = toS16(right[i]); fwrite(&r, 2, 1, f); } @@ -54,30 +260,162 @@ static bool writeWav(const char* path, const float* left, const float* right, return true; } +static std::string lowerExtension(const char* path) +{ + std::string ext = std::filesystem::path(path).extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) + { + return (char)std::tolower(c); + }); + return ext; +} + +static int convertWavToRx2(const char* inputPath, const char* outputPath, int32_t tempo) +{ + WavData wav; + std::string error; + if (!readWav(inputPath, wav, error)) + { + fprintf(stderr, "Failed to read WAV '%s': %s\n", inputPath, error.c_str()); + return 1; + } + + VLSuperFluxOptions options = {}; + vl_superflux_default_options(&options); + + VLError err = VL_OK; + VLFile file = vl_create_from_superflux(wav.channels, + wav.sampleRate, + tempo, + wav.left.data(), + wav.channels == 2 ? wav.right.data() : nullptr, + (int32_t)wav.left.size(), + &options, + &err); + if (!file) + { + fprintf(stderr, "Failed to slice WAV '%s': %s\n", inputPath, vl_error_string(err)); + return 1; + } + + VLFileInfo info = {}; + vl_get_info(file, &info); + + err = vl_save(file, outputPath); + if (err != VL_OK) + { + fprintf(stderr, "Failed to write RX2 '%s': %s\n", outputPath, vl_error_string(err)); + vl_close(file); + return 1; + } + + printf("=== %s ===\n", inputPath); + printf(" wrote : %s\n", outputPath); + printf(" channels : %d\n", info.channels); + printf(" sample rate : %d Hz\n", info.sample_rate); + printf(" total frames : %d\n", info.total_frames); + printf(" tempo : %.3f BPM\n", info.tempo / 1000.0); + printf(" ppq length : %d\n", info.ppq_length); + printf(" slices : %d\n", info.slice_count); + + for (int32_t i = 0; i < info.slice_count; ++i) + { + VLSliceInfo si = {}; + vl_get_slice_info(file, i, &si); + printf(" slice %-4d start=%-8d frames=%-8d ppq=%d\n", + i, si.sample_start, si.sample_length, si.ppq_pos); + } + + vl_close(file); + return 0; +} + /* ----------------------------------------------------------------------- Main ----------------------------------------------------------------------- */ -int main(int argc, char* argv[]) { - if (argc < 2) { - fprintf(stderr, "Usage: %s [output_dir]\n", argv[0]); +int main(int argc, char* argv[]) +{ + if (argc < 2) + { + fprintf(stderr, "Usage:\n"); + fprintf(stderr, " %s [output_dir]\n", argv[0]); + fprintf(stderr, " %s [output.rx2|output_dir] [tempo_bpm]\n", argv[0]); return 1; } const char* inputPath = argv[1]; - const char* outDir = argc >= 3 ? argv[2] : "."; + if (lowerExtension(inputPath) == ".wav") + { + double bpm = 120.0; + if (argc >= 4) + { + char* end = nullptr; + bpm = std::strtod(argv[3], &end); + if (!end || *end != '\0' || bpm <= 0.0) + { + fprintf(stderr, "Invalid tempo '%s'. Use BPM, e.g. 120 or 87.5.\n", argv[3]); + return 1; + } + } + + std::filesystem::path outputPath; + if (argc >= 3) + { + outputPath = argv[2]; + std::error_code dirEc; + if (std::filesystem::is_directory(outputPath, dirEc)) + { + std::filesystem::path filename = std::filesystem::path(inputPath).stem(); + filename.replace_extension(".rx2"); + outputPath /= filename; + } + } + else + { + outputPath = std::filesystem::path(inputPath).replace_extension(".rx2"); + } + + if (outputPath.empty()) + { + fprintf(stderr, "Invalid output path.\n"); + return 1; + } + + if (outputPath.extension().empty()) + outputPath.replace_extension(".rx2"); + + if (!outputPath.parent_path().empty()) + { + std::error_code ec; + std::filesystem::create_directories(outputPath.parent_path(), ec); + if (ec) + { + fprintf(stderr, "Failed to create output directory '%s': %s\n", + outputPath.parent_path().string().c_str(), ec.message().c_str()); + return 1; + } + } + + const int32_t tempo = (int32_t)std::lround(bpm * 1000.0); + return convertWavToRx2(inputPath, outputPath.string().c_str(), tempo); + } + + const char* outDir = argc >= 3 ? argv[2] : "."; std::error_code ec; std::filesystem::create_directories(outDir, ec); - if (ec) { - fprintf(stderr, "Failed to create output directory '%s': %s\n", - outDir, ec.message().c_str()); + if (ec) + { + fprintf(stderr, "Failed to create output directory '%s': %s\n", outDir, ec.message().c_str()); return 1; } /* --- open --- */ VLError err = VL_OK; - VLFile file = vl_open(inputPath, &err); - if (!file) { + VLFile file = vl_open(inputPath, &err); + if (!file) + { fprintf(stderr, "Failed to open '%s': %s\n", inputPath, vl_error_string(err)); return 1; } @@ -87,46 +425,47 @@ int main(int argc, char* argv[]) { vl_get_info(file, &info); printf("=== %s ===\n", inputPath); - printf(" channels : %d\n", info.channels); + printf(" channels : %d\n", info.channels); printf(" sample rate : %d Hz\n", info.sample_rate); - printf(" bit depth : %d\n", info.bit_depth); - printf(" total frames : %d\n", info.total_frames); - printf(" loop start : %d\n", info.loop_start); - printf(" loop end : %d\n", info.loop_end); + printf(" bit depth : %d\n", info.bit_depth); + printf(" total frames : %d\n", info.total_frames); + printf(" loop start : %d\n", info.loop_start); + printf(" loop end : %d\n", info.loop_end); printf(" tempo : %.3f BPM\n", info.tempo / 1000.0); printf(" orig. tempo : %.3f BPM\n", info.original_tempo / 1000.0); printf(" time sig : %d/%d\n", info.time_sig_num, info.time_sig_den); - printf(" ppq length : %d\n", info.ppq_length); - printf(" slices : %d\n", info.slice_count); - printf(" gain : %d\n", info.processing_gain); - printf(" transient : %s attack=%d decay=%d stretch=%d\n\n", - info.transient_enabled ? "on" : "off", - info.transient_attack, - info.transient_decay, + printf(" ppq length : %d\n", info.ppq_length); + printf(" slices : %d\n", info.slice_count); + printf(" gain : %d\n", info.processing_gain); + printf(" transient : %s attack=%d decay=%d stretch=%d\n\n", info.transient_enabled ? "on" : "off", info.transient_attack, info.transient_decay, info.transient_stretch); VLCreatorInfo creator = {}; - if (vl_get_creator_info(file, &creator) == VL_OK) { - if (creator.name[0]) printf(" creator name : %s\n", creator.name); - if (creator.copyright[0]) printf(" copyright : %s\n", creator.copyright); - if (creator.url[0]) printf(" url : %s\n", creator.url); - if (creator.email[0]) printf(" email : %s\n", creator.email); - if (creator.free_text[0]) printf(" free text : %s\n", creator.free_text); + if (vl_get_creator_info(file, &creator) == VL_OK) + { + if (creator.name[0]) + printf(" creator name : %s\n", creator.name); + if (creator.copyright[0]) + printf(" copyright : %s\n", creator.copyright); + if (creator.url[0]) + printf(" url : %s\n", creator.url); + if (creator.email[0]) + printf(" email : %s\n", creator.email); + if (creator.free_text[0]) + printf(" free text : %s\n", creator.free_text); printf("\n"); } /* --- slices --- */ - printf(" %-6s %-10s %-10s %-10s %-10s\n", - "slice", "ppq_pos", "smp_start", "smp_len", "out_frames"); - printf(" %-6s %-10s %-10s %-10s %-10s\n", - "------", "----------", "----------", "----------", "----------"); + printf(" %-6s %-10s %-10s %-10s %-10s\n", "slice", "ppq_pos", "smp_start", "smp_len", "out_frames"); + printf(" %-6s %-10s %-10s %-10s %-10s\n", "------", "----------", "----------", "----------", "----------"); - for (int32_t i = 0; i < info.slice_count; ++i) { + for (int32_t i = 0; i < info.slice_count; ++i) + { VLSliceInfo si; vl_get_slice_info(file, i, &si); const int32_t frames = vl_get_slice_frame_count(file, i); - printf(" %-6d %-10d %-10d %-10d %-10d\n", - i, si.ppq_pos, si.sample_start, si.sample_length, frames); + printf(" %-6d %-10d %-10d %-10d %-10d\n", i, si.ppq_pos, si.sample_start, si.sample_length, frames); } printf("\n"); @@ -134,9 +473,14 @@ int main(int argc, char* argv[]) { const bool stereo = info.channels >= 2; int exported = 0, failed = 0; - for (int32_t i = 0; i < info.slice_count; ++i) { + for (int32_t i = 0; i < info.slice_count; ++i) + { const int32_t frames = vl_get_slice_frame_count(file, i); - if (frames <= 0) { ++failed; continue; } + if (frames <= 0) + { + ++failed; + continue; + } std::vector lbuf((size_t)frames); std::vector rbuf((size_t)frames); @@ -144,7 +488,8 @@ int main(int argc, char* argv[]) { float* rptr = stereo ? rbuf.data() : nullptr; int32_t written = 0; err = vl_decode_slice(file, i, lbuf.data(), rptr, 0, frames, &written); - if (err != VL_OK) { + if (err != VL_OK) + { fprintf(stderr, " slice %d: decode error: %s\n", i, vl_error_string(err)); ++failed; continue; @@ -153,7 +498,8 @@ int main(int argc, char* argv[]) { char path[512]; std::snprintf(path, sizeof(path), "%s/slice_%03d.wav", outDir, i); - if (!writeWav(path, lbuf.data(), rptr, written, info.sample_rate)) { + if (!writeWav(path, lbuf.data(), rptr, written, info.sample_rate)) + { fprintf(stderr, " slice %d: failed to write '%s'\n", i, path); ++failed; continue; @@ -164,38 +510,38 @@ int main(int argc, char* argv[]) { } printf("\n %d/%d slices exported", exported, info.slice_count); - if (failed) printf(", %d failed", failed); + if (failed) + printf(", %d failed", failed); printf("\n"); - if (!failed) { - const std::filesystem::path roundtripPath = - std::filesystem::path(outDir) / "roundtrip.rx2"; + if (!failed) + { + const std::filesystem::path roundtripPath = std::filesystem::path(outDir) / "roundtrip.rx2"; VLError roundtripErr = vl_save(file, roundtripPath.string().c_str()); - if (roundtripErr != VL_OK) { - fprintf(stderr, "Failed to write roundtrip RX2 '%s': %s\n", - roundtripPath.string().c_str(), vl_error_string(roundtripErr)); + if (roundtripErr != VL_OK) + { + fprintf(stderr, "Failed to write roundtrip RX2 '%s': %s\n", roundtripPath.string().c_str(), vl_error_string(roundtripErr)); failed = 1; - } else { + } + else + { printf(" wrote %s\n", roundtripPath.string().c_str()); VLError reopenErr = VL_OK; VLFile reopened = vl_open(roundtripPath.string().c_str(), &reopenErr); - if (!reopened) { + if (!reopened) + { fprintf(stderr, "Failed to reopen roundtrip RX2: %s\n", vl_error_string(reopenErr)); failed = 1; - } else { + } + else + { VLFileInfo rtInfo; vl_get_info(reopened, &rtInfo); - printf(" roundtrip reopen: %d/%d slices, %d/%d frames, %.3f BPM, %d channel%s\n", - rtInfo.slice_count, - info.slice_count, - rtInfo.total_frames, - info.total_frames, - rtInfo.tempo / 1000.0, - rtInfo.channels, - rtInfo.channels == 1 ? "" : "s"); - if (rtInfo.slice_count != info.slice_count || - rtInfo.total_frames != info.total_frames) { + printf(" roundtrip reopen: %d/%d slices, %d/%d frames, %.3f BPM, %d channel%s\n", rtInfo.slice_count, info.slice_count, rtInfo.total_frames, + info.total_frames, rtInfo.tempo / 1000.0, rtInfo.channels, rtInfo.channels == 1 ? "" : "s"); + if (rtInfo.slice_count != info.slice_count || rtInfo.total_frames != info.total_frames) + { fprintf(stderr, "Roundtrip metadata mismatch for '%s'\n", inputPath); failed = 1; } diff --git a/docs/api.md b/docs/api.md index 648a53e..8ace50a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -124,6 +124,39 @@ Optional creator and tag metadata from the REX2 CREI chunk. Absent fields are returned as empty strings (`""`). +### `VLSuperFluxOptions` + +```c +typedef struct { /* fields omitted */ } VLSuperFluxOptions; +``` + +Tunable parameters for `vl_create_from_superflux()`. Initialize with +`vl_superflux_default_options()` and then override individual fields as needed. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `frame_size` | `int32_t` | 2048 | FFT/window size. Must be a power of two. | +| `fps` | `int32_t` | 200 | Onset detection frames per second. | +| `filter_bands` | `int32_t` | 24 | Log-frequency filter bands per octave. | +| `max_bins` | `int32_t` | 3 | Frequency bins for SuperFlux maximum filtering. | +| `diff_frames` | `int32_t` | 0 | Previous-frame distance; `<= 0` derives it from `ratio`. | +| `min_slice_frames` | `int32_t` | 0 | Minimum samples between slice starts; `<= 0` uses 10 ms. | +| `filter_equal` | `int32_t` | 0 | Non-zero normalizes triangular filter areas. | +| `online` | `int32_t` | 0 | Non-zero uses causal framing and peak picking. | +| `threshold` | `float` | 1.1 | Peak-picking threshold above the local average. | +| `combine_ms` | `float` | 30 | Suppress detections within this many milliseconds. | +| `pre_avg` | `float` | 0.15 | Seconds before peak for moving average. | +| `pre_max` | `float` | 0.01 | Seconds before peak for moving maximum. | +| `post_avg` | `float` | 0 | Seconds after peak for moving average. | +| `post_max` | `float` | 0.05 | Seconds after peak for moving maximum. | +| `delay_ms` | `float` | 0 | Detection timestamp offset in milliseconds. | +| `ratio` | `float` | 0.5 | Window ratio used to derive `diff_frames`. | +| `fmin` | `float` | 30 | Filterbank minimum frequency in Hz. | +| `fmax` | `float` | 17000 | Filterbank maximum frequency in Hz. | +| `log_mul` | `float` | 1 | Magnitude multiplier before `log10`. | +| `log_add` | `float` | 1 | Positive value added before `log10`. | + + ## Functions ### Open / Close @@ -201,6 +234,50 @@ first `vl_add_slice()` call; attempts to change metadata afterwards return **Returns** A valid `VLFile` on success, or `NULL` on failure. +#### `vl_superflux_default_options` + +```c +void vl_superflux_default_options(VLSuperFluxOptions* out); +``` + +Fill a caller-allocated `VLSuperFluxOptions` struct with the defaults used by +`vl_create_from_superflux()`. Passing `NULL` is allowed and does nothing. + + +#### `vl_create_from_superflux` + +```c +VLFile vl_create_from_superflux(int32_t channels, + int32_t sample_rate, + int32_t tempo, + const float* left, + const float* right, + int32_t frames, + const VLSuperFluxOptions* options, + VLError* err); +``` + +Create a new authoring handle from a complete mono or stereo loop. Stereo input +is downmixed for SuperFlux onset detection, while the original left/right +buffers are copied into the resulting slices. The returned handle can be saved +with `vl_save()` or `vl_save_to_memory()`. + +**Parameters** + +| Name | Description | +|------|-------------| +| `channels` | 1 (mono) or 2 (stereo). | +| `sample_rate` | Input/output sample rate in Hz. | +| `tempo` | Playback tempo in BPM × 1000. | +| `left` | Left or mono buffer containing `frames` float samples. | +| `right` | Right buffer for stereo. Must be non-NULL when `channels == 2`. | +| `frames` | Number of PCM frames in each input buffer. | +| `options` | SuperFlux options, or NULL for defaults. | +| `err` | Out-parameter for the status code. May be NULL. | + +**Returns** A valid `VLFile` on success, or `NULL` on failure. + + #### `vl_close` ```c @@ -290,20 +367,6 @@ and performs file loading, slice creation, saving, and buffer allocation outside the audio callback. -#### `vl_set_output_sample_rate` - -```c -VLError vl_set_output_sample_rate(VLFile file, int32_t rate); -``` - -Set the output sample rate for `vl_decode_slice()`. - -Resampling is not yet implemented. Returns `VL_ERROR_NOT_IMPLEMENTED` if -`rate` differs from the file's native rate. - -**Returns** `VL_OK` when `rate` matches the native rate. - - #### `vl_get_slice_frame_count` ```c @@ -486,6 +549,6 @@ freed. Unknown codes return `"unknown error"`. const char* vl_version_string(void); ``` -Return the library version string (e.g. `"velociloops 0.1.0"`). +Return the library version string (e.g. `"0.1.0"`). The returned pointer is valid for the lifetime of the process. diff --git a/docs/index.md b/docs/index.md index 5c66ac0..52b12a6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,10 +50,12 @@ target_link_libraries(my_app PRIVATE velociloops_library) #include #include -int main(void) { +int main(void) +{ VLError err; VLFile file = vl_open("loop.rx2", &err); - if (!file) { + if (!file) + { fprintf(stderr, "open failed: %s\n", vl_error_string(err)); return 1; } @@ -66,7 +68,8 @@ int main(void) { info.tempo / 1000.0, info.slice_count); /* Decode each slice to float buffers */ - for (int i = 0; i < info.slice_count; ++i) { + for (int i = 0; i < info.slice_count; ++i) + { int32_t n = vl_get_slice_frame_count(file, i); float *L = malloc(n * sizeof(float)); float *R = malloc(n * sizeof(float)); /* NULL for mono-only */ @@ -95,7 +98,8 @@ int main(void) { #include "velociloops.h" /* Assume `left` and `right` are float[frame_count] filled with audio data. */ -void write_example(const float* left, const float* right, int32_t frame_count) { +void write_example(const float* left, const float* right, int32_t frame_count) +{ /* 1. Create handle: stereo, 44.1 kHz, 120 BPM */ VLError err; VLFile file = vl_create_new(2, 44100, 120000, &err); @@ -110,7 +114,8 @@ void write_example(const float* left, const float* right, int32_t frame_count) { /* 3. Add slices in ascending ppq_pos order */ /* ppq_pos=0 places the first slice at the very start of the loop */ int32_t idx = vl_add_slice(file, 0, left, right, frame_count); - if (idx < 0) { + if (idx < 0) + { vl_close(file); return; } @@ -134,7 +139,8 @@ optionally writing the error code to an `err` out-parameter. ```c VLError err; VLFile f = vl_open("loop.rx2", &err); -if (!f) { +if (!f) +{ /* vl_error_string() never returns NULL */ fprintf(stderr, "error %d: %s\n", (int)err, vl_error_string(err)); return; @@ -142,9 +148,8 @@ if (!f) { VLFileInfo info; VLError e = vl_get_info(f, &info); -if (e != VL_OK) { +if (e != VL_OK) fprintf(stderr, "get_info: %s\n", vl_error_string(e)); -} vl_close(f); ``` diff --git a/fuzz/CMakeLists.txt b/fuzz/CMakeLists.txt new file mode 100644 index 0000000..a0396ed --- /dev/null +++ b/fuzz/CMakeLists.txt @@ -0,0 +1,11 @@ +add_executable(velociloops_fuzzer fuzz_velociloops.cpp) + +target_link_libraries(velociloops_fuzzer PRIVATE velociloops_static) + +target_compile_features(velociloops_fuzzer PRIVATE cxx_std_17) + +target_compile_options(velociloops_fuzzer PRIVATE + -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer -g) + +target_link_options(velociloops_fuzzer PRIVATE + -fsanitize=fuzzer,address,undefined) diff --git a/tests/fuzz/.gitignore b/fuzz/data/.gitignore similarity index 100% rename from tests/fuzz/.gitignore rename to fuzz/data/.gitignore diff --git a/tests/fuzz_velociloops.cpp b/fuzz/fuzz_velociloops.cpp similarity index 100% rename from tests/fuzz_velociloops.cpp rename to fuzz/fuzz_velociloops.cpp diff --git a/include/velociloops.h b/include/velociloops.h index 3b67616..4a91420 100644 --- a/include/velociloops.h +++ b/include/velociloops.h @@ -305,8 +305,42 @@ typedef struct char free_text[256]; /**< Arbitrary free-form text. */ } VLCreatorInfo; +/** + * @brief Tunable parameters for vl_create_from_superflux(). + * + * Initialize with vl_superflux_default_options() before changing individual + * fields. Pass NULL to vl_create_from_superflux() to use the same defaults. + * + * Time fields are seconds except @c combine_ms and @c delay_ms, which are + * milliseconds to match common onset-picking terminology. + */ +typedef struct +{ + int32_t frame_size; /**< FFT/window size in samples. Must be a power of two. Default: 2048. */ + int32_t fps; /**< Onset detection frames per second. Default: 200. */ + int32_t filter_bands; /**< Log-frequency filter bands per octave. Default: 24. */ + int32_t max_bins; /**< Frequency bins for SuperFlux maximum filtering. Default: 3. */ + int32_t diff_frames; /**< Previous-frame distance; <= 0 derives it from @c ratio. */ + int32_t min_slice_frames; /**< Minimum frames between slice starts. <= 0 uses 10 ms. */ + int32_t filter_equal; /**< Non-zero normalizes each triangular filter area. */ + int32_t online; /**< Non-zero uses causal framing and peak picking. */ + + float threshold; /**< Peak-picking threshold over local average. Default: 1.1. */ + float combine_ms; /**< Suppress detections within this many ms. Default: 50. */ + float pre_avg; /**< Seconds before the peak for moving average. Default: 0.15. */ + float pre_max; /**< Seconds before the peak for moving maximum. Default: 0.01. */ + float post_avg; /**< Seconds after the peak for moving average. Default: 0.0. */ + float post_max; /**< Seconds after the peak for moving maximum. Default: 0.05. */ + float delay_ms; /**< Detection timestamp offset in ms. Default: 0. */ + float ratio; /**< Window ratio used to derive @c diff_frames. Default: 0.5. */ + float fmin; /**< Filterbank minimum frequency in Hz. Default: 30. */ + float fmax; /**< Filterbank maximum frequency in Hz. Default: 17000. */ + float log_mul; /**< Magnitude multiplier before log10. Default: 1. */ + float log_add; /**< Positive value added before log10. Default: 1. */ +} VLSuperFluxOptions; + /* ----------------------------------------------------------------------- - Open / close + Open and decode existing files ----------------------------------------------------------------------- */ /** @@ -338,6 +372,10 @@ VLFile vl_open(const char* path, VLError* err); */ VLFile vl_open_from_memory(const void* data, size_t size, VLError* err); +/* ----------------------------------------------------------------------- + Create empty file handle + ----------------------------------------------------------------------- */ + /** * @brief Create a new, empty file handle for assembling a REX2 loop. * @@ -354,6 +392,50 @@ VLFile vl_open_from_memory(const void* data, size_t size, VLError* err); */ VLFile vl_create_new(int32_t channels, int32_t sample_rate, int32_t tempo, VLError* err); +/* ----------------------------------------------------------------------- + Create from onset detection + ----------------------------------------------------------------------- */ + +/** + * @brief Fill a VLSuperFluxOptions struct with the library defaults. + * + * @param out Pointer to caller-allocated options. NULL is ignored. + */ +void vl_superflux_default_options(VLSuperFluxOptions* out); + +/** + * @brief Create a sliced REX2 authoring handle from full-loop float PCM. + * + * The input is a complete mono or stereo loop as non-interleaved float buffers. + * VelociLoops downmixes stereo to mono for SuperFlux onset detection, then + * copies the original channel data into contiguous slices. The returned handle + * is equivalent to one assembled with vl_create_new() and vl_add_slice(), and + * can be passed directly to vl_save() or vl_save_to_memory(). + * + * @param channels 1 (mono) or 2 (stereo). + * @param sample_rate Input/output sample rate in Hz. + * @param tempo Playback tempo in BPM × 1000. + * @param left Left or mono input buffer, @p frames samples. + * @param right Right input buffer for stereo; must be non-NULL when + * @p channels is 2. Ignored for mono. + * @param frames Number of PCM frames in each input buffer. + * @param options SuperFlux parameters, or NULL for defaults. + * @param err Receives the status code; may be NULL. + * @return A valid VLFile handle ready to save, or NULL on failure. + */ +VLFile vl_create_from_superflux(int32_t channels, + int32_t sample_rate, + int32_t tempo, + const float* left, + const float* right, + int32_t frames, + const VLSuperFluxOptions* options, + VLError* err); + +/* ----------------------------------------------------------------------- + Close + ----------------------------------------------------------------------- */ + /** * @brief Release all resources associated with a VLFile handle. * @@ -427,21 +509,6 @@ VLError vl_set_slice_info(VLFile file, int32_t index, int32_t flags, int32_t ana Read: sample extraction ----------------------------------------------------------------------- */ -/** - * @brief Set the output sample rate for subsequent vl_decode_slice() calls. - * - * Stores @p rate for future use. Resampling is not yet implemented; calling - * this function with a rate that differs from the file's native rate currently - * returns VL_ERROR_NOT_IMPLEMENTED. - * - * @param file Open VLFile handle. - * @param rate Desired output sample rate in Hz. - * @return VL_OK if @p rate matches the file's native rate. - * VL_ERROR_NOT_IMPLEMENTED for any other non-zero rate. - * VL_ERROR_INVALID_ARG if @p rate <= 0. - */ -VLError vl_set_output_sample_rate(VLFile file, int32_t rate); - /** * @brief Return the number of frames vl_decode_slice() will write for a slice. * @@ -616,7 +683,7 @@ const char* vl_error_string(VLError err); /** * @brief Return the library version string. * - * Format: "velociloops MAJOR.MINOR.PATCH" (e.g. "velociloops 0.1.0"). + * Format: "MAJOR.MINOR.PATCH" (e.g. "1.2.8"). * The returned pointer is valid for the lifetime of the process. * * @return NUL-terminated ASCII version string. diff --git a/justfile b/justfile index d11001f..2a4c65e 100644 --- a/justfile +++ b/justfile @@ -14,48 +14,47 @@ open: generate -open build/VelociLoops.xcodeproj test: build - ctest --test-dir build -C Debug --output-on-failure + ctest --test-dir build -C Debug --output-on-failure --extra-verbose --parallel $(nproc) run: build ./build/demo/Debug/velociloops tests/data/120Stereo.rx2 coverage: - cmake -S . -B build-coverage -DCMAKE_BUILD_TYPE=Debug -DVELOCILOOPS_COVERAGE=ON + cmake -S . -B build-coverage -DCMAKE_BUILD_TYPE=Debug -DVELOCILOOPS_ENABLE_COVERAGE=ON cmake --build build-coverage --parallel $(nproc) - ctest --test-dir build-coverage --output-on-failure + ctest --test-dir build-coverage --output-on-failure --extra-verbose lcov -c -d build-coverage \ --ignore-errors mismatch,unused,inconsistent,unsupported \ - --include "$(pwd)/include/*" \ - --include "$(pwd)/src/*" \ + --include "*/src/*.cpp" \ -o build-coverage/coverage.info lcov --summary build-coverage/coverage.info --ignore-errors inconsistent,unsupported genhtml build-coverage/coverage.info --ignore-errors category,inconsistent,unsupported -o build-coverage/coverage_html open build-coverage/coverage_html/index.html sanitize: - cmake -S . -B build-sanitize -DCMAKE_BUILD_TYPE=Debug -DVELOCILOOPS_SANITIZERS=ON + cmake -S . -B build-sanitize -DCMAKE_BUILD_TYPE=Debug -DVELOCILOOPS_ENABLE_SANITIZERS=ON cmake --build build-sanitize --parallel $(nproc) - ctest --test-dir build-sanitize --output-on-failure + ctest --test-dir build-sanitize --output-on-failure --extra-verbose fuzz: cmake -S . -B build-fuzz -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang \ -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ \ - -DVELOCILOOPS_SANITIZERS=ON \ + -DVELOCILOOPS_ENABLE_SANITIZERS=ON \ -DVELOCILOOPS_ENABLE_FUZZING=ON cmake --build build-fuzz --parallel $(nproc) --target velociloops_fuzzer ASAN_OPTIONS=halt_on_error=1:detect_leaks=0 \ UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \ - ./build-fuzz/tests/velociloops_fuzzer \ - tests/fuzz/ -max_total_time=30 -max_len=980128 -rss_limit_mb=512 tests/data/ + ./build-fuzz/fuzz/velociloops_fuzzer \ + -max_total_time=30 -max_len=980128 -rss_limit_mb=512 -entropic=0 fuzz/data/ tests/data/ format: clang-format --style=file -i include/*.h src/*.cpp tests/*.cpp -visualize: generate +visualize RX="tests/data/120Stereo.rx2": generate uv venv --allow-existing uv pip install --requirement scripts/requirements.txt - uv run scripts/visualize_rx2.py + uv run scripts/visualize_rx2.py {{RX}} bump: perl -0pi -e 's/x=(\d+)/"x=" . ($1 + 1)/ge' README.md diff --git a/src/fft8g.h b/src/fft8g.h new file mode 100644 index 0000000..14957bc --- /dev/null +++ b/src/fft8g.h @@ -0,0 +1,1605 @@ +/* +Fast Fourier/Cosine/Sine Transform + dimension :one + data length :power of 2 + decimation :frequency + radix :8, 4, 2 + data :inplace + table :use +functions + cdft: Complex Discrete Fourier Transform + rdft: Real Discrete Fourier Transform + ddct: Discrete Cosine Transform + ddst: Discrete Sine Transform + dfct: Cosine Transform of RDFT (Real Symmetric DFT) + dfst: Sine Transform of RDFT (Real Anti-symmetric DFT) +function prototypes + void cdft(int, int, double *, int *, double *); + void rdft(int, int, double *, int *, double *); + void ddct(int, int, double *, int *, double *); + void ddst(int, int, double *, int *, double *); + void dfct(int, double *, double *, int *, double *); + void dfst(int, double *, double *, int *, double *); + + +-------- Complex DFT (Discrete Fourier Transform) -------- + [definition] + + X[k] = sum_j=0^n-1 x[j]*exp(2*pi*i*j*k/n), 0<=k + X[k] = sum_j=0^n-1 x[j]*exp(-2*pi*i*j*k/n), 0<=k + ip[0] = 0; // first time only + cdft(2*n, 1, a, ip, w); + + ip[0] = 0; // first time only + cdft(2*n, -1, a, ip, w); + [parameters] + 2*n :data length (int) + n >= 1, n = power of 2 + a[0...2*n-1] :input/output data (double *) + input data + a[2*j] = Re(x[j]), + a[2*j+1] = Im(x[j]), 0<=j= 2+sqrt(n) + strictly, + length of ip >= + 2+(1<<(int)(log(n+0.5)/log(2))/2). + ip[0],ip[1] are pointers of the cos/sin table. + w[0...n/2-1] :cos/sin table (double *) + w[],ip[] are initialized if ip[0] == 0. + [remark] + Inverse of + cdft(2*n, -1, a, ip, w); + is + cdft(2*n, 1, a, ip, w); + for (j = 0; j <= 2 * n - 1; j++) { + a[j] *= 1.0 / n; + } + . + + +-------- Real DFT / Inverse of Real DFT -------- + [definition] + RDFT + R[k] = sum_j=0^n-1 a[j]*cos(2*pi*j*k/n), 0<=k<=n/2 + I[k] = sum_j=0^n-1 a[j]*sin(2*pi*j*k/n), 0 IRDFT (excluding scale) + a[k] = (R[0] + R[n/2]*cos(pi*k))/2 + + sum_j=1^n/2-1 R[j]*cos(2*pi*j*k/n) + + sum_j=1^n/2-1 I[j]*sin(2*pi*j*k/n), 0<=k + ip[0] = 0; // first time only + rdft(n, 1, a, ip, w); + + ip[0] = 0; // first time only + rdft(n, -1, a, ip, w); + [parameters] + n :data length (int) + n >= 2, n = power of 2 + a[0...n-1] :input/output data (double *) + + output data + a[2*k] = R[k], 0<=k + input data + a[2*j] = R[j], 0<=j= 2+sqrt(n/2) + strictly, + length of ip >= + 2+(1<<(int)(log(n/2+0.5)/log(2))/2). + ip[0],ip[1] are pointers of the cos/sin table. + w[0...n/2-1] :cos/sin table (double *) + w[],ip[] are initialized if ip[0] == 0. + [remark] + Inverse of + rdft(n, 1, a, ip, w); + is + rdft(n, -1, a, ip, w); + for (j = 0; j <= n - 1; j++) { + a[j] *= 2.0 / n; + } + . + + +-------- DCT (Discrete Cosine Transform) / Inverse of DCT -------- + [definition] + IDCT (excluding scale) + C[k] = sum_j=0^n-1 a[j]*cos(pi*j*(k+1/2)/n), 0<=k DCT + C[k] = sum_j=0^n-1 a[j]*cos(pi*(j+1/2)*k/n), 0<=k + ip[0] = 0; // first time only + ddct(n, 1, a, ip, w); + + ip[0] = 0; // first time only + ddct(n, -1, a, ip, w); + [parameters] + n :data length (int) + n >= 2, n = power of 2 + a[0...n-1] :input/output data (double *) + output data + a[k] = C[k], 0<=k= 2+sqrt(n/2) + strictly, + length of ip >= + 2+(1<<(int)(log(n/2+0.5)/log(2))/2). + ip[0],ip[1] are pointers of the cos/sin table. + w[0...n*5/4-1] :cos/sin table (double *) + w[],ip[] are initialized if ip[0] == 0. + [remark] + Inverse of + ddct(n, -1, a, ip, w); + is + a[0] *= 0.5; + ddct(n, 1, a, ip, w); + for (j = 0; j <= n - 1; j++) { + a[j] *= 2.0 / n; + } + . + + +-------- DST (Discrete Sine Transform) / Inverse of DST -------- + [definition] + IDST (excluding scale) + S[k] = sum_j=1^n A[j]*sin(pi*j*(k+1/2)/n), 0<=k DST + S[k] = sum_j=0^n-1 a[j]*sin(pi*(j+1/2)*k/n), 0 + ip[0] = 0; // first time only + ddst(n, 1, a, ip, w); + + ip[0] = 0; // first time only + ddst(n, -1, a, ip, w); + [parameters] + n :data length (int) + n >= 2, n = power of 2 + a[0...n-1] :input/output data (double *) + + input data + a[j] = A[j], 0 + output data + a[k] = S[k], 0= 2+sqrt(n/2) + strictly, + length of ip >= + 2+(1<<(int)(log(n/2+0.5)/log(2))/2). + ip[0],ip[1] are pointers of the cos/sin table. + w[0...n*5/4-1] :cos/sin table (double *) + w[],ip[] are initialized if ip[0] == 0. + [remark] + Inverse of + ddst(n, -1, a, ip, w); + is + a[0] *= 0.5; + ddst(n, 1, a, ip, w); + for (j = 0; j <= n - 1; j++) { + a[j] *= 2.0 / n; + } + . + + +-------- Cosine Transform of RDFT (Real Symmetric DFT) -------- + [definition] + C[k] = sum_j=0^n a[j]*cos(pi*j*k/n), 0<=k<=n + [usage] + ip[0] = 0; // first time only + dfct(n, a, t, ip, w); + [parameters] + n :data length - 1 (int) + n >= 2, n = power of 2 + a[0...n] :input/output data (double *) + output data + a[k] = C[k], 0<=k<=n + t[0...n/2] :work area (double *) + ip[0...*] :work area for bit reversal (int *) + length of ip >= 2+sqrt(n/4) + strictly, + length of ip >= + 2+(1<<(int)(log(n/4+0.5)/log(2))/2). + ip[0],ip[1] are pointers of the cos/sin table. + w[0...n*5/8-1] :cos/sin table (double *) + w[],ip[] are initialized if ip[0] == 0. + [remark] + Inverse of + a[0] *= 0.5; + a[n] *= 0.5; + dfct(n, a, t, ip, w); + is + a[0] *= 0.5; + a[n] *= 0.5; + dfct(n, a, t, ip, w); + for (j = 0; j <= n; j++) { + a[j] *= 2.0 / n; + } + . + + +-------- Sine Transform of RDFT (Real Anti-symmetric DFT) -------- + [definition] + S[k] = sum_j=1^n-1 a[j]*sin(pi*j*k/n), 0= 2, n = power of 2 + a[0...n-1] :input/output data (double *) + output data + a[k] = S[k], 0= 2+sqrt(n/4) + strictly, + length of ip >= + 2+(1<<(int)(log(n/4+0.5)/log(2))/2). + ip[0],ip[1] are pointers of the cos/sin table. + w[0...n*5/8-1] :cos/sin table (double *) + w[],ip[] are initialized if ip[0] == 0. + [remark] + Inverse of + dfst(n, a, t, ip, w); + is + dfst(n, a, t, ip, w); + for (j = 1; j <= n - 1; j++) { + a[j] *= 2.0 / n; + } + . + + +Appendix : + The cos/sin table is recalculated when the larger table required. + w[] and ip[] are compatible with all routines. +*/ + +void makewt(int nw, int *ip, double *w); +void makect(int nc, int *ip, double *c); +void cft1st(int n, double *a, double *w); +void cftmdl(int n, int l, double *a, double *w); +void bitrv2(int n, int *ip, double *a); +void bitrv2conj(int n, int *ip, double *a); +void cftfsub(int n, double *a, double *w); +void cftbsub(int n, double *a, double *w); +void rftfsub(int n, double *a, int nc, double *c); +void rftbsub(int n, double *a, int nc, double *c); +void dstsub(int n, double *a, int nc, double *c); +void dctsub(int n, double *a, int nc, double *c); + + +void cdft(int n, int isgn, double *a, int *ip, double *w) +{ + if (n > (ip[0] << 2)) { + makewt(n >> 2, ip, w); + } + if (n > 4) { + if (isgn >= 0) { + bitrv2(n, ip + 2, a); + cftfsub(n, a, w); + } else { + bitrv2conj(n, ip + 2, a); + cftbsub(n, a, w); + } + } else if (n == 4) { + cftfsub(n, a, w); + } +} + + +void rdft(int n, int isgn, double *a, int *ip, double *w) +{ + int nw, nc; + double xi; + + nw = ip[0]; + if (n > (nw << 2)) { + nw = n >> 2; + makewt(nw, ip, w); + } + nc = ip[1]; + if (n > (nc << 2)) { + nc = n >> 2; + makect(nc, ip, w + nw); + } + if (isgn >= 0) { + if (n > 4) { + bitrv2(n, ip + 2, a); + cftfsub(n, a, w); + rftfsub(n, a, nc, w + nw); + } else if (n == 4) { + cftfsub(n, a, w); + } + xi = a[0] - a[1]; + a[0] += a[1]; + a[1] = xi; + } else { + a[1] = 0.5 * (a[0] - a[1]); + a[0] -= a[1]; + if (n > 4) { + rftbsub(n, a, nc, w + nw); + bitrv2(n, ip + 2, a); + cftbsub(n, a, w); + } else if (n == 4) { + cftfsub(n, a, w); + } + } +} + + +void ddct(int n, int isgn, double *a, int *ip, double *w) +{ + int j, nw, nc; + double xr; + + nw = ip[0]; + if (n > (nw << 2)) { + nw = n >> 2; + makewt(nw, ip, w); + } + nc = ip[1]; + if (n > nc) { + nc = n; + makect(nc, ip, w + nw); + } + if (isgn < 0) { + xr = a[n - 1]; + for (j = n - 2; j >= 2; j -= 2) { + a[j + 1] = a[j] - a[j - 1]; + a[j] += a[j - 1]; + } + a[1] = a[0] - xr; + a[0] += xr; + if (n > 4) { + rftbsub(n, a, nc, w + nw); + bitrv2(n, ip + 2, a); + cftbsub(n, a, w); + } else if (n == 4) { + cftfsub(n, a, w); + } + } + dctsub(n, a, nc, w + nw); + if (isgn >= 0) { + if (n > 4) { + bitrv2(n, ip + 2, a); + cftfsub(n, a, w); + rftfsub(n, a, nc, w + nw); + } else if (n == 4) { + cftfsub(n, a, w); + } + xr = a[0] - a[1]; + a[0] += a[1]; + for (j = 2; j < n; j += 2) { + a[j - 1] = a[j] - a[j + 1]; + a[j] += a[j + 1]; + } + a[n - 1] = xr; + } +} + + +void ddst(int n, int isgn, double *a, int *ip, double *w) +{ + int j, nw, nc; + double xr; + + nw = ip[0]; + if (n > (nw << 2)) { + nw = n >> 2; + makewt(nw, ip, w); + } + nc = ip[1]; + if (n > nc) { + nc = n; + makect(nc, ip, w + nw); + } + if (isgn < 0) { + xr = a[n - 1]; + for (j = n - 2; j >= 2; j -= 2) { + a[j + 1] = -a[j] - a[j - 1]; + a[j] -= a[j - 1]; + } + a[1] = a[0] + xr; + a[0] -= xr; + if (n > 4) { + rftbsub(n, a, nc, w + nw); + bitrv2(n, ip + 2, a); + cftbsub(n, a, w); + } else if (n == 4) { + cftfsub(n, a, w); + } + } + dstsub(n, a, nc, w + nw); + if (isgn >= 0) { + if (n > 4) { + bitrv2(n, ip + 2, a); + cftfsub(n, a, w); + rftfsub(n, a, nc, w + nw); + } else if (n == 4) { + cftfsub(n, a, w); + } + xr = a[0] - a[1]; + a[0] += a[1]; + for (j = 2; j < n; j += 2) { + a[j - 1] = -a[j] - a[j + 1]; + a[j] -= a[j + 1]; + } + a[n - 1] = -xr; + } +} + + +void dfct(int n, double *a, double *t, int *ip, double *w) +{ + int j, k, l, m, mh, nw, nc; + double xr, xi, yr, yi; + + nw = ip[0]; + if (n > (nw << 3)) { + nw = n >> 3; + makewt(nw, ip, w); + } + nc = ip[1]; + if (n > (nc << 1)) { + nc = n >> 1; + makect(nc, ip, w + nw); + } + m = n >> 1; + yi = a[m]; + xi = a[0] + a[n]; + a[0] -= a[n]; + t[0] = xi - yi; + t[m] = xi + yi; + if (n > 2) { + mh = m >> 1; + for (j = 1; j < mh; j++) { + k = m - j; + xr = a[j] - a[n - j]; + xi = a[j] + a[n - j]; + yr = a[k] - a[n - k]; + yi = a[k] + a[n - k]; + a[j] = xr; + a[k] = yr; + t[j] = xi - yi; + t[k] = xi + yi; + } + t[mh] = a[mh] + a[n - mh]; + a[mh] -= a[n - mh]; + dctsub(m, a, nc, w + nw); + if (m > 4) { + bitrv2(m, ip + 2, a); + cftfsub(m, a, w); + rftfsub(m, a, nc, w + nw); + } else if (m == 4) { + cftfsub(m, a, w); + } + a[n - 1] = a[0] - a[1]; + a[1] = a[0] + a[1]; + for (j = m - 2; j >= 2; j -= 2) { + a[2 * j + 1] = a[j] + a[j + 1]; + a[2 * j - 1] = a[j] - a[j + 1]; + } + l = 2; + m = mh; + while (m >= 2) { + dctsub(m, t, nc, w + nw); + if (m > 4) { + bitrv2(m, ip + 2, t); + cftfsub(m, t, w); + rftfsub(m, t, nc, w + nw); + } else if (m == 4) { + cftfsub(m, t, w); + } + a[n - l] = t[0] - t[1]; + a[l] = t[0] + t[1]; + k = 0; + for (j = 2; j < m; j += 2) { + k += l << 2; + a[k - l] = t[j] - t[j + 1]; + a[k + l] = t[j] + t[j + 1]; + } + l <<= 1; + mh = m >> 1; + for (j = 0; j < mh; j++) { + k = m - j; + t[j] = t[m + k] - t[m + j]; + t[k] = t[m + k] + t[m + j]; + } + t[mh] = t[m + mh]; + m = mh; + } + a[l] = t[0]; + a[n] = t[2] - t[1]; + a[0] = t[2] + t[1]; + } else { + a[1] = a[0]; + a[2] = t[0]; + a[0] = t[1]; + } +} + + +void dfst(int n, double *a, double *t, int *ip, double *w) +{ + int j, k, l, m, mh, nw, nc; + double xr, xi, yr, yi; + + nw = ip[0]; + if (n > (nw << 3)) { + nw = n >> 3; + makewt(nw, ip, w); + } + nc = ip[1]; + if (n > (nc << 1)) { + nc = n >> 1; + makect(nc, ip, w + nw); + } + if (n > 2) { + m = n >> 1; + mh = m >> 1; + for (j = 1; j < mh; j++) { + k = m - j; + xr = a[j] + a[n - j]; + xi = a[j] - a[n - j]; + yr = a[k] + a[n - k]; + yi = a[k] - a[n - k]; + a[j] = xr; + a[k] = yr; + t[j] = xi + yi; + t[k] = xi - yi; + } + t[0] = a[mh] - a[n - mh]; + a[mh] += a[n - mh]; + a[0] = a[m]; + dstsub(m, a, nc, w + nw); + if (m > 4) { + bitrv2(m, ip + 2, a); + cftfsub(m, a, w); + rftfsub(m, a, nc, w + nw); + } else if (m == 4) { + cftfsub(m, a, w); + } + a[n - 1] = a[1] - a[0]; + a[1] = a[0] + a[1]; + for (j = m - 2; j >= 2; j -= 2) { + a[2 * j + 1] = a[j] - a[j + 1]; + a[2 * j - 1] = -a[j] - a[j + 1]; + } + l = 2; + m = mh; + while (m >= 2) { + dstsub(m, t, nc, w + nw); + if (m > 4) { + bitrv2(m, ip + 2, t); + cftfsub(m, t, w); + rftfsub(m, t, nc, w + nw); + } else if (m == 4) { + cftfsub(m, t, w); + } + a[n - l] = t[1] - t[0]; + a[l] = t[0] + t[1]; + k = 0; + for (j = 2; j < m; j += 2) { + k += l << 2; + a[k - l] = -t[j] - t[j + 1]; + a[k + l] = t[j] - t[j + 1]; + } + l <<= 1; + mh = m >> 1; + for (j = 1; j < mh; j++) { + k = m - j; + t[j] = t[m + k] + t[m + j]; + t[k] = t[m + k] - t[m + j]; + } + t[0] = t[m + mh]; + m = mh; + } + a[l] = t[0]; + } + a[0] = 0; +} + + +/* -------- initializing routines -------- */ + +void makewt(int nw, int *ip, double *w) +{ + int j, nwh; + double delta, x, y; + + ip[0] = nw; + ip[1] = 1; + if (nw > 2) { + nwh = nw >> 1; + delta = atan(1.0) / nwh; + w[0] = 1; + w[1] = 0; + w[nwh] = cos(delta * nwh); + w[nwh + 1] = w[nwh]; + if (nwh > 2) { + for (j = 2; j < nwh; j += 2) { + x = cos(delta * j); + y = sin(delta * j); + w[j] = x; + w[j + 1] = y; + w[nw - j] = y; + w[nw - j + 1] = x; + } + for (j = nwh - 2; j >= 2; j -= 2) { + x = w[2 * j]; + y = w[2 * j + 1]; + w[nwh + j] = x; + w[nwh + j + 1] = y; + } + bitrv2(nw, ip + 2, w); + } + } +} + + +void makect(int nc, int *ip, double *c) +{ + int j, nch; + double delta; + + ip[1] = nc; + if (nc > 1) { + nch = nc >> 1; + delta = atan(1.0) / nch; + c[0] = cos(delta * nch); + c[nch] = 0.5 * c[0]; + for (j = 1; j < nch; j++) { + c[j] = 0.5 * cos(delta * j); + c[nc - j] = 0.5 * sin(delta * j); + } + } +} + + +/* -------- child routines -------- */ + + +void bitrv2(int n, int *ip, double *a) +{ + int j, j1, k, k1, l, m, m2; + double xr, xi, yr, yi; + + ip[0] = 0; + l = n; + m = 1; + while ((m << 3) < l) { + l >>= 1; + for (j = 0; j < m; j++) { + ip[m + j] = ip[j] + l; + } + m <<= 1; + } + m2 = 2 * m; + if ((m << 3) == l) { + for (k = 0; k < m; k++) { + for (j = 0; j < k; j++) { + j1 = 2 * j + ip[k]; + k1 = 2 * k + ip[j]; + xr = a[j1]; + xi = a[j1 + 1]; + yr = a[k1]; + yi = a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 += 2 * m2; + xr = a[j1]; + xi = a[j1 + 1]; + yr = a[k1]; + yi = a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 -= m2; + xr = a[j1]; + xi = a[j1 + 1]; + yr = a[k1]; + yi = a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 += 2 * m2; + xr = a[j1]; + xi = a[j1 + 1]; + yr = a[k1]; + yi = a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + } + j1 = 2 * k + m2 + ip[k]; + k1 = j1 + m2; + xr = a[j1]; + xi = a[j1 + 1]; + yr = a[k1]; + yi = a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + } + } else { + for (k = 1; k < m; k++) { + for (j = 0; j < k; j++) { + j1 = 2 * j + ip[k]; + k1 = 2 * k + ip[j]; + xr = a[j1]; + xi = a[j1 + 1]; + yr = a[k1]; + yi = a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 += m2; + xr = a[j1]; + xi = a[j1 + 1]; + yr = a[k1]; + yi = a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + } + } + } +} + + +void bitrv2conj(int n, int *ip, double *a) +{ + int j, j1, k, k1, l, m, m2; + double xr, xi, yr, yi; + + ip[0] = 0; + l = n; + m = 1; + while ((m << 3) < l) { + l >>= 1; + for (j = 0; j < m; j++) { + ip[m + j] = ip[j] + l; + } + m <<= 1; + } + m2 = 2 * m; + if ((m << 3) == l) { + for (k = 0; k < m; k++) { + for (j = 0; j < k; j++) { + j1 = 2 * j + ip[k]; + k1 = 2 * k + ip[j]; + xr = a[j1]; + xi = -a[j1 + 1]; + yr = a[k1]; + yi = -a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 += 2 * m2; + xr = a[j1]; + xi = -a[j1 + 1]; + yr = a[k1]; + yi = -a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 -= m2; + xr = a[j1]; + xi = -a[j1 + 1]; + yr = a[k1]; + yi = -a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 += 2 * m2; + xr = a[j1]; + xi = -a[j1 + 1]; + yr = a[k1]; + yi = -a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + } + k1 = 2 * k + ip[k]; + a[k1 + 1] = -a[k1 + 1]; + j1 = k1 + m2; + k1 = j1 + m2; + xr = a[j1]; + xi = -a[j1 + 1]; + yr = a[k1]; + yi = -a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + k1 += m2; + a[k1 + 1] = -a[k1 + 1]; + } + } else { + a[1] = -a[1]; + a[m2 + 1] = -a[m2 + 1]; + for (k = 1; k < m; k++) { + for (j = 0; j < k; j++) { + j1 = 2 * j + ip[k]; + k1 = 2 * k + ip[j]; + xr = a[j1]; + xi = -a[j1 + 1]; + yr = a[k1]; + yi = -a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + j1 += m2; + k1 += m2; + xr = a[j1]; + xi = -a[j1 + 1]; + yr = a[k1]; + yi = -a[k1 + 1]; + a[j1] = yr; + a[j1 + 1] = yi; + a[k1] = xr; + a[k1 + 1] = xi; + } + k1 = 2 * k + ip[k]; + a[k1 + 1] = -a[k1 + 1]; + a[k1 + m2 + 1] = -a[k1 + m2 + 1]; + } + } +} + + +void cftfsub(int n, double *a, double *w) +{ + int j, j1, j2, j3, l; + double x0r, x0i, x1r, x1i, x2r, x2i, x3r, x3i; + + l = 2; + if (n >= 16) { + cft1st(n, a, w); + l = 16; + while ((l << 3) <= n) { + cftmdl(n, l, a, w); + l <<= 3; + } + } + if ((l << 1) < n) { + for (j = 0; j < l; j += 2) { + j1 = j + l; + j2 = j1 + l; + j3 = j2 + l; + x0r = a[j] + a[j1]; + x0i = a[j + 1] + a[j1 + 1]; + x1r = a[j] - a[j1]; + x1i = a[j + 1] - a[j1 + 1]; + x2r = a[j2] + a[j3]; + x2i = a[j2 + 1] + a[j3 + 1]; + x3r = a[j2] - a[j3]; + x3i = a[j2 + 1] - a[j3 + 1]; + a[j] = x0r + x2r; + a[j + 1] = x0i + x2i; + a[j2] = x0r - x2r; + a[j2 + 1] = x0i - x2i; + a[j1] = x1r - x3i; + a[j1 + 1] = x1i + x3r; + a[j3] = x1r + x3i; + a[j3 + 1] = x1i - x3r; + } + } else if ((l << 1) == n) { + for (j = 0; j < l; j += 2) { + j1 = j + l; + x0r = a[j] - a[j1]; + x0i = a[j + 1] - a[j1 + 1]; + a[j] += a[j1]; + a[j + 1] += a[j1 + 1]; + a[j1] = x0r; + a[j1 + 1] = x0i; + } + } +} + + +void cftbsub(int n, double *a, double *w) +{ + int j, j1, j2, j3, j4, j5, j6, j7, l; + double wn4r, x0r, x0i, x1r, x1i, x2r, x2i, x3r, x3i, + y0r, y0i, y1r, y1i, y2r, y2i, y3r, y3i, + y4r, y4i, y5r, y5i, y6r, y6i, y7r, y7i; + + l = 2; + if (n > 16) { + cft1st(n, a, w); + l = 16; + while ((l << 3) < n) { + cftmdl(n, l, a, w); + l <<= 3; + } + } + if ((l << 2) < n) { + wn4r = w[2]; + for (j = 0; j < l; j += 2) { + j1 = j + l; + j2 = j1 + l; + j3 = j2 + l; + j4 = j3 + l; + j5 = j4 + l; + j6 = j5 + l; + j7 = j6 + l; + x0r = a[j] + a[j1]; + x0i = -a[j + 1] - a[j1 + 1]; + x1r = a[j] - a[j1]; + x1i = -a[j + 1] + a[j1 + 1]; + x2r = a[j2] + a[j3]; + x2i = a[j2 + 1] + a[j3 + 1]; + x3r = a[j2] - a[j3]; + x3i = a[j2 + 1] - a[j3 + 1]; + y0r = x0r + x2r; + y0i = x0i - x2i; + y2r = x0r - x2r; + y2i = x0i + x2i; + y1r = x1r - x3i; + y1i = x1i - x3r; + y3r = x1r + x3i; + y3i = x1i + x3r; + x0r = a[j4] + a[j5]; + x0i = a[j4 + 1] + a[j5 + 1]; + x1r = a[j4] - a[j5]; + x1i = a[j4 + 1] - a[j5 + 1]; + x2r = a[j6] + a[j7]; + x2i = a[j6 + 1] + a[j7 + 1]; + x3r = a[j6] - a[j7]; + x3i = a[j6 + 1] - a[j7 + 1]; + y4r = x0r + x2r; + y4i = x0i + x2i; + y6r = x0r - x2r; + y6i = x0i - x2i; + x0r = x1r - x3i; + x0i = x1i + x3r; + x2r = x1r + x3i; + x2i = x1i - x3r; + y5r = wn4r * (x0r - x0i); + y5i = wn4r * (x0r + x0i); + y7r = wn4r * (x2r - x2i); + y7i = wn4r * (x2r + x2i); + a[j1] = y1r + y5r; + a[j1 + 1] = y1i - y5i; + a[j5] = y1r - y5r; + a[j5 + 1] = y1i + y5i; + a[j3] = y3r - y7i; + a[j3 + 1] = y3i - y7r; + a[j7] = y3r + y7i; + a[j7 + 1] = y3i + y7r; + a[j] = y0r + y4r; + a[j + 1] = y0i - y4i; + a[j4] = y0r - y4r; + a[j4 + 1] = y0i + y4i; + a[j2] = y2r - y6i; + a[j2 + 1] = y2i - y6r; + a[j6] = y2r + y6i; + a[j6 + 1] = y2i + y6r; + } + } else if ((l << 2) == n) { + for (j = 0; j < l; j += 2) { + j1 = j + l; + j2 = j1 + l; + j3 = j2 + l; + x0r = a[j] + a[j1]; + x0i = -a[j + 1] - a[j1 + 1]; + x1r = a[j] - a[j1]; + x1i = -a[j + 1] + a[j1 + 1]; + x2r = a[j2] + a[j3]; + x2i = a[j2 + 1] + a[j3 + 1]; + x3r = a[j2] - a[j3]; + x3i = a[j2 + 1] - a[j3 + 1]; + a[j] = x0r + x2r; + a[j + 1] = x0i - x2i; + a[j2] = x0r - x2r; + a[j2 + 1] = x0i + x2i; + a[j1] = x1r - x3i; + a[j1 + 1] = x1i - x3r; + a[j3] = x1r + x3i; + a[j3 + 1] = x1i + x3r; + } + } else { + for (j = 0; j < l; j += 2) { + j1 = j + l; + x0r = a[j] - a[j1]; + x0i = -a[j + 1] + a[j1 + 1]; + a[j] += a[j1]; + a[j + 1] = -a[j + 1] - a[j1 + 1]; + a[j1] = x0r; + a[j1 + 1] = x0i; + } + } +} + + +void cft1st(int n, double *a, double *w) +{ + int j, k1; + double wn4r, wtmp, wk1r, wk1i, wk2r, wk2i, wk3r, wk3i, + wk4r, wk4i, wk5r, wk5i, wk6r, wk6i, wk7r, wk7i; + double x0r, x0i, x1r, x1i, x2r, x2i, x3r, x3i, + y0r, y0i, y1r, y1i, y2r, y2i, y3r, y3i, + y4r, y4i, y5r, y5i, y6r, y6i, y7r, y7i; + + wn4r = w[2]; + x0r = a[0] + a[2]; + x0i = a[1] + a[3]; + x1r = a[0] - a[2]; + x1i = a[1] - a[3]; + x2r = a[4] + a[6]; + x2i = a[5] + a[7]; + x3r = a[4] - a[6]; + x3i = a[5] - a[7]; + y0r = x0r + x2r; + y0i = x0i + x2i; + y2r = x0r - x2r; + y2i = x0i - x2i; + y1r = x1r - x3i; + y1i = x1i + x3r; + y3r = x1r + x3i; + y3i = x1i - x3r; + x0r = a[8] + a[10]; + x0i = a[9] + a[11]; + x1r = a[8] - a[10]; + x1i = a[9] - a[11]; + x2r = a[12] + a[14]; + x2i = a[13] + a[15]; + x3r = a[12] - a[14]; + x3i = a[13] - a[15]; + y4r = x0r + x2r; + y4i = x0i + x2i; + y6r = x0r - x2r; + y6i = x0i - x2i; + x0r = x1r - x3i; + x0i = x1i + x3r; + x2r = x1r + x3i; + x2i = x1i - x3r; + y5r = wn4r * (x0r - x0i); + y5i = wn4r * (x0r + x0i); + y7r = wn4r * (x2r - x2i); + y7i = wn4r * (x2r + x2i); + a[2] = y1r + y5r; + a[3] = y1i + y5i; + a[10] = y1r - y5r; + a[11] = y1i - y5i; + a[6] = y3r - y7i; + a[7] = y3i + y7r; + a[14] = y3r + y7i; + a[15] = y3i - y7r; + a[0] = y0r + y4r; + a[1] = y0i + y4i; + a[8] = y0r - y4r; + a[9] = y0i - y4i; + a[4] = y2r - y6i; + a[5] = y2i + y6r; + a[12] = y2r + y6i; + a[13] = y2i - y6r; + if (n > 16) { + wk1r = w[4]; + wk1i = w[5]; + x0r = a[16] + a[18]; + x0i = a[17] + a[19]; + x1r = a[16] - a[18]; + x1i = a[17] - a[19]; + x2r = a[20] + a[22]; + x2i = a[21] + a[23]; + x3r = a[20] - a[22]; + x3i = a[21] - a[23]; + y0r = x0r + x2r; + y0i = x0i + x2i; + y2r = x0r - x2r; + y2i = x0i - x2i; + y1r = x1r - x3i; + y1i = x1i + x3r; + y3r = x1r + x3i; + y3i = x1i - x3r; + x0r = a[24] + a[26]; + x0i = a[25] + a[27]; + x1r = a[24] - a[26]; + x1i = a[25] - a[27]; + x2r = a[28] + a[30]; + x2i = a[29] + a[31]; + x3r = a[28] - a[30]; + x3i = a[29] - a[31]; + y4r = x0r + x2r; + y4i = x0i + x2i; + y6r = x0r - x2r; + y6i = x0i - x2i; + x0r = x1r - x3i; + x0i = x1i + x3r; + x2r = x1r + x3i; + x2i = x3r - x1i; + y5r = wk1i * x0r - wk1r * x0i; + y5i = wk1i * x0i + wk1r * x0r; + y7r = wk1r * x2r + wk1i * x2i; + y7i = wk1r * x2i - wk1i * x2r; + x0r = wk1r * y1r - wk1i * y1i; + x0i = wk1r * y1i + wk1i * y1r; + a[18] = x0r + y5r; + a[19] = x0i + y5i; + a[26] = y5i - x0i; + a[27] = x0r - y5r; + x0r = wk1i * y3r - wk1r * y3i; + x0i = wk1i * y3i + wk1r * y3r; + a[22] = x0r - y7r; + a[23] = x0i + y7i; + a[30] = y7i - x0i; + a[31] = x0r + y7r; + a[16] = y0r + y4r; + a[17] = y0i + y4i; + a[24] = y4i - y0i; + a[25] = y0r - y4r; + x0r = y2r - y6i; + x0i = y2i + y6r; + a[20] = wn4r * (x0r - x0i); + a[21] = wn4r * (x0i + x0r); + x0r = y6r - y2i; + x0i = y2r + y6i; + a[28] = wn4r * (x0r - x0i); + a[29] = wn4r * (x0i + x0r); + k1 = 4; + for (j = 32; j < n; j += 16) { + k1 += 4; + wk1r = w[k1]; + wk1i = w[k1 + 1]; + wk2r = w[k1 + 2]; + wk2i = w[k1 + 3]; + wtmp = 2 * wk2i; + wk3r = wk1r - wtmp * wk1i; + wk3i = wtmp * wk1r - wk1i; + wk4r = 1 - wtmp * wk2i; + wk4i = wtmp * wk2r; + wtmp = 2 * wk4i; + wk5r = wk3r - wtmp * wk1i; + wk5i = wtmp * wk1r - wk3i; + wk6r = wk2r - wtmp * wk2i; + wk6i = wtmp * wk2r - wk2i; + wk7r = wk1r - wtmp * wk3i; + wk7i = wtmp * wk3r - wk1i; + x0r = a[j] + a[j + 2]; + x0i = a[j + 1] + a[j + 3]; + x1r = a[j] - a[j + 2]; + x1i = a[j + 1] - a[j + 3]; + x2r = a[j + 4] + a[j + 6]; + x2i = a[j + 5] + a[j + 7]; + x3r = a[j + 4] - a[j + 6]; + x3i = a[j + 5] - a[j + 7]; + y0r = x0r + x2r; + y0i = x0i + x2i; + y2r = x0r - x2r; + y2i = x0i - x2i; + y1r = x1r - x3i; + y1i = x1i + x3r; + y3r = x1r + x3i; + y3i = x1i - x3r; + x0r = a[j + 8] + a[j + 10]; + x0i = a[j + 9] + a[j + 11]; + x1r = a[j + 8] - a[j + 10]; + x1i = a[j + 9] - a[j + 11]; + x2r = a[j + 12] + a[j + 14]; + x2i = a[j + 13] + a[j + 15]; + x3r = a[j + 12] - a[j + 14]; + x3i = a[j + 13] - a[j + 15]; + y4r = x0r + x2r; + y4i = x0i + x2i; + y6r = x0r - x2r; + y6i = x0i - x2i; + x0r = x1r - x3i; + x0i = x1i + x3r; + x2r = x1r + x3i; + x2i = x1i - x3r; + y5r = wn4r * (x0r - x0i); + y5i = wn4r * (x0r + x0i); + y7r = wn4r * (x2r - x2i); + y7i = wn4r * (x2r + x2i); + x0r = y1r + y5r; + x0i = y1i + y5i; + a[j + 2] = wk1r * x0r - wk1i * x0i; + a[j + 3] = wk1r * x0i + wk1i * x0r; + x0r = y1r - y5r; + x0i = y1i - y5i; + a[j + 10] = wk5r * x0r - wk5i * x0i; + a[j + 11] = wk5r * x0i + wk5i * x0r; + x0r = y3r - y7i; + x0i = y3i + y7r; + a[j + 6] = wk3r * x0r - wk3i * x0i; + a[j + 7] = wk3r * x0i + wk3i * x0r; + x0r = y3r + y7i; + x0i = y3i - y7r; + a[j + 14] = wk7r * x0r - wk7i * x0i; + a[j + 15] = wk7r * x0i + wk7i * x0r; + a[j] = y0r + y4r; + a[j + 1] = y0i + y4i; + x0r = y0r - y4r; + x0i = y0i - y4i; + a[j + 8] = wk4r * x0r - wk4i * x0i; + a[j + 9] = wk4r * x0i + wk4i * x0r; + x0r = y2r - y6i; + x0i = y2i + y6r; + a[j + 4] = wk2r * x0r - wk2i * x0i; + a[j + 5] = wk2r * x0i + wk2i * x0r; + x0r = y2r + y6i; + x0i = y2i - y6r; + a[j + 12] = wk6r * x0r - wk6i * x0i; + a[j + 13] = wk6r * x0i + wk6i * x0r; + } + } +} + + +void cftmdl(int n, int l, double *a, double *w) +{ + int j, j1, j2, j3, j4, j5, j6, j7, k, k1, m; + double wn4r, wtmp, wk1r, wk1i, wk2r, wk2i, wk3r, wk3i, + wk4r, wk4i, wk5r, wk5i, wk6r, wk6i, wk7r, wk7i; + double x0r, x0i, x1r, x1i, x2r, x2i, x3r, x3i, + y0r, y0i, y1r, y1i, y2r, y2i, y3r, y3i, + y4r, y4i, y5r, y5i, y6r, y6i, y7r, y7i; + + m = l << 3; + wn4r = w[2]; + for (j = 0; j < l; j += 2) { + j1 = j + l; + j2 = j1 + l; + j3 = j2 + l; + j4 = j3 + l; + j5 = j4 + l; + j6 = j5 + l; + j7 = j6 + l; + x0r = a[j] + a[j1]; + x0i = a[j + 1] + a[j1 + 1]; + x1r = a[j] - a[j1]; + x1i = a[j + 1] - a[j1 + 1]; + x2r = a[j2] + a[j3]; + x2i = a[j2 + 1] + a[j3 + 1]; + x3r = a[j2] - a[j3]; + x3i = a[j2 + 1] - a[j3 + 1]; + y0r = x0r + x2r; + y0i = x0i + x2i; + y2r = x0r - x2r; + y2i = x0i - x2i; + y1r = x1r - x3i; + y1i = x1i + x3r; + y3r = x1r + x3i; + y3i = x1i - x3r; + x0r = a[j4] + a[j5]; + x0i = a[j4 + 1] + a[j5 + 1]; + x1r = a[j4] - a[j5]; + x1i = a[j4 + 1] - a[j5 + 1]; + x2r = a[j6] + a[j7]; + x2i = a[j6 + 1] + a[j7 + 1]; + x3r = a[j6] - a[j7]; + x3i = a[j6 + 1] - a[j7 + 1]; + y4r = x0r + x2r; + y4i = x0i + x2i; + y6r = x0r - x2r; + y6i = x0i - x2i; + x0r = x1r - x3i; + x0i = x1i + x3r; + x2r = x1r + x3i; + x2i = x1i - x3r; + y5r = wn4r * (x0r - x0i); + y5i = wn4r * (x0r + x0i); + y7r = wn4r * (x2r - x2i); + y7i = wn4r * (x2r + x2i); + a[j1] = y1r + y5r; + a[j1 + 1] = y1i + y5i; + a[j5] = y1r - y5r; + a[j5 + 1] = y1i - y5i; + a[j3] = y3r - y7i; + a[j3 + 1] = y3i + y7r; + a[j7] = y3r + y7i; + a[j7 + 1] = y3i - y7r; + a[j] = y0r + y4r; + a[j + 1] = y0i + y4i; + a[j4] = y0r - y4r; + a[j4 + 1] = y0i - y4i; + a[j2] = y2r - y6i; + a[j2 + 1] = y2i + y6r; + a[j6] = y2r + y6i; + a[j6 + 1] = y2i - y6r; + } + if (m < n) { + wk1r = w[4]; + wk1i = w[5]; + for (j = m; j < l + m; j += 2) { + j1 = j + l; + j2 = j1 + l; + j3 = j2 + l; + j4 = j3 + l; + j5 = j4 + l; + j6 = j5 + l; + j7 = j6 + l; + x0r = a[j] + a[j1]; + x0i = a[j + 1] + a[j1 + 1]; + x1r = a[j] - a[j1]; + x1i = a[j + 1] - a[j1 + 1]; + x2r = a[j2] + a[j3]; + x2i = a[j2 + 1] + a[j3 + 1]; + x3r = a[j2] - a[j3]; + x3i = a[j2 + 1] - a[j3 + 1]; + y0r = x0r + x2r; + y0i = x0i + x2i; + y2r = x0r - x2r; + y2i = x0i - x2i; + y1r = x1r - x3i; + y1i = x1i + x3r; + y3r = x1r + x3i; + y3i = x1i - x3r; + x0r = a[j4] + a[j5]; + x0i = a[j4 + 1] + a[j5 + 1]; + x1r = a[j4] - a[j5]; + x1i = a[j4 + 1] - a[j5 + 1]; + x2r = a[j6] + a[j7]; + x2i = a[j6 + 1] + a[j7 + 1]; + x3r = a[j6] - a[j7]; + x3i = a[j6 + 1] - a[j7 + 1]; + y4r = x0r + x2r; + y4i = x0i + x2i; + y6r = x0r - x2r; + y6i = x0i - x2i; + x0r = x1r - x3i; + x0i = x1i + x3r; + x2r = x1r + x3i; + x2i = x3r - x1i; + y5r = wk1i * x0r - wk1r * x0i; + y5i = wk1i * x0i + wk1r * x0r; + y7r = wk1r * x2r + wk1i * x2i; + y7i = wk1r * x2i - wk1i * x2r; + x0r = wk1r * y1r - wk1i * y1i; + x0i = wk1r * y1i + wk1i * y1r; + a[j1] = x0r + y5r; + a[j1 + 1] = x0i + y5i; + a[j5] = y5i - x0i; + a[j5 + 1] = x0r - y5r; + x0r = wk1i * y3r - wk1r * y3i; + x0i = wk1i * y3i + wk1r * y3r; + a[j3] = x0r - y7r; + a[j3 + 1] = x0i + y7i; + a[j7] = y7i - x0i; + a[j7 + 1] = x0r + y7r; + a[j] = y0r + y4r; + a[j + 1] = y0i + y4i; + a[j4] = y4i - y0i; + a[j4 + 1] = y0r - y4r; + x0r = y2r - y6i; + x0i = y2i + y6r; + a[j2] = wn4r * (x0r - x0i); + a[j2 + 1] = wn4r * (x0i + x0r); + x0r = y6r - y2i; + x0i = y2r + y6i; + a[j6] = wn4r * (x0r - x0i); + a[j6 + 1] = wn4r * (x0i + x0r); + } + k1 = 4; + for (k = 2 * m; k < n; k += m) { + k1 += 4; + wk1r = w[k1]; + wk1i = w[k1 + 1]; + wk2r = w[k1 + 2]; + wk2i = w[k1 + 3]; + wtmp = 2 * wk2i; + wk3r = wk1r - wtmp * wk1i; + wk3i = wtmp * wk1r - wk1i; + wk4r = 1 - wtmp * wk2i; + wk4i = wtmp * wk2r; + wtmp = 2 * wk4i; + wk5r = wk3r - wtmp * wk1i; + wk5i = wtmp * wk1r - wk3i; + wk6r = wk2r - wtmp * wk2i; + wk6i = wtmp * wk2r - wk2i; + wk7r = wk1r - wtmp * wk3i; + wk7i = wtmp * wk3r - wk1i; + for (j = k; j < l + k; j += 2) { + j1 = j + l; + j2 = j1 + l; + j3 = j2 + l; + j4 = j3 + l; + j5 = j4 + l; + j6 = j5 + l; + j7 = j6 + l; + x0r = a[j] + a[j1]; + x0i = a[j + 1] + a[j1 + 1]; + x1r = a[j] - a[j1]; + x1i = a[j + 1] - a[j1 + 1]; + x2r = a[j2] + a[j3]; + x2i = a[j2 + 1] + a[j3 + 1]; + x3r = a[j2] - a[j3]; + x3i = a[j2 + 1] - a[j3 + 1]; + y0r = x0r + x2r; + y0i = x0i + x2i; + y2r = x0r - x2r; + y2i = x0i - x2i; + y1r = x1r - x3i; + y1i = x1i + x3r; + y3r = x1r + x3i; + y3i = x1i - x3r; + x0r = a[j4] + a[j5]; + x0i = a[j4 + 1] + a[j5 + 1]; + x1r = a[j4] - a[j5]; + x1i = a[j4 + 1] - a[j5 + 1]; + x2r = a[j6] + a[j7]; + x2i = a[j6 + 1] + a[j7 + 1]; + x3r = a[j6] - a[j7]; + x3i = a[j6 + 1] - a[j7 + 1]; + y4r = x0r + x2r; + y4i = x0i + x2i; + y6r = x0r - x2r; + y6i = x0i - x2i; + x0r = x1r - x3i; + x0i = x1i + x3r; + x2r = x1r + x3i; + x2i = x1i - x3r; + y5r = wn4r * (x0r - x0i); + y5i = wn4r * (x0r + x0i); + y7r = wn4r * (x2r - x2i); + y7i = wn4r * (x2r + x2i); + x0r = y1r + y5r; + x0i = y1i + y5i; + a[j1] = wk1r * x0r - wk1i * x0i; + a[j1 + 1] = wk1r * x0i + wk1i * x0r; + x0r = y1r - y5r; + x0i = y1i - y5i; + a[j5] = wk5r * x0r - wk5i * x0i; + a[j5 + 1] = wk5r * x0i + wk5i * x0r; + x0r = y3r - y7i; + x0i = y3i + y7r; + a[j3] = wk3r * x0r - wk3i * x0i; + a[j3 + 1] = wk3r * x0i + wk3i * x0r; + x0r = y3r + y7i; + x0i = y3i - y7r; + a[j7] = wk7r * x0r - wk7i * x0i; + a[j7 + 1] = wk7r * x0i + wk7i * x0r; + a[j] = y0r + y4r; + a[j + 1] = y0i + y4i; + x0r = y0r - y4r; + x0i = y0i - y4i; + a[j4] = wk4r * x0r - wk4i * x0i; + a[j4 + 1] = wk4r * x0i + wk4i * x0r; + x0r = y2r - y6i; + x0i = y2i + y6r; + a[j2] = wk2r * x0r - wk2i * x0i; + a[j2 + 1] = wk2r * x0i + wk2i * x0r; + x0r = y2r + y6i; + x0i = y2i - y6r; + a[j6] = wk6r * x0r - wk6i * x0i; + a[j6 + 1] = wk6r * x0i + wk6i * x0r; + } + } + } +} + + +void rftfsub(int n, double *a, int nc, double *c) +{ + int j, k, kk, ks, m; + double wkr, wki, xr, xi, yr, yi; + + m = n >> 1; + ks = 2 * nc / m; + kk = 0; + for (j = 2; j < m; j += 2) { + k = n - j; + kk += ks; + wkr = 0.5 - c[nc - kk]; + wki = c[kk]; + xr = a[j] - a[k]; + xi = a[j + 1] + a[k + 1]; + yr = wkr * xr - wki * xi; + yi = wkr * xi + wki * xr; + a[j] -= yr; + a[j + 1] -= yi; + a[k] += yr; + a[k + 1] -= yi; + } +} + + +void rftbsub(int n, double *a, int nc, double *c) +{ + int j, k, kk, ks, m; + double wkr, wki, xr, xi, yr, yi; + + a[1] = -a[1]; + m = n >> 1; + ks = 2 * nc / m; + kk = 0; + for (j = 2; j < m; j += 2) { + k = n - j; + kk += ks; + wkr = 0.5 - c[nc - kk]; + wki = c[kk]; + xr = a[j] - a[k]; + xi = a[j + 1] + a[k + 1]; + yr = wkr * xr + wki * xi; + yi = wkr * xi - wki * xr; + a[j] -= yr; + a[j + 1] = yi - a[j + 1]; + a[k] += yr; + a[k + 1] = yi - a[k + 1]; + } + a[m + 1] = -a[m + 1]; +} + + +void dctsub(int n, double *a, int nc, double *c) +{ + int j, k, kk, ks, m; + double wkr, wki, xr; + + m = n >> 1; + ks = nc / n; + kk = 0; + for (j = 1; j < m; j++) { + k = n - j; + kk += ks; + wkr = c[kk] - c[nc - kk]; + wki = c[kk] + c[nc - kk]; + xr = wki * a[j] - wkr * a[k]; + a[j] = wkr * a[j] + wki * a[k]; + a[k] = xr; + } + a[m] *= c[0]; +} + + +void dstsub(int n, double *a, int nc, double *c) +{ + int j, k, kk, ks, m; + double wkr, wki, xr; + + m = n >> 1; + ks = nc / n; + kk = 0; + for (j = 1; j < m; j++) { + k = n - j; + kk += ks; + wkr = c[kk] - c[nc - kk]; + wki = c[kk] + c[nc - kk]; + xr = wki * a[k] - wkr * a[j]; + a[k] = wkr * a[k] + wki * a[j]; + a[j] = xr; + } + a[m] *= c[0]; +} diff --git a/src/velociloops.cpp b/src/velociloops.cpp index e8f40e2..bf94828 100644 --- a/src/velociloops.cpp +++ b/src/velociloops.cpp @@ -12,6 +12,7 @@ #include #include + /* ----------------------------------------------------------------------- DWOP decompressor ----------------------------------------------------------------------- */ @@ -19,6 +20,8 @@ namespace { +#include "fft8g.h" + inline int32_t clampInt32(int64_t v) { if (v > std::numeric_limits::max()) @@ -1088,6 +1091,9 @@ class VLFileImpl bool silenceSelected = false; bool headerValid = true; VLError loadError = VL_OK; + bool loadedFromFile = false; + uint16_t globBars = 1; + uint8_t globBeats = 0; static constexpr int32_t kREXPPQ = 15360; @@ -1117,6 +1123,9 @@ class VLFileImpl gateSensitivity = 0; headerValid = true; loadError = VL_OK; + loadedFromFile = false; + globBars = 1; + globBeats = 0; std::memset(&creator, 0, sizeof(creator)); @@ -1126,7 +1135,10 @@ class VLFileImpl if (fileData[0] == 'F' && fileData[1] == 'O' && fileData[2] == 'R' && fileData[3] == 'M' && fileData[8] == 'A' && fileData[9] == 'I' && fileData[10] == 'F' && fileData[11] == 'F') { - return loadLegacyAIFF(); + const bool ok = loadLegacyAIFF(); + if (ok) + loadedFromFile = true; + return ok; } if (fileData[0] != 'C' || fileData[1] != 'A' || fileData[2] != 'T' || fileData[3] != ' ') @@ -1184,6 +1196,7 @@ class VLFileImpl return fail(VL_ERROR_FILE_CORRUPT); finalizeRenderedLengths(); + loadedFromFile = true; return true; } @@ -1769,6 +1782,8 @@ class VLFileImpl } info.slice_count = (int32_t)be32(d); + globBars = be16(d + 4); + globBeats = d[6]; info.time_sig_num = d[7]; info.time_sig_den = d[8]; analysisSensitivity = d[9]; @@ -1777,11 +1792,17 @@ class VLFileImpl if (tempo < 20000u || tempo > 450000u) fail(VL_ERROR_INVALID_TEMPO); info.tempo = (int32_t)tempo; - info.ppq_length = 61440; + { + const int32_t timeSigNum = info.time_sig_num > 0 ? info.time_sig_num : 4; + const int32_t totalBeats = (int32_t)globBars * timeSigNum + (int32_t)globBeats; + const int64_t ppqLen = totalBeats > 0 ? (int64_t)totalBeats * kREXPPQ : (int64_t)4 * kREXPPQ; + info.ppq_length = (int32_t)std::clamp(ppqLen, (int64_t)1, (int64_t)INT32_MAX); + } processingGain = be16(d + 12); silenceSelected = d[21] != 0; info.processing_gain = processingGain; info.silence_selected = silenceSelected ? 1 : 0; + loadedFromFile = true; } void parseCREI(const uint8_t* d, uint32_t sz) @@ -1876,8 +1897,8 @@ class VLFileImpl static uint16_t rex2FilterPoints(uint8_t sensitivity) { - const uint32_t sens = std::min(sensitivity, 100u); - const uint32_t visibleRange = (sens * 0x7fffu + 99u) / 100u; + const uint32_t sens = std::min(sensitivity, 99u); + const uint32_t visibleRange = (sens * 0x7fffu + 98u) / 99u; return (uint16_t)(0x7fffu - visibleRange); } @@ -2231,6 +2252,9 @@ void normaliseInfoForSave(VLFileImpl& impl) impl.loopStart = (uint32_t)std::max(0, impl.info.loop_start); impl.loopEnd = (uint32_t)std::max(impl.info.loop_start, impl.info.loop_end); + if (!impl.loadedFromFile && impl.analysisSensitivity == 0) + impl.analysisSensitivity = 99; + if (impl.info.ppq_length <= 0) { const uint32_t frames = impl.loopEnd > impl.loopStart ? impl.loopEnd - impl.loopStart : impl.totalFrames; @@ -2240,6 +2264,13 @@ void normaliseInfoForSave(VLFileImpl& impl) impl.info.ppq_length = std::max(1, (int32_t)std::lround(beats * VLFileImpl::kREXPPQ)); } + { + const int32_t timeSigNum = impl.info.time_sig_num > 0 ? impl.info.time_sig_num : 4; + const int32_t totalBeats = std::max(1, (impl.info.ppq_length + VLFileImpl::kREXPPQ / 2) / VLFileImpl::kREXPPQ); + impl.globBars = (uint16_t)(totalBeats / timeSigNum); + impl.globBeats = (uint8_t)(totalBeats % timeSigNum); + } + impl.processingGain = (uint16_t)std::clamp(impl.info.processing_gain > 0 ? impl.info.processing_gain : 1000, 0, 1000); impl.transientEnabled = impl.info.transient_enabled != 0; impl.transientAttack = (uint16_t)std::clamp(impl.info.transient_attack, 0, 1023); @@ -2282,14 +2313,13 @@ std::vector buildREX2File(VLFileImpl& impl) { const size_t c = w.beginChunk("GLOB"); w.put32((uint32_t)impl.slices.size()); - w.put8(0); - w.put8(1); - w.put8(0); + w.put16(impl.globBars); + w.put8(impl.globBeats); w.put8((uint8_t)impl.info.time_sig_num); w.put8((uint8_t)impl.info.time_sig_den); - w.put8(0x4e); - w.put8(0); - w.put8(0); + w.put8(impl.analysisSensitivity); + const uint16_t gate = impl.slices.empty() ? impl.gateSensitivity : std::max(1, impl.gateSensitivity); + w.put16(gate); w.put16(impl.processingGain); w.put16(1); w.put32((uint32_t)impl.info.tempo); @@ -2307,10 +2337,8 @@ std::vector buildREX2File(VLFileImpl& impl) w.put8(0); w.put8(1); w.put8(0); - w.put8(0); - w.put32((uint32_t)impl.info.original_tempo); - w.put16(0); - w.put8(8); + w.put32((uint32_t)(impl.totalFrames * impl.info.channels * (impl.info.bit_depth / 8))); + w.put32((uint32_t)impl.slices.size()); w.endChunk(c); } @@ -2388,9 +2416,6 @@ std::vector buildREX2File(VLFileImpl& impl) struct VLFile_s { VLFileImpl impl; - int32_t outputSampleRate = 44100; - bool isNew = false; - bool dirty = false; }; /* ----------------------------------------------------------------------- @@ -2400,6 +2425,354 @@ struct VLFile_s namespace { +constexpr double kVLTwoPi = 6.283185307179586476925286766559; + +bool isPowerOfTwo(int32_t value) +{ + return value > 0 && (value & (value - 1)) == 0; +} + +VLSuperFluxOptions superfluxDefaults() +{ + VLSuperFluxOptions o = {}; + o.frame_size = 2048; + o.fps = 200; + o.filter_bands = 24; + o.max_bins = 3; + o.diff_frames = 0; + o.min_slice_frames = 0; + o.filter_equal = 0; + o.online = 0; + o.threshold = 1.1f; + o.combine_ms = 50.0f; + o.pre_avg = 0.15f; + o.pre_max = 0.01f; + o.post_avg = 0.0f; + o.post_max = 0.05f; + o.delay_ms = 0.0f; + o.ratio = 0.5f; + o.fmin = 30.0f; + o.fmax = 17000.0f; + o.log_mul = 1.0f; + o.log_add = 1.0f; + return o; +} + +VLError validateSuperFluxOptions(const VLSuperFluxOptions& o, int32_t sampleRate) +{ + if (!isPowerOfTwo(o.frame_size) || o.frame_size < 64 || o.frame_size > 32768) + return VL_ERROR_INVALID_ARG; + + if (o.fps <= 0 || o.fps > sampleRate) + return VL_ERROR_INVALID_ARG; + + if (o.filter_bands < 1 || o.max_bins < 1) + return VL_ERROR_INVALID_ARG; + + if (o.diff_frames < 0) + return VL_ERROR_INVALID_ARG; + + if (o.min_slice_frames < 0) + return VL_ERROR_INVALID_ARG; + + if (!std::isfinite(o.threshold) || !std::isfinite(o.combine_ms) || !std::isfinite(o.pre_avg) || !std::isfinite(o.pre_max) || + !std::isfinite(o.post_avg) || !std::isfinite(o.post_max) || !std::isfinite(o.delay_ms) || !std::isfinite(o.ratio) || + !std::isfinite(o.fmin) || !std::isfinite(o.fmax) || !std::isfinite(o.log_mul) || !std::isfinite(o.log_add)) + { + return VL_ERROR_INVALID_ARG; + } + + if (o.combine_ms < 0.0f || o.pre_avg < 0.0f || o.pre_max < 0.0f || o.post_avg < 0.0f || o.post_max < 0.0f) + return VL_ERROR_INVALID_ARG; + + if (o.ratio < 0.0f || o.ratio > 1.0f) + return VL_ERROR_INVALID_ARG; + + if (o.fmin <= 0.0f || o.fmax <= o.fmin || o.log_mul <= 0.0f || o.log_add <= 0.0f) + return VL_ERROR_INVALID_ARG; + + return VL_OK; +} + +std::vector makeHannWindow(int32_t frameSize) +{ + std::vector window((size_t)frameSize, 1.0f); + if (frameSize <= 1) + return window; + + for (int32_t i = 0; i < frameSize; ++i) + window[(size_t)i] = (float)(0.5 - 0.5 * std::cos(kVLTwoPi * (double)i / (double)(frameSize - 1))); + + return window; +} + +std::vector superfluxFrequencies(int32_t bands, float fmin, float fmax) +{ + std::vector frequencies; + const double factor = std::pow(2.0, 1.0 / (double)bands); + + double freq = 440.0; + frequencies.push_back((float)freq); + while (freq <= (double)fmax) + { + freq *= factor; + frequencies.push_back((float)freq); + } + + freq = 440.0; + while (freq >= (double)fmin) + { + freq /= factor; + frequencies.push_back((float)freq); + } + + std::sort(frequencies.begin(), frequencies.end()); + return frequencies; +} + +bool buildSuperFluxFilterbank(int32_t numFftBins, + int32_t sampleRate, + const VLSuperFluxOptions& options, + std::vector& filterbank, + int32_t& numBands) +{ + const float fmax = std::min(options.fmax, (float)sampleRate * 0.5f); + std::vector frequencies = superfluxFrequencies(options.filter_bands, options.fmin, fmax); + const double factor = ((double)sampleRate * 0.5) / (double)numFftBins; + + std::vector bins; + bins.reserve(frequencies.size()); + for (float frequency : frequencies) + { + const int32_t bin = (int32_t)std::lround((double)frequency / factor); + if (bin >= 0 && bin < numFftBins) + bins.push_back(bin); + } + + std::sort(bins.begin(), bins.end()); + bins.erase(std::unique(bins.begin(), bins.end()), bins.end()); + if (bins.size() < 5) + return false; + + numBands = (int32_t)bins.size() - 2; + if (numBands < 3) + return false; + + filterbank.assign((size_t)numFftBins * (size_t)numBands, 0.0f); + for (int32_t band = 0; band < numBands; ++band) + { + const int32_t start = bins[(size_t)band]; + const int32_t mid = bins[(size_t)band + 1u]; + const int32_t stop = bins[(size_t)band + 2u]; + if (mid <= start || stop <= mid) + continue; + + const float height = options.filter_equal ? 2.0f / (float)(stop - start) : 1.0f; + for (int32_t bin = start; bin < mid; ++bin) + { + const float t = (float)(bin - start) / (float)(mid - start); + filterbank[(size_t)bin * (size_t)numBands + (size_t)band] = t * height; + } + for (int32_t bin = mid; bin < stop; ++bin) + { + const float t = (float)(bin - mid) / (float)(stop - mid); + filterbank[(size_t)bin * (size_t)numBands + (size_t)band] = (1.0f - t) * height; + } + } + + return true; +} + +int32_t deriveSuperFluxDiffFrames(const std::vector& window, double hopSize, float ratio) +{ + size_t sample = 0; + while (sample < window.size() && window[sample] <= ratio) + ++sample; + + const double diffSamples = (double)window.size() * 0.5 - (double)sample; + return std::max(1, (int32_t)std::lround(diffSamples / hopSize)); +} + +bool computeSuperFluxActivations(const float* left, + const float* right, + int32_t channels, + int32_t frames, + int32_t sampleRate, + const VLSuperFluxOptions& options, + std::vector& activations) +{ + const int32_t frameSize = options.frame_size; + const int32_t numFftBins = frameSize / 2; + const double hopSize = (double)sampleRate / (double)options.fps; + const int32_t numFrames = std::max(1, (int32_t)std::ceil((double)frames / hopSize)); + + std::vector window = makeHannWindow(frameSize); + std::vector filterbank; + int32_t numBands = 0; + if (!buildSuperFluxFilterbank(numFftBins, sampleRate, options, filterbank, numBands)) + return false; + + std::vector spec((size_t)numFrames * (size_t)numBands, 0.0f); + std::vector fftBuffer((size_t)frameSize); + std::vector magnitudes((size_t)numFftBins); + std::vector fftIp(2 + (size_t)frameSize, 0); + std::vector fftW((size_t)frameSize / 2); + + for (int32_t frame = 0; frame < numFrames; ++frame) + { + const int32_t seek = options.online ? (int32_t)((double)(frame + 1) * hopSize - (double)frameSize) + : (int32_t)((double)frame * hopSize - (double)frameSize * 0.5); + + for (int32_t i = 0; i < frameSize; ++i) + { + const int32_t src = seek + i; + float sample = 0.0f; + if (src >= 0 && src < frames) + { + const float l = std::isfinite(left[src]) ? left[src] : 0.0f; + if (channels == 2) + { + const float r = std::isfinite(right[src]) ? right[src] : 0.0f; + sample = (l + r) * 0.5f; + } + else + { + sample = l; + } + } + fftBuffer[(size_t)i] = (double)(sample * window[(size_t)i]); + } + + rdft(frameSize, 1, fftBuffer.data(), fftIp.data(), fftW.data()); + + magnitudes[0] = (float)std::abs(fftBuffer[0]); + for (int32_t bin = 1; bin < numFftBins; ++bin) + { + const double re = fftBuffer[(size_t)bin * 2]; + const double im = fftBuffer[(size_t)bin * 2 + 1]; + magnitudes[(size_t)bin] = (float)std::sqrt(re * re + im * im); + } + + for (int32_t band = 0; band < numBands; ++band) + { + double value = 0.0; + for (int32_t bin = 0; bin < numFftBins; ++bin) + value += (double)magnitudes[(size_t)bin] * (double)filterbank[(size_t)bin * (size_t)numBands + (size_t)band]; + + const float logged = std::log10(options.log_mul * (float)value + options.log_add); + spec[(size_t)frame * (size_t)numBands + (size_t)band] = logged; + } + } + + const int32_t diffFrames = + options.diff_frames > 0 ? options.diff_frames : deriveSuperFluxDiffFrames(window, hopSize, options.ratio); + + activations.assign((size_t)numFrames, 0.0f); + for (int32_t frame = diffFrames; frame < numFrames; ++frame) + { + float sum = 0.0f; + const int32_t prevFrame = frame - diffFrames; + for (int32_t band = 0; band < numBands; ++band) + { + int32_t first = band - options.max_bins / 2; + int32_t last = first + options.max_bins - 1; + if (first < 0) + { + last -= first; + first = 0; + } + if (last >= numBands) + { + first = std::max(0, first - (last - numBands + 1)); + last = numBands - 1; + } + float prevMax = spec[(size_t)prevFrame * (size_t)numBands + (size_t)band]; + for (int32_t k = first; k <= last; ++k) + prevMax = std::max(prevMax, spec[(size_t)prevFrame * (size_t)numBands + (size_t)k]); + + const float diff = spec[(size_t)frame * (size_t)numBands + (size_t)band] - prevMax; + if (diff > 0.0f) + sum += diff; + } + activations[(size_t)frame] = sum; + } + + return true; +} + +std::vector pickSuperFluxOnsets(const std::vector& activations, int32_t fps, const VLSuperFluxOptions& options) +{ + const int32_t count = (int32_t)activations.size(); + const int32_t preAvg = std::max(0, (int32_t)std::lround((double)fps * (double)options.pre_avg)); + const int32_t preMax = std::max(0, (int32_t)std::lround((double)fps * (double)options.pre_max)); + const int32_t postAvg = options.online ? 0 : std::max(0, (int32_t)std::lround((double)fps * (double)options.post_avg)); + const int32_t postMax = options.online ? 0 : std::max(0, (int32_t)std::lround((double)fps * (double)options.post_max)); + const double combineSeconds = (double)options.combine_ms / 1000.0; + const double delaySeconds = (double)options.delay_ms / 1000.0; + + std::vector detections; + double lastDetection = -std::numeric_limits::infinity(); + for (int32_t frame = 0; frame < count; ++frame) + { + const int32_t maxStart = std::max(0, frame - preMax); + const int32_t maxStop = std::min(count - 1, frame + postMax); + float movMax = 0.0f; + for (int32_t i = maxStart; i <= maxStop; ++i) + movMax = std::max(movMax, activations[(size_t)i]); + + const int32_t avgStart = std::max(0, frame - preAvg); + const int32_t avgStop = std::min(count - 1, frame + postAvg); + double avg = 0.0; + for (int32_t i = avgStart; i <= avgStop; ++i) + avg += activations[(size_t)i]; + avg /= (double)(avgStop - avgStart + 1); + + const float value = activations[(size_t)frame]; + if (value <= 0.0f || value != movMax || (double)value < avg + (double)options.threshold) + continue; + + const double time = (double)frame / (double)fps + delaySeconds; + if (detections.empty() || time - lastDetection > combineSeconds) + { + detections.push_back(time); + lastDetection = time; + } + } + + return detections; +} + +std::vector superfluxSliceBoundaries(const std::vector& detections, + int32_t frames, + int32_t sampleRate, + const VLSuperFluxOptions& options) +{ + const uint32_t combineFrames = + (uint32_t)std::max(1, (int32_t)std::lround((double)options.combine_ms / 1000.0 * (double)sampleRate)); + const uint32_t minSliceFrames = + (uint32_t)(options.min_slice_frames > 0 ? (uint32_t)options.min_slice_frames : std::max(combineFrames, (uint32_t)std::max(1, (int32_t)std::lround((double)sampleRate * 0.01)))); + + std::vector boundaries; + boundaries.push_back(0); + + for (double detection : detections) + { + const int64_t rounded = (int64_t)std::llround(detection * (double)sampleRate); + if (rounded <= 0 || rounded >= frames) + continue; + + const uint32_t sample = (uint32_t)rounded; + if (sample - boundaries.back() >= minSliceFrames) + boundaries.push_back(sample); + } + + if ((uint32_t)frames - boundaries.back() < minSliceFrames && boundaries.size() > 1) + boundaries.pop_back(); + + boundaries.push_back((uint32_t)frames); + return boundaries; +} + int32_t addSliceAtSample(VLFile file, uint32_t sample_start, int32_t ppq_pos, const float* left, const float* right, int32_t frames) { if (!file) @@ -2454,7 +2827,6 @@ int32_t addSliceAtSample(VLFile file, uint32_t sample_start, int32_t ppq_pos, co file->impl.info.slice_count = (int32_t)file->impl.slices.size(); if (file->impl.info.loop_end <= file->impl.info.loop_start) file->impl.info.loop_end = file->impl.info.total_frames; - file->dirty = true; return (int32_t)file->impl.slices.size() - 1; } @@ -2528,7 +2900,6 @@ VLFile vl_open_from_memory(const void* data, size_t size, VLError* err) return nullptr; } - h->outputSampleRate = h->impl.info.sample_rate ? h->impl.info.sample_rate : 44100; set(VL_OK); return h; } @@ -2566,9 +2937,6 @@ VLFile vl_create_new(int32_t channels, int32_t sample_rate, int32_t tempo, VLErr return nullptr; } - h->isNew = true; - h->outputSampleRate = sample_rate; - h->impl.info.channels = channels; h->impl.info.sample_rate = sample_rate; h->impl.info.slice_count = 0; @@ -2598,6 +2966,151 @@ VLFile vl_create_new(int32_t channels, int32_t sample_rate, int32_t tempo, VLErr return h; } +void vl_superflux_default_options(VLSuperFluxOptions* out) +{ + if (out) + *out = superfluxDefaults(); +} + +VLFile vl_create_from_superflux(int32_t channels, + int32_t sample_rate, + int32_t tempo, + const float* left, + const float* right, + int32_t frames, + const VLSuperFluxOptions* options, + VLError* err) +{ + auto set = [&](VLError e) + { + if (err) + *err = e; + }; + + if (channels != 1 && channels != 2) + { + set(VL_ERROR_INVALID_ARG); + return nullptr; + } + + if (sample_rate < 8000 || sample_rate > 192000) + { + set(VL_ERROR_INVALID_SAMPLE_RATE); + return nullptr; + } + + if (tempo <= 0) + { + set(VL_ERROR_INVALID_TEMPO); + return nullptr; + } + + if (!left || frames <= 0 || (channels == 2 && !right)) + { + set(VL_ERROR_INVALID_ARG); + return nullptr; + } + + VLSuperFluxOptions opts = options ? *options : superfluxDefaults(); + VLError optionError = validateSuperFluxOptions(opts, sample_rate); + if (optionError != VL_OK) + { + set(optionError); + return nullptr; + } + + try + { + std::vector activations; + if (!computeSuperFluxActivations(left, right, channels, frames, sample_rate, opts, activations)) + { + set(VL_ERROR_INVALID_ARG); + return nullptr; + } + + const std::vector detections = pickSuperFluxOnsets(activations, opts.fps, opts); + const std::vector boundaries = superfluxSliceBoundaries(detections, frames, sample_rate, opts); + + VLError createError = VL_OK; + VLFile file = vl_create_new(channels, sample_rate, tempo, &createError); + if (!file) + { + set(createError); + return nullptr; + } + + VLFileInfo info = {}; + if (vl_get_info(file, &info) != VL_OK) + { + vl_close(file); + set(VL_ERROR_INVALID_HANDLE); // LCOV_EXCL_LINE freshly-created handles are valid. + return nullptr; + } + + const double beats = ((double)frames * (double)tempo) / (60000.0 * (double)sample_rate); + info.ppq_length = std::max(1, (int32_t)std::lround(beats * (double)VLFileImpl::kREXPPQ)); + info.total_frames = frames; + info.loop_start = 0; + info.loop_end = frames; + info.transient_enabled = 0; + info.transient_stretch = 0; + info.transient_attack = 0; + info.transient_decay = 1023; + VLError infoError = vl_set_info(file, &info); + if (infoError != VL_OK) + { + vl_close(file); + set(infoError); + return nullptr; + } + + int32_t previousPpq = -1; + for (size_t i = 0; i + 1 < boundaries.size(); ++i) + { + const uint32_t start = boundaries[i]; + const uint32_t stop = boundaries[i + 1u]; + if (stop <= start) + continue; + + int32_t ppq = (int32_t)(((uint64_t)start * (uint64_t)info.ppq_length + (uint32_t)frames / 2u) / (uint32_t)frames); + if (ppq <= previousPpq) + ppq = previousPpq + 1; + ppq = std::min(ppq, std::max(0, info.ppq_length - 1)); + previousPpq = ppq; + + const int32_t sliceIndex = + addSliceAtSample(file, start, ppq, left + start, channels == 2 ? right + start : nullptr, (int32_t)(stop - start)); + if (sliceIndex < 0) + { + const VLError sliceError = (VLError)sliceIndex; + vl_close(file); + set(sliceError); + return nullptr; + } + } + + if (file->impl.slices.empty()) + { + vl_close(file); + set(VL_ERROR_INVALID_ARG); // LCOV_EXCL_LINE boundaries always create at least one slice for frames > 0. + return nullptr; + } + + set(VL_OK); + return file; + } + catch (const std::bad_alloc&) + { + set(VL_ERROR_OUT_OF_MEMORY); + return nullptr; + } + catch (...) + { + set(VL_ERROR_OUT_OF_MEMORY); + return nullptr; + } +} + void vl_close(VLFile file) { delete file; @@ -2671,7 +3184,6 @@ VLError vl_set_slice_info(VLFile file, int32_t index, int32_t flags, int32_t ana if (err != VL_OK) return err; - file->dirty = true; return VL_OK; } @@ -2679,21 +3191,6 @@ VLError vl_set_slice_info(VLFile file, int32_t index, int32_t flags, int32_t ana Read: sample extraction ----------------------------------------------------------------------- */ -VLError vl_set_output_sample_rate(VLFile file, int32_t rate) -{ - if (!file) - return VL_ERROR_INVALID_HANDLE; - - if (rate < 8000 || rate > 192000) - return VL_ERROR_INVALID_SAMPLE_RATE; - - if (rate != file->impl.info.sample_rate) - return VL_ERROR_NOT_IMPLEMENTED; - - file->outputSampleRate = rate; - return VL_OK; -} - int32_t vl_get_slice_frame_count(VLFile file, int32_t index) { if (!file) @@ -2864,8 +3361,6 @@ VLError vl_set_info(VLFile file, const VLFileInfo* info) file->impl.transientDecay = (uint16_t)std::clamp(info->transient_decay > 0 ? info->transient_decay : 1023, 0, 1023); file->impl.transientStretch = (uint16_t)std::clamp(info->transient_stretch, 0, 100); file->impl.silenceSelected = info->silence_selected != 0; - file->outputSampleRate = info->sample_rate; - file->dirty = true; return VL_OK; } @@ -2887,7 +3382,6 @@ VLError vl_set_creator_info(VLFile file, const VLCreatorInfo* info) file->impl.creator.url[sizeof(file->impl.creator.url) - 1] = '\0'; file->impl.creator.email[sizeof(file->impl.creator.email) - 1] = '\0'; file->impl.creator.free_text[sizeof(file->impl.creator.free_text) - 1] = '\0'; - file->dirty = true; return VL_OK; } @@ -2914,7 +3408,6 @@ VLError vl_remove_slice(VLFile file, int32_t index) file->impl.slices.erase(file->impl.slices.begin() + index); file->impl.info.slice_count = (int32_t)file->impl.slices.size(); - file->dirty = true; return VL_OK; } @@ -2953,28 +3446,7 @@ VLError vl_save_to_memory(VLFile file, void* buf, size_t* size_out) if (!size_out) return VL_ERROR_INVALID_ARG; - if (!file->isNew && !file->dirty && !file->impl.fileData.empty()) - { - const std::vector& encoded = file->impl.fileData; - - if (!buf) - { - *size_out = encoded.size(); - return VL_OK; - } - - if (*size_out < encoded.size()) - { - *size_out = encoded.size(); - return VL_ERROR_BUFFER_TOO_SMALL; - } - - std::memcpy(buf, encoded.data(), encoded.size()); - *size_out = encoded.size(); - return VL_OK; - } - - if (file->impl.slices.empty() || file->impl.pcm.empty()) + if (file->impl.pcm.empty()) return VL_ERROR_INVALID_ARG; std::vector encoded = buildREX2File(file->impl); @@ -3026,5 +3498,5 @@ const char* vl_error_string(VLError err) const char* vl_version_string(void) { - return "velociloops 0.1.0"; + return "0.2.0"; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d41922d..00235b7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,36 +10,20 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(doctest) -add_executable(velociloops_tests test_velociloops.cpp) -target_link_libraries(velociloops_tests PRIVATE velociloops_static doctest::doctest) -target_compile_features(velociloops_tests PRIVATE cxx_std_17) -target_compile_definitions(velociloops_tests - PRIVATE - VELOCILOOPS_TEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" -) - -add_test(NAME velociloops_tests COMMAND velociloops_tests) - -if (VELOCILOOPS_COVERAGE) - target_compile_options(velociloops_tests PRIVATE --coverage) - target_link_options(velociloops_tests PRIVATE --coverage) -endif() - -if (VELOCILOOPS_SANITIZERS) - target_compile_options(velociloops_tests PRIVATE - -fsanitize=address,undefined -fno-omit-frame-pointer -g) - target_link_options(velociloops_tests PRIVATE - -fsanitize=address,undefined) -endif() +function(velociloops_declare_test TARGET_NAME) + add_executable(${TARGET_NAME} ${ARGN} test_main.cpp) + target_link_libraries(${TARGET_NAME} PRIVATE velociloops_static doctest::doctest) + target_compile_features(${TARGET_NAME} PRIVATE cxx_std_17) + target_compile_definitions(${TARGET_NAME} + PRIVATE + VELOCILOOPS_TEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" + ) + if (VELOCILOOPS_ENABLE_COVERAGE) + target_compile_options(${TARGET_NAME} PRIVATE --coverage) + target_link_options(${TARGET_NAME} PRIVATE --coverage) + endif() + add_test(NAME ${TARGET_NAME} COMMAND ${TARGET_NAME}) +endfunction() -if (VELOCILOOPS_ENABLE_FUZZING) - add_executable(velociloops_fuzzer fuzz_velociloops.cpp) - target_link_libraries(velociloops_fuzzer PRIVATE velociloops_static) - target_compile_features(velociloops_fuzzer PRIVATE cxx_std_17) - target_compile_definitions(velociloops_fuzzer - PRIVATE VELOCILOOPS_TEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data") - target_compile_options(velociloops_fuzzer PRIVATE - -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer -g) - target_link_options(velociloops_fuzzer PRIVATE - -fsanitize=fuzzer,address,undefined) -endif() +velociloops_declare_test(velociloops_tests test_velociloops.cpp) +velociloops_declare_test(velociloops_superflux test_superflux.cpp) diff --git a/tests/data/120Mono copy.rx2 b/tests/data/120Mono copy.rx2 deleted file mode 100644 index 1cbcd98..0000000 Binary files a/tests/data/120Mono copy.rx2 and /dev/null differ diff --git a/tests/data/120Mono copy.wav b/tests/data/120Mono copy.wav deleted file mode 100644 index 55fdc95..0000000 Binary files a/tests/data/120Mono copy.wav and /dev/null differ diff --git a/tests/data/120Stereo copy.rx2 b/tests/data/120Stereo copy.rx2 deleted file mode 100644 index 214aba7..0000000 Binary files a/tests/data/120Stereo copy.rx2 and /dev/null differ diff --git a/tests/data/120Stereo copy.wav b/tests/data/120Stereo copy.wav deleted file mode 100644 index ffac9a1..0000000 Binary files a/tests/data/120Stereo copy.wav and /dev/null differ diff --git a/tests/test_main.cpp b/tests/test_main.cpp new file mode 100644 index 0000000..0a3f254 --- /dev/null +++ b/tests/test_main.cpp @@ -0,0 +1,2 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include diff --git a/tests/test_superflux.cpp b/tests/test_superflux.cpp new file mode 100644 index 0000000..a194933 --- /dev/null +++ b/tests/test_superflux.cpp @@ -0,0 +1,465 @@ +#include "velociloops.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +namespace fs = std::filesystem; + +const fs::path kDataDir = VELOCILOOPS_TEST_DATA_DIR; + +struct ScopedVLFile +{ + VLFile handle = nullptr; + + explicit ScopedVLFile(VLFile h = nullptr) : handle(h) {} + ~ScopedVLFile() { vl_close(handle); } + + ScopedVLFile(const ScopedVLFile&) = delete; + ScopedVLFile& operator=(const ScopedVLFile&) = delete; + + explicit operator bool() const { return handle != nullptr; } +}; + +uint16_t le16(const uint8_t* p) +{ + return (uint16_t)(p[0] | ((uint16_t)p[1] << 8)); +} + +uint32_t le32(const uint8_t* p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +struct WavAudio +{ + uint16_t channels = 0; + uint32_t sampleRate = 0; + uint32_t frameCount = 0; + std::vector left; + std::vector right; // empty for mono + bool valid = false; +}; + +WavAudio loadWavFloat(const fs::path& path) +{ + std::ifstream in(path, std::ios::binary); + if (!in) + return {}; + + const std::vector bytes{std::istreambuf_iterator(in), {}}; + + if (bytes.size() < 12 || std::memcmp(bytes.data(), "RIFF", 4) != 0 || std::memcmp(bytes.data() + 8, "WAVE", 4) != 0) + return {}; + + uint16_t audioFormat = 0, channels = 0, bitDepth = 0; + uint32_t sampleRate = 0; + size_t dataOffset = 0; + uint32_t dataSize = 0; + + size_t off = 12; + while (off + 8 <= bytes.size()) + { + const uint8_t* chunk = bytes.data() + off; + const uint32_t size = le32(chunk + 4); + off += 8; + if (off + size > bytes.size()) + return {}; + + if (std::memcmp(chunk, "fmt ", 4) == 0 && size >= 16) + { + audioFormat = le16(bytes.data() + off); + channels = le16(bytes.data() + off + 2); + sampleRate = le32(bytes.data() + off + 4); + bitDepth = le16(bytes.data() + off + 14); + } + else if (std::memcmp(chunk, "data", 4) == 0) + { + dataOffset = off; + dataSize = size; + } + + off += size + (size & 1u); + } + + if (audioFormat != 1 || channels < 1 || channels > 2 || dataSize == 0) + return {}; + if (bitDepth != 16 && bitDepth != 24) + return {}; + + const uint32_t bytesPerFrame = channels * (bitDepth / 8); + const uint32_t frameCount = dataSize / bytesPerFrame; + + WavAudio out; + out.channels = channels; + out.sampleRate = sampleRate; + out.frameCount = frameCount; + out.left.resize(frameCount); + if (channels == 2) + out.right.resize(frameCount); + + if (bitDepth == 16) + { + for (uint32_t i = 0; i < frameCount; ++i) + { + const size_t base = dataOffset + (size_t)i * bytesPerFrame; + out.left[i] = (float)(int16_t)le16(bytes.data() + base) * (1.0f / 32768.0f); + if (channels == 2) + out.right[i] = (float)(int16_t)le16(bytes.data() + base + 2) * (1.0f / 32768.0f); + } + } + else + { + for (uint32_t i = 0; i < frameCount; ++i) + { + const size_t base = dataOffset + (size_t)i * bytesPerFrame; + const uint8_t* p = bytes.data() + base; + const int32_t s = (int32_t)p[0] | ((int32_t)p[1] << 8) | ((int32_t)(int8_t)p[2] << 16); + out.left[i] = (float)s * (1.0f / 8388608.0f); + if (channels == 2) + { + const uint8_t* q = p + 3; + const int32_t s2 = (int32_t)q[0] | ((int32_t)q[1] << 8) | ((int32_t)(int8_t)q[2] << 16); + out.right[i] = (float)s2 * (1.0f / 8388608.0f); + } + } + } + + out.valid = true; + return out; +} + +std::vector wavFixtures() +{ + std::vector files; + for (const auto& entry : fs::directory_iterator(kDataDir)) + { + if (entry.is_regular_file() && entry.path().extension() == ".wav") + files.push_back(entry.path()); + } + std::sort(files.begin(), files.end()); + return files; +} + +} // namespace + +TEST_CASE("superflux fixture suite") +{ + const auto fixtures = wavFixtures(); + CHECK(!fixtures.empty()); + + for (const auto& path : fixtures) + { + CAPTURE(path.filename().string()); + + const WavAudio wav = loadWavFloat(path); + REQUIRE(wav.valid); + REQUIRE(wav.frameCount > 0); + + VLError err = VL_OK; + ScopedVLFile file(vl_create_from_superflux( + (int32_t)wav.channels, + (int32_t)wav.sampleRate, + 120000, + wav.left.data(), + wav.right.empty() ? nullptr : wav.right.data(), + (int32_t)wav.frameCount, + nullptr, + &err)); + + REQUIRE(file); + CHECK(err == VL_OK); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.channels == (int32_t)wav.channels); + CHECK(info.sample_rate == (int32_t)wav.sampleRate); + CHECK(info.total_frames == (int32_t)wav.frameCount); + CHECK(info.slice_count >= 1); + CHECK(info.loop_start == 0); + CHECK(info.loop_end == (int32_t)wav.frameCount); + + int32_t nextExpectedStart = info.loop_start; + int32_t prevPpq = -1; + for (int32_t i = 0; i < info.slice_count; ++i) + { + CAPTURE(i); + VLSliceInfo slice = {}; + REQUIRE(vl_get_slice_info(file.handle, i, &slice) == VL_OK); + + CHECK(slice.sample_start == nextExpectedStart); + CHECK(slice.sample_length > 0); + CHECK(slice.ppq_pos >= prevPpq); + + nextExpectedStart = slice.sample_start + slice.sample_length; + prevPpq = slice.ppq_pos; + + const int32_t frames = vl_get_slice_frame_count(file.handle, i); + REQUIRE(frames > 0); + + std::vector left((size_t)frames); + std::vector right(info.channels == 2 ? (size_t)frames : 0u); + int32_t written = 0; + REQUIRE(vl_decode_slice(file.handle, i, + left.data(), + right.empty() ? nullptr : right.data(), + 0, frames, &written) == VL_OK); + CHECK(written == frames); + CHECK(std::all_of(left.begin(), left.end(), [](float s) { return std::isfinite(s); })); + CHECK(std::all_of(right.begin(), right.end(), [](float s) { return std::isfinite(s); })); + } + + CHECK(nextExpectedStart == info.loop_end); + } +} + +TEST_CASE("superflux output can be saved and reopened with consistent metadata") +{ + const WavAudio wav = loadWavFloat(kDataDir / "120Stereo.wav"); + REQUIRE(wav.valid); + CHECK(wav.channels == 2); + + VLError err = VL_OK; + ScopedVLFile file(vl_create_from_superflux( + 2, (int32_t)wav.sampleRate, 120000, + wav.left.data(), wav.right.data(), (int32_t)wav.frameCount, + nullptr, &err)); + REQUIRE(file); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + + size_t size = 0; + REQUIRE(vl_save_to_memory(file.handle, nullptr, &size) == VL_OK); + REQUIRE(size > 64); + + std::vector bytes(size); + REQUIRE(vl_save_to_memory(file.handle, bytes.data(), &size) == VL_OK); + + ScopedVLFile reopened(vl_open_from_memory(bytes.data(), bytes.size(), &err)); + REQUIRE(reopened); + + VLFileInfo reopenedInfo = {}; + REQUIRE(vl_get_info(reopened.handle, &reopenedInfo) == VL_OK); + CHECK(reopenedInfo.channels == info.channels); + CHECK(reopenedInfo.sample_rate == info.sample_rate); + CHECK(reopenedInfo.slice_count == info.slice_count); + CHECK(reopenedInfo.total_frames == info.total_frames); + CHECK(reopenedInfo.loop_start == info.loop_start); + CHECK(reopenedInfo.loop_end == info.loop_end); + + for (int32_t i = 0; i < reopenedInfo.slice_count; ++i) + { + CAPTURE(i); + const int32_t frames = vl_get_slice_frame_count(reopened.handle, i); + REQUIRE(frames > 0); + + std::vector left((size_t)frames); + std::vector right((size_t)frames); + int32_t written = 0; + REQUIRE(vl_decode_slice(reopened.handle, i, left.data(), right.data(), 0, frames, &written) == VL_OK); + CHECK(written == frames); + CHECK(std::all_of(left.begin(), left.end(), [](float s) { return std::isfinite(s); })); + CHECK(std::all_of(right.begin(), right.end(), [](float s) { return std::isfinite(s); })); + } +} + +TEST_CASE("superflux lower threshold detects more onsets") +{ + const WavAudio wav = loadWavFloat(kDataDir / "120Mono.wav"); + REQUIRE(wav.valid); + + auto countSlices = [&](float threshold) -> int32_t + { + VLSuperFluxOptions opts = {}; + vl_superflux_default_options(&opts); + opts.threshold = threshold; + + VLError err = VL_OK; + ScopedVLFile file(vl_create_from_superflux( + 1, (int32_t)wav.sampleRate, 120000, + wav.left.data(), nullptr, (int32_t)wav.frameCount, + &opts, &err)); + if (!file) + return 0; + VLFileInfo info = {}; + vl_get_info(file.handle, &info); + return info.slice_count; + }; + + const int32_t countLowThreshold = countSlices(0.5f); + const int32_t countHighThreshold = countSlices(5.0f); + + CHECK(countLowThreshold >= countHighThreshold); + CHECK(countLowThreshold >= 1); + CHECK(countHighThreshold >= 1); +} + +TEST_CASE("superflux very high threshold produces 1 slice") +{ + const WavAudio wav = loadWavFloat(kDataDir / "120FourBeats.wav"); + REQUIRE(wav.valid); + + VLSuperFluxOptions opts = {}; + vl_superflux_default_options(&opts); + opts.threshold = 1000.0f; + + VLError err = VL_OK; + ScopedVLFile file(vl_create_from_superflux( + (int32_t)wav.channels, + (int32_t)wav.sampleRate, + 120000, + wav.left.data(), + wav.right.empty() ? nullptr : wav.right.data(), + (int32_t)wav.frameCount, + &opts, &err)); + REQUIRE(file); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.slice_count == 1); + CHECK(info.total_frames == (int32_t)wav.frameCount); +} + +TEST_CASE("superflux larger combine_ms produces fewer or equal slices") +{ + const WavAudio wav = loadWavFloat(kDataDir / "240FiveHundredSlices.wav"); + REQUIRE(wav.valid); + + auto countSlices = [&](float combineMs) -> int32_t + { + VLSuperFluxOptions opts = {}; + vl_superflux_default_options(&opts); + opts.combine_ms = combineMs; + + VLError err = VL_OK; + ScopedVLFile file(vl_create_from_superflux( + (int32_t)wav.channels, + (int32_t)wav.sampleRate, + 240000, + wav.left.data(), + wav.right.empty() ? nullptr : wav.right.data(), + (int32_t)wav.frameCount, + &opts, &err)); + if (!file) + return 0; + VLFileInfo info = {}; + vl_get_info(file.handle, &info); + return info.slice_count; + }; + + const int32_t countTight = countSlices(10.0f); + const int32_t countLoose = countSlices(200.0f); + + CHECK(countTight >= countLoose); + CHECK(countLoose >= 1); +} + +TEST_CASE("superflux min_slice_frames enforces minimum distance between slice starts") +{ + const WavAudio wav = loadWavFloat(kDataDir / "120FourBeats.wav"); + REQUIRE(wav.valid); + + const int32_t minFrames = (int32_t)wav.sampleRate / 10; // 100 ms + + VLSuperFluxOptions opts = {}; + vl_superflux_default_options(&opts); + opts.threshold = 0.5f; + opts.min_slice_frames = minFrames; + + VLError err = VL_OK; + ScopedVLFile file(vl_create_from_superflux( + (int32_t)wav.channels, + (int32_t)wav.sampleRate, + 120000, + wav.left.data(), + wav.right.empty() ? nullptr : wav.right.data(), + (int32_t)wav.frameCount, + &opts, &err)); + REQUIRE(file); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + + for (int32_t i = 0; i + 1 < info.slice_count; ++i) + { + CAPTURE(i); + VLSliceInfo curr = {}; + VLSliceInfo next = {}; + REQUIRE(vl_get_slice_info(file.handle, i, &curr) == VL_OK); + REQUIRE(vl_get_slice_info(file.handle, i + 1, &next) == VL_OK); + CHECK(next.sample_start - curr.sample_start >= minFrames); + } +} + +TEST_CASE("superflux handles 24-bit mono WAV correctly") +{ + const WavAudio wav = loadWavFloat(kDataDir / "120Mono24Bits.wav"); + REQUIRE(wav.valid); + CHECK(wav.channels == 1); + + VLError err = VL_OK; + ScopedVLFile file(vl_create_from_superflux( + 1, (int32_t)wav.sampleRate, 120000, + wav.left.data(), nullptr, (int32_t)wav.frameCount, + nullptr, &err)); + + REQUIRE(file); + CHECK(err == VL_OK); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.channels == 1); + CHECK(info.sample_rate == (int32_t)wav.sampleRate); + CHECK(info.total_frames == (int32_t)wav.frameCount); + CHECK(info.slice_count >= 1); + CHECK(info.loop_end == (int32_t)wav.frameCount); +} + +TEST_CASE("superflux stereo WAV produces stereo output with decodable channels") +{ + const WavAudio wav = loadWavFloat(kDataDir / "120Stereo.wav"); + REQUIRE(wav.valid); + CHECK(wav.channels == 2); + + VLError err = VL_OK; + ScopedVLFile file(vl_create_from_superflux( + 2, (int32_t)wav.sampleRate, 120000, + wav.left.data(), wav.right.data(), (int32_t)wav.frameCount, + nullptr, &err)); + REQUIRE(file); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.channels == 2); + CHECK(info.slice_count >= 1); + + const int32_t frames = vl_get_slice_frame_count(file.handle, 0); + REQUIRE(frames > 0); + + std::vector left((size_t)frames); + std::vector right((size_t)frames); + int32_t written = 0; + REQUIRE(vl_decode_slice(file.handle, 0, left.data(), right.data(), 0, frames, &written) == VL_OK); + CHECK(written == frames); + CHECK(std::all_of(left.begin(), left.end(), [](float s) { return std::isfinite(s); })); + CHECK(std::all_of(right.begin(), right.end(), [](float s) { return std::isfinite(s); })); + + const double leftRightDiff = std::inner_product( + left.begin(), left.end(), right.begin(), 0.0, + std::plus(), [](float l, float r) { return std::fabs((double)l - (double)r); }); + CHECK(leftRightDiff > 0.0); +} + diff --git a/tests/test_velociloops.cpp b/tests/test_velociloops.cpp index 3d62037..3145b4e 100644 --- a/tests/test_velociloops.cpp +++ b/tests/test_velociloops.cpp @@ -1,4 +1,3 @@ -#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include "velociloops.h" #include @@ -13,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +22,8 @@ namespace thread_local bool gTrackAllocations = false; thread_local size_t gAllocationCount = 0; +thread_local bool gFailNextAllocation = false; +thread_local bool gFailNextNothrowAllocation = false; void noteAllocation() noexcept { @@ -31,6 +33,11 @@ void noteAllocation() noexcept void* allocateForTest(size_t size) { + if (gFailNextAllocation) + { + gFailNextAllocation = false; + throw std::bad_alloc(); + } noteAllocation(); if (void* p = std::malloc(size == 0 ? 1 : size)) return p; @@ -39,6 +46,11 @@ void* allocateForTest(size_t size) void* allocateForTest(size_t size, const std::nothrow_t&) noexcept { + if (gFailNextNothrowAllocation) + { + gFailNextNothrowAllocation = false; + return nullptr; + } noteAllocation(); return std::malloc(size == 0 ? 1 : size); } @@ -210,6 +222,11 @@ uint32_t be32(const uint8_t* p) return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3]; } +uint16_t be16(const uint8_t* p) +{ + return (uint16_t)(((uint16_t)p[0] << 8) | (uint16_t)p[1]); +} + void putBe32(std::vector& bytes, size_t off, uint32_t value) { REQUIRE(off + 4 <= bytes.size()); @@ -344,10 +361,9 @@ std::set expectedOpenableContainers() { return { "100HasCreatorInfo.rx2", "100WeirdSampleRate.rx2", "120AllMuted.rx2", "120FourBeats.rx2", - "120Gated.rx2", "120GatedMuted.rx2", "120Mono copy.rx2", "120Mono24Bits.rx2", - "120Mono.rx2", "120SevenEights.rx2", "120Stereo copy.rx2", "120Stereo.rx2", - "120ThreeBeats.rx2", "120TransmitAsOneSlice.rx2", "120RcyTest.rcy", "120RexTest.rex", - "240FiveHundredSlices.rx2", "450OneHundredBars.rx2", + "120Gated.rx2", "120GatedMuted.rx2", "120Mono24Bits.rx2", "120Mono.rx2", + "120SevenEights.rx2", "120Stereo.rx2", "120ThreeBeats.rx2", "120TransmitAsOneSlice.rx2", + "120RcyTest.rcy", "120RexTest.rex", "240FiveHundredSlices.rx2", "450OneHundredBars.rx2", }; } @@ -435,6 +451,51 @@ void checkDecodedBuffer(const std::vector& buffer) })); } +void addSyntheticOnset(std::vector& left, std::vector* right, size_t start) +{ + const size_t burstFrames = 768; + for (size_t i = 0; i < burstFrames && start + i < left.size(); ++i) + { + const float env = 1.0f - (float)i / (float)burstFrames; + const float value = std::sin((float)i * 0.58f) * env * 0.85f; + left[start + i] += value; + if (right) + (*right)[start + i] += std::cos((float)i * 0.43f) * env * 0.55f; + } +} + +bool hasSliceNear(VLFile file, int32_t sliceCount, int32_t expectedStart, int32_t tolerance) +{ + for (int32_t i = 0; i < sliceCount; ++i) + { + VLSliceInfo slice = {}; + if (vl_get_slice_info(file, i, &slice) == VL_OK && std::abs(slice.sample_start - expectedStart) <= tolerance) + return true; + } + return false; +} + +void checkSliceMetadataMatches(VLFile expectedFile, VLFile actualFile, int32_t sliceCount, bool comparePpq) +{ + constexpr int32_t kSerializedFlags = VL_SLICE_FLAG_MUTED | VL_SLICE_FLAG_LOCKED | VL_SLICE_FLAG_SELECTED; + + for (int32_t index = 0; index < sliceCount; ++index) + { + CAPTURE(index); + VLSliceInfo expected = {}; + VLSliceInfo actual = {}; + REQUIRE(vl_get_slice_info(expectedFile, index, &expected) == VL_OK); + REQUIRE(vl_get_slice_info(actualFile, index, &actual) == VL_OK); + + if (comparePpq) + CHECK(actual.ppq_pos == expected.ppq_pos); + CHECK(actual.sample_start == expected.sample_start); + CHECK(actual.sample_length == expected.sample_length); + CHECK(actual.analysis_points == expected.analysis_points); + CHECK((actual.flags & kSerializedFlags) == (expected.flags & kSerializedFlags)); + } +} + DecodedFile decodeWholeFile(VLFile file) { DecodedFile decoded; @@ -610,7 +671,8 @@ std::vector buildSmallEncodedFile(int32_t channels = 1, bool withCreato TEST_CASE("utility functions expose stable strings") { - CHECK(std::string(vl_version_string()).find("velociloops ") == 0); + std::regex versionFormat(R"(\d+\.\d+\.\d+)"); + CHECK(std::regex_match(vl_version_string(), versionFormat)); CHECK(std::string(vl_error_string(VL_OK)) == "OK"); CHECK(std::string(vl_error_string(VL_ERROR_INVALID_HANDLE)) == "invalid handle"); CHECK(std::string(vl_error_string(VL_ERROR_INVALID_ARG)) == "invalid argument"); @@ -650,7 +712,6 @@ TEST_CASE("invalid API arguments return errors") CHECK(vl_get_creator_info(nullptr, nullptr) == VL_ERROR_INVALID_HANDLE); CHECK(vl_get_slice_info(nullptr, 0, nullptr) == VL_ERROR_INVALID_HANDLE); CHECK(vl_set_slice_info(nullptr, 0, 0, -1) == VL_ERROR_INVALID_HANDLE); - CHECK(vl_set_output_sample_rate(nullptr, 44100) == VL_ERROR_INVALID_HANDLE); CHECK(vl_get_slice_frame_count(nullptr, 0) == VL_ERROR_INVALID_HANDLE); CHECK(vl_decode_slice(nullptr, 0, nullptr, nullptr, 0, 0, nullptr) == VL_ERROR_INVALID_HANDLE); CHECK(vl_set_info(nullptr, nullptr) == VL_ERROR_INVALID_HANDLE); @@ -659,6 +720,36 @@ TEST_CASE("invalid API arguments return errors") CHECK(vl_remove_slice(nullptr, 0) == VL_ERROR_INVALID_HANDLE); CHECK(vl_save(nullptr, "out.rx2") == VL_ERROR_INVALID_HANDLE); CHECK(vl_save_to_memory(nullptr, nullptr, nullptr) == VL_ERROR_INVALID_HANDLE); + + vl_superflux_default_options(nullptr); + float sample = 0.0f; + CHECK(vl_create_from_superflux(0, 44100, 120000, &sample, nullptr, 1, nullptr, &err) == nullptr); + CHECK(err == VL_ERROR_INVALID_ARG); + CHECK(vl_create_from_superflux(1, 7999, 120000, &sample, nullptr, 1, nullptr, &err) == nullptr); + CHECK(err == VL_ERROR_INVALID_SAMPLE_RATE); + CHECK(vl_create_from_superflux(1, 44100, 0, &sample, nullptr, 1, nullptr, &err) == nullptr); + CHECK(err == VL_ERROR_INVALID_TEMPO); + CHECK(vl_create_from_superflux(1, 44100, 120000, nullptr, nullptr, 1, nullptr, &err) == nullptr); + CHECK(err == VL_ERROR_INVALID_ARG); + CHECK(vl_create_from_superflux(2, 44100, 120000, &sample, nullptr, 1, nullptr, &err) == nullptr); + CHECK(err == VL_ERROR_INVALID_ARG); + + { + VLSuperFluxOptions badOpts = {}; + vl_superflux_default_options(&badOpts); + badOpts.frame_size = 63; + CHECK(vl_create_from_superflux(1, 44100, 120000, &sample, nullptr, 1, &badOpts, &err) == nullptr); + CHECK(err == VL_ERROR_INVALID_ARG); + } + + { + VLSuperFluxOptions badOpts = {}; + vl_superflux_default_options(&badOpts); + badOpts.frame_size = 64; + badOpts.fmax = 100.0f; + CHECK(vl_create_from_superflux(1, 44100, 120000, &sample, nullptr, 1, &badOpts, &err) == nullptr); + CHECK(err == VL_ERROR_INVALID_ARG); + } } TEST_CASE("malformed and patched containers cover parser edge cases") @@ -768,6 +859,257 @@ TEST_CASE("malformed and patched containers cover parser edge cases") CHECK(left == 0.0f); CHECK(right == 0.0f); } + + SUBCASE("SINF totalFrames exceeding one hour at maximum sample rate is rejected") + { + std::vector bytes = buildSmallEncodedFile(); + const ChunkRange sinf = findChunk(bytes, "SINF"); + REQUIRE(sinf.found); + REQUIRE(sinf.size >= 10); + putBe32(bytes, sinf.payload + 6, 3600u * 192000u + 1u); + + VLError err = VL_OK; + CHECK(vl_open_from_memory(bytes.data(), bytes.size(), &err) == nullptr); + CHECK(err == VL_ERROR_INVALID_SIZE); + } + + SUBCASE("truncated mono SDAT causes decompressor EOF and file corrupt error") + { + std::vector bytes = buildSmallEncodedFile(1); + const ChunkRange sdat = findChunk(bytes, "SDAT"); + REQUIRE(sdat.found); + REQUIRE(sdat.size >= 4); + putBe32(bytes, sdat.payload - 4, 1u); + + VLError err = VL_OK; + CHECK(vl_open_from_memory(bytes.data(), bytes.size(), &err) == nullptr); + CHECK(err == VL_ERROR_INVALID_SIZE); + } + + SUBCASE("truncated stereo SDAT causes decompressor EOF and file corrupt error") + { + std::vector bytes = buildSmallEncodedFile(2); + const ChunkRange sdat = findChunk(bytes, "SDAT"); + REQUIRE(sdat.found); + REQUIRE(sdat.size >= 4); + putBe32(bytes, sdat.payload - 4, 1u); + + VLError err = VL_OK; + CHECK(vl_open_from_memory(bytes.data(), bytes.size(), &err) == nullptr); + CHECK(err == VL_ERROR_INVALID_SIZE); + } + + SUBCASE("RCY APP binary magic mismatch falls back to single synthesized slice") + { + std::vector bytes = readFile(kDataDir / "120RcyTest.rcy"); + const ChunkRange appl = findAIFFChunk(bytes, "APPL"); + REQUIRE(appl.found); + REQUIRE(appl.size >= 8); + putBe32(bytes, appl.payload + 4, 0xDEADBEEFu); + + VLError err = VL_OK; + ScopedVLFile file(vl_open_from_memory(bytes.data(), bytes.size(), &err)); + REQUIRE(file); + CHECK(err == VL_OK); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.slice_count == 1); + CHECK(info.loop_end > info.loop_start); + } + + SUBCASE("RCY APP storedCount of zero falls back to single synthesized slice") + { + std::vector bytes = readFile(kDataDir / "120RcyTest.rcy"); + const ChunkRange appl = findAIFFChunk(bytes, "APPL"); + REQUIRE(appl.found); + REQUIRE(appl.size >= 0xa4u); + putBe16(bytes, appl.payload + 0xa2u, 0u); + + VLError err = VL_OK; + ScopedVLFile file(vl_open_from_memory(bytes.data(), bytes.size(), &err)); + REQUIRE(file); + CHECK(err == VL_OK); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.slice_count == 1); + } + + SUBCASE("RCY APP storedCount above 1000 falls back to single synthesized slice") + { + std::vector bytes = readFile(kDataDir / "120RcyTest.rcy"); + const ChunkRange appl = findAIFFChunk(bytes, "APPL"); + REQUIRE(appl.found); + REQUIRE(appl.size >= 0xa4u); + putBe16(bytes, appl.payload + 0xa2u, 1001u); + + VLError err = VL_OK; + ScopedVLFile file(vl_open_from_memory(bytes.data(), bytes.size(), &err)); + REQUIRE(file); + CHECK(err == VL_OK); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.slice_count == 1); + } + + SUBCASE("RCY APP storedCount exceeding available binary size falls back to single synthesized slice") + { + std::vector bytes = readFile(kDataDir / "120RcyTest.rcy"); + const ChunkRange appl = findAIFFChunk(bytes, "APPL"); + REQUIRE(appl.found); + REQUIRE(appl.size >= 0xa4u); + REQUIRE(bytes.size() >= 8u); + // 120RcyTest.rcy APPL binarySize == 0xa0 + 1000*8 exactly, so storedCount alone + // can never exceed it — shrink the APPL chunk to make binarySize < 0xa0 + 5*8. + const uint32_t newApplSz = 0xb4u; + putBe32(bytes, appl.payload - 4, newApplSz); + // Shrink FORM so the AIFF scanner stops cleanly after the truncated APPL. + putBe32(bytes, 4u, (uint32_t)(appl.payload + newApplSz - 8u)); + putBe16(bytes, appl.payload + 0xa2u, 5u); + + VLError err = VL_OK; + ScopedVLFile file(vl_open_from_memory(bytes.data(), bytes.size(), &err)); + REQUIRE(file); + CHECK(err == VL_OK); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.slice_count == 1); + } + + SUBCASE("RCY slice with muted state and selected flag is preserved through load") + { + std::vector bytes = readFile(kDataDir / "120RcyTest.rcy"); + const ChunkRange appl = findAIFFChunk(bytes, "APPL"); + REQUIRE(appl.found); + REQUIRE(appl.size >= 0xa4u + 8u); + bytes[appl.payload + 0xa4u] = 0x82u; + + VLError err = VL_OK; + ScopedVLFile file(vl_open_from_memory(bytes.data(), bytes.size(), &err)); + REQUIRE(file); + CHECK(err == VL_OK); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + REQUIRE(info.slice_count > 0); + + VLSliceInfo slice = {}; + REQUIRE(vl_get_slice_info(file.handle, 0, &slice) == VL_OK); + CHECK((slice.flags & VL_SLICE_FLAG_MUTED) != 0); + CHECK((slice.flags & VL_SLICE_FLAG_SELECTED) != 0); + } + + SUBCASE("REX APP binary magic mismatch falls back to single synthesized slice") + { + std::vector bytes = readFile(kDataDir / "120RexTest.rex"); + const ChunkRange appl = findAIFFChunk(bytes, "APPL"); + REQUIRE(appl.found); + REQUIRE(appl.size >= 8u); + putBe32(bytes, appl.payload + 4u, 0xDEADBEEFu); + + VLError err = VL_OK; + ScopedVLFile file(vl_open_from_memory(bytes.data(), bytes.size(), &err)); + REQUIRE(file); + CHECK(err == VL_OK); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.slice_count == 1); + } + + SUBCASE("REX slice with zero length is skipped during parsing") + { + std::vector bytes = readFile(kDataDir / "120RexTest.rex"); + const ChunkRange appl = findAIFFChunk(bytes, "APPL"); + REQUIRE(appl.found); + REQUIRE(appl.size >= 0x400u + 4u); + putBe32(bytes, appl.payload + 0x400u, 0u); + + VLError err = VL_OK; + ScopedVLFile file(vl_open_from_memory(bytes.data(), bytes.size(), &err)); + REQUIRE(file); + CHECK(err == VL_OK); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.slice_count == 9); + } + + SUBCASE("REX with all zero-length slices falls back to single synthesized slice") + { + std::vector bytes = readFile(kDataDir / "120RexTest.rex"); + const ChunkRange appl = findAIFFChunk(bytes, "APPL"); + REQUIRE(appl.found); + REQUIRE(appl.size >= 0x3fcu + 4u); + const uint16_t storedCount = be16(bytes.data() + appl.payload + 14u); + REQUIRE(storedCount > 0); + for (uint16_t i = 0; i < storedCount; ++i) + putBe32(bytes, appl.payload + 0x400u + (size_t)i * 12u, 0u); + + VLError err = VL_OK; + ScopedVLFile file(vl_open_from_memory(bytes.data(), bytes.size(), &err)); + REQUIRE(file); + CHECK(err == VL_OK); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.slice_count == 1); + } + + SUBCASE("REX slices all at loop-end ppq produce zero samplesPerPpq and fall back to single slice") + { + std::vector bytes = readFile(kDataDir / "120RexTest.rex"); + const ChunkRange appl = findAIFFChunk(bytes, "APPL"); + REQUIRE(appl.found); + REQUIRE(appl.size >= 0x3fcu + 4u); + const uint32_t storedPpqLength = be32(bytes.data() + appl.payload + 10u); + const uint16_t storedCount = be16(bytes.data() + appl.payload + 14u); + REQUIRE(storedCount > 0); + for (uint16_t i = 0; i < storedCount; ++i) + putBe32(bytes, appl.payload + 0x404u + (size_t)i * 12u, storedPpqLength); + + VLError err = VL_OK; + ScopedVLFile file(vl_open_from_memory(bytes.data(), bytes.size(), &err)); + REQUIRE(file); + CHECK(err == VL_OK); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.slice_count == 1); + } + + SUBCASE("AIFF APPL chunk size extending beyond file bounds breaks parsing early") + { + std::vector bytes = readFile(kDataDir / "120RcyTest.rcy"); + const ChunkRange appl = findAIFFChunk(bytes, "APPL"); + REQUIRE(appl.found); + putBe32(bytes, appl.payload - 4u, (uint32_t)bytes.size()); + + VLError err = VL_OK; + CHECK(vl_open_from_memory(bytes.data(), bytes.size(), &err) == nullptr); + CHECK(err == VL_ERROR_FILE_CORRUPT); + } + + SUBCASE("AIFF MARK marker name length overflow breaks marker loop") + { + std::vector bytes = readFile(kDataDir / "120RcyTest.rcy"); + const ChunkRange mark = findAIFFChunk(bytes, "MARK"); + REQUIRE(mark.found); + REQUIRE(mark.size >= 9u); + bytes[mark.payload + 8u] = 0xFFu; + + VLError err = VL_OK; + ScopedVLFile file(vl_open_from_memory(bytes.data(), bytes.size(), &err)); + REQUIRE(file); + CHECK(err == VL_OK); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.total_frames > 0); + } } TEST_CASE("WAV fixtures are valid PCM references") @@ -776,10 +1118,10 @@ TEST_CASE("WAV fixtures are valid PCM references") for (const fs::path& path : dataFiles()) { if (path.extension() != ".wav") - { continue; - } + CAPTURE(path.filename().string()); + WavInfo info; REQUIRE(readWavInfo(path, info)); CHECK((info.channels == 1 || info.channels == 2)); @@ -789,7 +1131,8 @@ TEST_CASE("WAV fixtures are valid PCM references") CHECK(info.dataBytes > 0); ++wavCount; } - CHECK(wavCount == 17); + + CHECK(wavCount == 15); } TEST_CASE("mono slice rendering matches decoded DWOP trace after render offset") @@ -1060,7 +1403,7 @@ TEST_CASE("all test data files are classified by the public opener") { const auto files = dataFiles(); const auto expected = expectedOpenableContainers(); - CHECK(files.size() == 40 + syntheticCorruptFixtures().size()); + CHECK(files.size() == 36 + syntheticCorruptFixtures().size()); int decodableCount = 0; int rejectedCount = 0; @@ -1155,11 +1498,6 @@ TEST_CASE("every decodable fixture renders every slice and roundtrips through me CHECK(vl_get_slice_frame_count(file.handle, -1) == VL_ERROR_INVALID_SLICE); CHECK(vl_get_slice_frame_count(file.handle, info.slice_count) == VL_ERROR_INVALID_SLICE); - CHECK(vl_set_output_sample_rate(file.handle, 1) == VL_ERROR_INVALID_SAMPLE_RATE); - CHECK(vl_set_output_sample_rate(file.handle, info.sample_rate) == VL_OK); - const int32_t otherRate = info.sample_rate == 44100 ? 48000 : 44100; - CHECK(vl_set_output_sample_rate(file.handle, otherRate) == VL_ERROR_NOT_IMPLEMENTED); - VLCreatorInfo creator = {}; const VLError creatorResult = vl_get_creator_info(file.handle, &creator); if (creatorResult == VL_OK) @@ -1199,20 +1537,33 @@ TEST_CASE("every decodable fixture renders every slice and roundtrips through me checkInfoSane(info); } - size_t originalSize = 0; - REQUIRE(vl_save_to_memory(file.handle, nullptr, &originalSize) == VL_OK); - CHECK(originalSize == readFile(path).size()); + size_t savedSize = 0; + REQUIRE(vl_save_to_memory(file.handle, nullptr, &savedSize) == VL_OK); + REQUIRE(savedSize > 64); + + std::vector shortSave(savedSize - 1); + size_t shortSaveSize = shortSave.size(); + CHECK(vl_save_to_memory(file.handle, shortSave.data(), &shortSaveSize) == VL_ERROR_BUFFER_TOO_SMALL); + CHECK(shortSaveSize == savedSize); - std::vector shortOriginal(originalSize > 0 ? originalSize - 1 : 0); - size_t shortOriginalSize = shortOriginal.size(); - CHECK(vl_save_to_memory(file.handle, shortOriginal.data(), &shortOriginalSize) == VL_ERROR_BUFFER_TOO_SMALL); - CHECK(shortOriginalSize == originalSize); + std::vector savedCopy(savedSize); + size_t copySize = savedCopy.size(); + REQUIRE(vl_save_to_memory(file.handle, savedCopy.data(), ©Size) == VL_OK); + CHECK(copySize == savedSize); + CHECK(vl_save_to_memory(file.handle, savedCopy.data(), nullptr) == VL_ERROR_INVALID_ARG); - std::vector originalCopy(originalSize); - size_t copySize = originalCopy.size(); - REQUIRE(vl_save_to_memory(file.handle, originalCopy.data(), ©Size) == VL_OK); - CHECK(originalCopy == readFile(path)); - CHECK(vl_save_to_memory(file.handle, originalCopy.data(), nullptr) == VL_ERROR_INVALID_ARG); + ScopedVLFile savedReopened(vl_open_from_memory(savedCopy.data(), savedCopy.size(), &err)); + REQUIRE(savedReopened); + VLFileInfo savedInfo = {}; + REQUIRE(vl_get_info(savedReopened.handle, &savedInfo) == VL_OK); + CHECK(savedInfo.channels == info.channels); + CHECK(savedInfo.sample_rate == info.sample_rate); + CHECK(savedInfo.slice_count == info.slice_count); + CHECK(savedInfo.total_frames == info.total_frames); + CHECK(savedInfo.loop_start == info.loop_start); + CHECK(savedInfo.loop_end == info.loop_end); + CHECK(savedInfo.tempo == info.tempo); + checkSliceMetadataMatches(file.handle, savedReopened.handle, info.slice_count, path.extension() != ".rex"); if (expectedRenderableContainers().count(name) != 0) { @@ -1221,6 +1572,88 @@ TEST_CASE("every decodable fixture renders every slice and roundtrips through me } } +TEST_CASE("SuperFlux authoring creates sliced mono and stereo files from full-loop floats") +{ + constexpr int32_t sampleRate = 44100; + constexpr int32_t frames = sampleRate * 2; + const std::vector expectedStarts = {0, sampleRate / 2, sampleRate, sampleRate + sampleRate / 2}; + + VLSuperFluxOptions options = {}; + vl_superflux_default_options(&options); + options.threshold = 0.05f; + options.combine_ms = 90.0f; + options.min_slice_frames = sampleRate / 5; + options.post_max = 0.03f; + + SUBCASE("mono") + { + std::vector left((size_t)frames, 0.0f); + for (int32_t start : expectedStarts) + addSyntheticOnset(left, nullptr, (size_t)start); + + VLError err = VL_OK; + ScopedVLFile file(vl_create_from_superflux(1, sampleRate, 120000, left.data(), nullptr, frames, &options, &err)); + REQUIRE(file); + CHECK(err == VL_OK); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.channels == 1); + CHECK(info.sample_rate == sampleRate); + CHECK(info.total_frames == frames); + CHECK(info.loop_end == frames); + CHECK(info.transient_enabled == 0); + CHECK(info.slice_count >= (int32_t)expectedStarts.size()); + for (int32_t expected : expectedStarts) + CHECK(hasSliceNear(file.handle, info.slice_count, expected, options.frame_size)); + + size_t size = 0; + REQUIRE(vl_save_to_memory(file.handle, nullptr, &size) == VL_OK); + std::vector bytes(size); + REQUIRE(vl_save_to_memory(file.handle, bytes.data(), &size) == VL_OK); + + ScopedVLFile reopened(vl_open_from_memory(bytes.data(), bytes.size(), &err)); + REQUIRE(reopened); + VLFileInfo reopenedInfo = {}; + REQUIRE(vl_get_info(reopened.handle, &reopenedInfo) == VL_OK); + CHECK(reopenedInfo.slice_count == info.slice_count); + CHECK(reopenedInfo.total_frames == frames); + } + + SUBCASE("stereo") + { + std::vector left((size_t)frames, 0.0f); + std::vector right((size_t)frames, 0.0f); + for (int32_t start : expectedStarts) + addSyntheticOnset(left, &right, (size_t)start); + + VLError err = VL_OK; + ScopedVLFile file(vl_create_from_superflux(2, sampleRate, 120000, left.data(), right.data(), frames, &options, &err)); + REQUIRE(file); + CHECK(err == VL_OK); + + VLFileInfo info = {}; + REQUIRE(vl_get_info(file.handle, &info) == VL_OK); + CHECK(info.channels == 2); + CHECK(info.slice_count >= (int32_t)expectedStarts.size()); + for (int32_t expected : expectedStarts) + CHECK(hasSliceNear(file.handle, info.slice_count, expected, options.frame_size)); + + const int32_t firstFrames = vl_get_slice_frame_count(file.handle, 0); + REQUIRE(firstFrames > 0); + std::vector decodedLeft((size_t)firstFrames); + std::vector decodedRight((size_t)firstFrames); + int32_t written = 0; + REQUIRE(vl_decode_slice(file.handle, 0, decodedLeft.data(), decodedRight.data(), 0, firstFrames, &written) == VL_OK); + CHECK(written == firstFrames); + CHECK(std::inner_product(decodedLeft.begin(), decodedLeft.end(), decodedRight.begin(), 0.0, std::plus(), + [](float l, float r) + { + return std::fabs(l - r); + }) > 0.01); + } +} + TEST_CASE("fresh mono and stereo files can be assembled, mutated, saved, and reopened") { CHECK(vl_create_new(0, 44100, 120000, nullptr) == nullptr); @@ -1246,6 +1679,10 @@ TEST_CASE("fresh mono and stereo files can be assembled, mutated, saved, and reo info.tempo = 0; CHECK(vl_set_info(file.handle, &info) == VL_ERROR_INVALID_TEMPO); CHECK(vl_set_info(file.handle, nullptr) == VL_ERROR_INVALID_ARG); + info.tempo = 120000; + info.channels = 3; + CHECK(vl_set_info(file.handle, &info) == VL_ERROR_INVALID_ARG); + info.channels = 1; VLCreatorInfo creator = {}; CHECK(vl_set_creator_info(file.handle, nullptr) == VL_ERROR_INVALID_ARG); @@ -1463,3 +1900,32 @@ TEST_CASE("fresh mono and stereo files can be assembled, mutated, saved, and reo }) > 0.01); } } + +TEST_CASE("out of memory conditions are handled gracefully") +{ + SUBCASE("vl_open_from_memory returns out-of-memory when allocation fails") + { + const std::vector bytes = buildSmallEncodedFile(); + VLError err = VL_OK; + gFailNextNothrowAllocation = true; + CHECK(vl_open_from_memory(bytes.data(), bytes.size(), &err) == nullptr); + CHECK(err == VL_ERROR_OUT_OF_MEMORY); + } + + SUBCASE("vl_create_new returns out-of-memory when allocation fails") + { + VLError err = VL_OK; + gFailNextNothrowAllocation = true; + CHECK(vl_create_new(1, 44100, 120000, &err) == nullptr); + CHECK(err == VL_ERROR_OUT_OF_MEMORY); + } + + SUBCASE("vl_create_from_superflux returns out-of-memory on bad_alloc") + { + VLError err = VL_OK; + float sample = 0.0f; + gFailNextAllocation = true; + CHECK(vl_create_from_superflux(1, 44100, 120000, &sample, nullptr, 1, nullptr, &err) == nullptr); + CHECK(err == VL_ERROR_OUT_OF_MEMORY); + } +}