diff --git a/.vscode/launch.json b/.vscode/launch.json index f5e2185..32c144a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -80,7 +80,19 @@ "request": "launch", "program": "${workspaceFolder}/build/tests/Debug/libdsp_tests.exe", "args": [ - "--gtest_filter=Phase*" + "--gtest_filter=Buchla*" + ], + "stopAtEntry": false, + "cwd": "${fileDirname}", + "environment": [], + "console": "externalTerminal" + }, + { + "name": "PerfTests", + "type": "cppvsdbg", + "request": "launch", + "program": "${workspaceFolder}/build/tests/perf/Debug/perf_tests.exe", + "args": [ ], "stopAtEntry": false, "cwd": "${fileDirname}", diff --git a/include/buchla_lpg.h b/include/buchla_lpg.h index 19ba95b..063b249 100644 --- a/include/buchla_lpg.h +++ b/include/buchla_lpg.h @@ -10,6 +10,8 @@ float GetCurrent(float vc, float offset, bool smoothed); float ProcessCurrent(float c); +void GetCurrent(const float* vc_in, float* vc_out, size_t size, bool smoothed); + /// @brief Buchla Low Pass Gate based on Josh Rohs' MATLAB implementation /// Original paper: http://www.music.mcgill.ca/~gary/courses/projects/618_2018/rohs/MUMT618_Buchla_LPG_Report.pdf class BuchlaLPG @@ -28,10 +30,15 @@ class BuchlaLPG /// @param size The size of the input and output buffer void ProcessBlock(const float* cv_in, const float* in, float* out, size_t size); + void ProcessCurrent(const float* vc_in, float* vc_out, size_t size); + + void ProcessAudio(const float* vc_in, const float* in, float* out, size_t size); + private: float samplerate_; float dt_ = 0.f; float f_ = 0.f; + float f_inv_ = 0.f; float prev_current_ = 0.f; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7bbfe58..31c8684 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -28,6 +28,8 @@ endif() target_include_directories(dsp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_options(dsp PRIVATE -g -gcodeview) +target_link_options(dsp PRIVATE -fuse-ld=lld -g -Wl,--pdb=) if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") target_compile_options(dsp PRIVATE ${CLANG_COMPILER_OPTION}) elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") diff --git a/src/buchla_lpg.cpp b/src/buchla_lpg.cpp index edc68d6..4bafa31 100644 --- a/src/buchla_lpg.cpp +++ b/src/buchla_lpg.cpp @@ -1,3 +1,5 @@ +/// Original MATLAB implementation: +/// http://www.music.mcgill.ca/~gary/courses/projects/618_2018/rohs/MUMT618_Buchla_LPG_Report.pdf #include "buchla_lpg.h" #include @@ -70,12 +72,21 @@ float GetCurrent(float vc, float offset, bool smoothed) return current; } +void GetCurrent(const float* vc_in, float* vc_out, size_t size, bool smoothed) +{ + for (size_t i = 0; i < size; ++i) + { + vc_out[i] = GetCurrent(vc_in[i], kOffset, smoothed); + } +} + void BuchlaLPG::Init(float samplerate) { samplerate_ = samplerate; dt_ = 1.f / samplerate_; f_ = 1.f / (2.f * samplerate_); + f_inv_ = 1.f / f_; } void BuchlaLPG::ProcessBlock(const float* cv_in, const float* in, float* out, size_t size) @@ -151,4 +162,82 @@ void BuchlaLPG::ProcessBlock(const float* cv_in, const float* in, float* out, si } } +void BuchlaLPG::ProcessCurrent(const float* vc_in, float* vc_out, size_t size) +{ + for (size_t i = 0; i < size; ++i) + { + const float current = vc_in[i]; + int8_t dir = sign(current - prev_current_); + if (dir != prev_dir_) + { + switch (dir) + { + case 1: + wc_ = kWcOn * (1.f - kRhoOn + kRhoOn * current / 0.040f); + break; + case -1: + wc_ = kWcOff * (1.f + kRhoOff - (kRhoOff) * (yc_) / 0.040f); + break; + case 0: + break; + default: + assert(false); + break; + } + } + + const float g = wc_ * dt_ * 0.5f; + const float vc = (current - sc_) * g / (1.f + g); + yc_ = vc + sc_; + sc_ = yc_ + vc; + + prev_current_ = current; + prev_dir_ = dir; + + const float processed_current = std::max(yc_, 1.01e-5f); + vc_out[i] = 1.f / (std::pow(processed_current, 1.4f)) * kAVac + kBVac; + } +} + +void BuchlaLPG::ProcessAudio(const float* vc_in, const float* in, float* out, size_t size) +{ + for (size_t i = 0; i < size; ++i) + { + const float rf = vc_in[i]; + + const float a = 0.f; // kNormalizedFb * amax; + const float a1 = 1.f / (rf * kC1); + const float a2 = -1.f / kC1 * (1 / rf + 1.f / kRa); + const float b1 = 1.f / (rf * kC2); + const float b2 = -2.f / (rf * kC2); + const float b3 = b1; + const float d1 = a; + + const float yi = in[i]; + const float d_o = 1.f / (1.f - f_ * a2); + const float d_x = 1.f / (1.f - f_ * b2); + + constexpr float kD2 = -1.f; + + if (non_lin_) + { + } + else + { + float yx = (sx_ + f_ * b1 * yi + f_ * b3 * d_o * so_ + f_ * kB4 * sd_ + kB4 * d1 * d_o * so_) * d_x; + yx = yx / (1 - d_x * (f_ * f_ * b3 * d_o * a1 + kB4 * f_ * d1 * d_o * a1 + kB4 * kD2)); + yo_ = (so_ + f_ * a1 * yx) * d_o; + const float yd = sd_ + (1.f / f_) * (d1 * yo_ + kD2 * yx); + + sx_ = sx_ + 2.f * f_ * (b1 * yi + b2 * yx + b3 * yo_ + kB4 * yd); + + so_ = so_ + 2.f * f_ * (a1 * yx + a2 * yo_); + + sd_ = -sd_ - (2.f / f_) * (d1 * yo_ + kD2 * yx); + } + + out[i] = yo_; + } +} + } // namespace sfdsp \ No newline at end of file diff --git a/tests/buchla_lpg_tests.cpp b/tests/buchla_lpg_tests.cpp index 1da9360..60f3292 100644 --- a/tests/buchla_lpg_tests.cpp +++ b/tests/buchla_lpg_tests.cpp @@ -44,4 +44,47 @@ TEST(BuchlaLPGTest, Current) info.samplerate = kSamplerate; info.format = SF_FORMAT_WAV | SF_FORMAT_FLOAT; WriteWavFile("out_lpg.wav", audio_buffer.get(), info, kSize); +} + +TEST(BuchlaLPGTestPerf, Current) +{ + constexpr float kSamplerate = 96000.f; + + sfdsp::BasicOscillator cv; + cv.Init(kSamplerate, 4, sfdsp::OscillatorType::Square); + cv.SetDuty(0.05f); + + constexpr size_t kSize = 4 * kSamplerate; + auto cv_out = std::make_unique(kSize); + cv.ProcessBlock(cv_out.get(), kSize); + + constexpr float kModDepth = 8.f; + + for (size_t i = 0; i < kSize; ++i) + { + cv_out[i] = kModDepth / 2.f * (1.f + cv_out[i]); + } + + auto audio_buffer = std::make_unique(kSize); + sfdsp::BasicOscillator audio; + audio.Init(kSamplerate, 440, sfdsp::OscillatorType::Square); + audio.ProcessBlock(audio_buffer.get(), kSize); + + for (size_t i = 0; i < kSize; ++i) + { + audio_buffer[i] = 0.3f * audio_buffer[i]; + } + + sfdsp::BuchlaLPG lpg; + lpg.Init(kSamplerate); + + sfdsp::GetCurrent(cv_out.get(), cv_out.get(), kSize, false); + lpg.ProcessCurrent(cv_out.get(), cv_out.get(), kSize); + lpg.ProcessAudio(cv_out.get(), audio_buffer.get(), audio_buffer.get(), kSize); + + SF_INFO info; + info.channels = 1; + info.samplerate = kSamplerate; + info.format = SF_FORMAT_WAV | SF_FORMAT_FLOAT; + WriteWavFile("perf_lpg.wav", audio_buffer.get(), info, kSize); } \ No newline at end of file diff --git a/tests/perf/buchla_lpg_perf.cpp b/tests/perf/buchla_lpg_perf.cpp index e917a4d..2ff2844 100644 --- a/tests/perf/buchla_lpg_perf.cpp +++ b/tests/perf/buchla_lpg_perf.cpp @@ -10,15 +10,16 @@ using namespace ankerl; using namespace std::chrono_literals; constexpr size_t kSamplerate = 96000; -constexpr size_t kOutputSize = 960; +constexpr size_t kOutputSize = 96000; TEST_CASE("BuchlaLPG") { nanobench::Bench bench; bench.title("Buchla LPG"); - // bench.relative(true); bench.timeUnit(1ms, "ms"); bench.performanceCounters(true); + bench.minEpochIterations(50); + bench.warmup(10); sfdsp::BasicOscillator cv; cv.Init(kSamplerate, 4, sfdsp::OscillatorType::Square); @@ -45,15 +46,62 @@ TEST_CASE("BuchlaLPG") sfdsp::BuchlaLPG lpg; lpg.Init(kSamplerate); - lpg.ProcessBlock(cv_out.get(), audio_buffer.get(), audio_buffer.get(), kOutputSize); + // lpg.ProcessBlock(cv_out.get(), audio_buffer.get(), audio_buffer.get(), kOutputSize); std::unique_ptr audio_out = std::make_unique(kOutputSize); constexpr size_t kBlockSize = 128; bench.run("Buchla LPG", [&]() { - for (size_t i = 0; i < kOutputSize; i += kBlockSize) - { - lpg.ProcessBlock(cv_out.get() + i, audio_buffer.get() + i, audio_out.get() + i, kBlockSize); - } + // lpg.ProcessBlock(cv_out.get(), audio_buffer.get(), audio_out.get(), kOutputSize); + sfdsp::GetCurrent(cv_out.get(), cv_out.get(), kOutputSize, false); + lpg.ProcessCurrent(cv_out.get(), cv_out.get(), kOutputSize); + lpg.ProcessAudio(cv_out.get(), audio_buffer.get(), audio_buffer.get(), kOutputSize); }); } + +TEST_CASE("BuchlaLPG_Detailed") +{ + nanobench::Bench bench; + bench.title("Buchla LPG - Detailed"); + bench.relative(true); + bench.timeUnit(1ms, "ms"); + bench.performanceCounters(true); + bench.minEpochIterations(50); + bench.warmup(10); + + sfdsp::BasicOscillator cv; + cv.Init(kSamplerate, 4, sfdsp::OscillatorType::Square); + cv.SetDuty(0.05f); + auto cv_out = std::make_unique(kOutputSize); + cv.ProcessBlock(cv_out.get(), kOutputSize); + + constexpr float kModDepth = 8.f; + for (size_t i = 0; i < kOutputSize; ++i) + { + cv_out[i] = kModDepth / 2.f * (1.f + cv_out[i]); + } + + auto audio_buffer = std::make_unique(kOutputSize); + sfdsp::BasicOscillator audio; + audio.Init(kSamplerate, 440, sfdsp::OscillatorType::Square); + audio.ProcessBlock(audio_buffer.get(), kOutputSize); + + for (size_t i = 0; i < kOutputSize; ++i) + { + audio_buffer[i] = 0.3f * audio_buffer[i]; + } + + sfdsp::BuchlaLPG lpg; + lpg.Init(kSamplerate); + + lpg.ProcessBlock(cv_out.get(), audio_buffer.get(), audio_buffer.get(), kOutputSize); + + std::unique_ptr audio_out = std::make_unique(kOutputSize); + + bench.run("GetCurrent", [&]() { sfdsp::GetCurrent(cv_out.get(), cv_out.get(), kOutputSize, false); }); + + bench.run("ProcessCurrent", [&]() { lpg.ProcessCurrent(cv_out.get(), cv_out.get(), kOutputSize); }); + + bench.run("ProcessAudio", + [&]() { lpg.ProcessAudio(cv_out.get(), audio_buffer.get(), audio_out.get(), kOutputSize); }); +}