diff --git a/modules/common/chowdsp_core/JUCEHelpers/juce_ExtraDefinitions.h b/modules/common/chowdsp_core/JUCEHelpers/juce_ExtraDefinitions.h index 0e0f54760..cb801bd4e 100644 --- a/modules/common/chowdsp_core/JUCEHelpers/juce_ExtraDefinitions.h +++ b/modules/common/chowdsp_core/JUCEHelpers/juce_ExtraDefinitions.h @@ -1,11 +1,17 @@ #pragma once +#include + /** * This file contains custom overrides of some JUCE macros. */ // @TODO: figure out a way to re-implement jassert... +#if CHOWDSP_JASSERT_IS_CASSERT +#define jassert(expression) assert (expression) +#else #define jassert(expression) +#endif #define jassertfalse #define jassertquiet(expression) diff --git a/modules/common/chowdsp_core/chowdsp_core.h b/modules/common/chowdsp_core/chowdsp_core.h index eb961273f..6aedf1b72 100644 --- a/modules/common/chowdsp_core/chowdsp_core.h +++ b/modules/common/chowdsp_core/chowdsp_core.h @@ -45,6 +45,10 @@ BEGIN_JUCE_MODULE_DECLARATION #define CHOWDSP_ALLOW_TEMPLATE_INSTANTIATIONS 1 #endif +#ifndef CHOWDSP_JASSERT_IS_CASSERT +#define CHOWDSP_JASSERT_IS_CASSERT 0 +#endif + #if ! CHOWDSP_USING_JUCE #define JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED 0 #include "JUCEHelpers/juce_TargetPlatform.h" diff --git a/modules/dsp/chowdsp_buffers/Buffers/chowdsp_BufferView.h b/modules/dsp/chowdsp_buffers/Buffers/chowdsp_BufferView.h index 2924d432d..97560a0dd 100644 --- a/modules/dsp/chowdsp_buffers/Buffers/chowdsp_BufferView.h +++ b/modules/dsp/chowdsp_buffers/Buffers/chowdsp_BufferView.h @@ -246,7 +246,7 @@ class BufferView std::enable_if_t, void> initialise (SampleType* const* data, int sampleOffset, int startChannel = 0) { jassert (juce::isPositiveAndNotGreaterThan (numChannels, maxNumChannels)); - jassert (numSamples > 0); + jassert (numSamples >= 0); for (size_t ch = 0; ch < (size_t) numChannels; ++ch) channelPointers[ch] = data[ch + (size_t) startChannel] + sampleOffset; } @@ -255,7 +255,7 @@ class BufferView std::enable_if_t, void> initialise (const SampleType* const* data, int sampleOffset, int startChannel = 0) { jassert (juce::isPositiveAndNotGreaterThan (numChannels, maxNumChannels)); - jassert (numSamples > 0); + jassert (numSamples >= 0); for (size_t ch = 0; ch < (size_t) numChannels; ++ch) channelPointers[ch] = data[ch + (size_t) startChannel] + sampleOffset; } diff --git a/modules/dsp/chowdsp_dsp_data_structures/Processors/chowdsp_BufferMultiple.h b/modules/dsp/chowdsp_dsp_data_structures/Processors/chowdsp_BufferMultiple.h new file mode 100644 index 000000000..7f53797b4 --- /dev/null +++ b/modules/dsp/chowdsp_dsp_data_structures/Processors/chowdsp_BufferMultiple.h @@ -0,0 +1,170 @@ +#pragma once + +namespace chowdsp +{ +/** + * BufferMultiple can be used to process buffers that need to be a multiple of some + * number of samples. + * + * BufferMultiple requires less latency than RebufferedProcessor, but RebufferedProcessor + * should still be preferred when an exact buffer length is required. + * + * Here's an example where we need to process buffers that are a multiple of 3 samples, + * for example if we wanted to down-sample by 3x: + * @code + * // prepare + * BufferMultiple bufferMultiple {}; + * const auto m3BufferSize = bufferMultiple.prepare (spec, 3); + * arenaBytesNeeded += m3BufferSize * numChannels * sizeof (float); + * nextProcessor.prepare ({ sampleRate, m3BufferSize, numChannels }); + * ... + * arena.reset (arenaBytesNeeded); + * + * // use + * auto m3Buffer = bufferMultiple.processBufferIn (arena, buffer); + * nextProcessor.process (m3Buffer); + * bufferMultiple.processBufferOut (m3Buffer, buffer); + * @endcode + */ +template +class BufferMultiple +{ +public: + BufferMultiple() = default; + + /** + * Prepares the processor for a given multiple. + * + * This method returns the maximum number of samples + * that may be allocated by the arena allocator used + * by processBufferIn(). Note that this number may not + * be a multiple of the requested multiple do to buffer + * padding restrictions. + */ + int prepare (const juce::dsp::ProcessSpec& spec, int multiple) + { + jassert (multiple > 1 && multiple <= static_cast (maxMultiple)); + M = multiple; + numChannels = static_cast (spec.numChannels); + + reset(); + + return Math::round_to_next_multiple (Math::round_to_next_multiple (static_cast (spec.maximumBlockSize), M), + static_cast (SIMDUtils::defaultSIMDAlignment / sizeof (T))); + } + + /** Resets the processor state. */ + void reset() noexcept + { + leftoverDataIn.fill (T {}); + leftoverDataOut.fill (T {}); + numSamplesLeftoverIn = M - 1; + numSamplesLeftoverOut = 1; + } + + /** Returns the latency of the "multiple" buffer. */ + [[nodiscard]] int getMultipleBufferLatency() const noexcept + { + return M - 1; + } + + /** Returns the "round trip" latency for this processor. */ + [[nodiscard]] int getRoundTripLatency() const noexcept + { + return M; + } + + /** Returns a buffer that is the requested multiple number of samples. */ + BufferView processBufferIn (ArenaAllocatorView arena, const BufferView& input) noexcept + { + const auto numSamplesIn = input.getNumSamples(); + const auto numMultiplesOut = (numSamplesIn + numSamplesLeftoverIn - 1) / M; + const auto numSamplesOut = M * numMultiplesOut; + const auto newNumLeftoverSamples = numSamplesIn + numSamplesLeftoverIn - numSamplesOut; + jassert (newNumLeftoverSamples <= M); + + auto leftoverSamplesToUse = std::min (numSamplesLeftoverIn, numSamplesOut); + + const auto bufferOut = make_temp_buffer (arena, input.getNumChannels(), numSamplesOut); + + for (auto [ch, outData] : buffer_iters::channels (bufferOut)) + { + const auto inputs = input.getReadSpan (ch); + const auto leftovers = getLeftoversIn (ch); + + std::copy (leftovers.begin(), leftovers.begin() + leftoverSamplesToUse, outData.begin()); + std::copy (inputs.begin(), inputs.begin() + numSamplesOut - leftoverSamplesToUse, outData.begin() + leftoverSamplesToUse); + + if (leftoverSamplesToUse < numSamplesLeftoverIn) + { + std::copy (leftovers.begin() + leftoverSamplesToUse, leftovers.end(), leftovers.begin()); + } + + std::copy (inputs.begin() + numSamplesOut - leftoverSamplesToUse, + inputs.end(), + leftovers.begin() + numSamplesLeftoverIn - leftoverSamplesToUse); + } + numSamplesLeftoverIn = newNumLeftoverSamples; + + return bufferOut; + } + + /** + * Copies the multiple buffer into some output buffer. + * + * The output buffer is expected to be the same size as the + * most recent buffer provided to processBufferIn. + */ + void processBufferOut (const BufferView& input, const BufferView& output) noexcept + { + const auto numSamplesIn = input.getNumSamples(); + const auto numSamplesOut = output.getNumSamples(); + const auto newNumLeftoverSamples = numSamplesIn + numSamplesLeftoverOut - numSamplesOut; + jassert (newNumLeftoverSamples <= M); + + auto leftoverSamplesToUse = std::min (numSamplesLeftoverOut, numSamplesOut); + + for (auto [ch, outData] : buffer_iters::channels (output)) + { + const auto inputs = input.getReadSpan (ch); + const auto leftovers = getLeftoversOut (ch); + + std::copy (leftovers.begin(), leftovers.begin() + leftoverSamplesToUse, outData.begin()); + std::copy (inputs.begin(), inputs.begin() + numSamplesOut - leftoverSamplesToUse, outData.begin() + leftoverSamplesToUse); + + if (leftoverSamplesToUse < numSamplesLeftoverOut) + { + std::copy (leftovers.begin() + leftoverSamplesToUse, leftovers.end(), leftovers.begin()); + } + + std::copy (inputs.begin() + numSamplesOut - leftoverSamplesToUse, + inputs.end(), + leftovers.begin() + numSamplesLeftoverOut - leftoverSamplesToUse); + } + + numSamplesLeftoverOut = newNumLeftoverSamples; + } + +private: + nonstd::span getLeftoversIn (int ch) + { + return { leftoverDataIn.data() + ch * M, static_cast (M) }; + } + + nonstd::span getLeftoversOut (int ch) + { + return { leftoverDataOut.data() + ch * M, static_cast (M) }; + } + + int M = 0; + int numChannels = 0; + int numSamplesLeftoverIn = 0; + int numSamplesLeftoverOut = -1; + + static constexpr size_t maxMultiple = 8; + std::array leftoverDataIn {}; + std::array leftoverDataOut {}; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BufferMultiple) +}; +} // namespace chowdsp diff --git a/modules/dsp/chowdsp_dsp_data_structures/chowdsp_dsp_data_structures.h b/modules/dsp/chowdsp_dsp_data_structures/chowdsp_dsp_data_structures.h index 726f10d25..8a5ee2bd4 100644 --- a/modules/dsp/chowdsp_dsp_data_structures/chowdsp_dsp_data_structures.h +++ b/modules/dsp/chowdsp_dsp_data_structures/chowdsp_dsp_data_structures.h @@ -46,6 +46,7 @@ BEGIN_JUCE_MODULE_DECLARATION #include "Other/chowdsp_SmoothedBufferValue.h" #include "Processors/chowdsp_RebufferedProcessor.h" +#include "Processors/chowdsp_BufferMultiple.h" #include "LookupTables/chowdsp_LookupTableTransform.h" #include "LookupTables/chowdsp_LookupTableCache.h" diff --git a/tests/dsp_tests/CMakeLists.txt b/tests/dsp_tests/CMakeLists.txt index e3e565e64..b657c8e29 100644 --- a/tests/dsp_tests/CMakeLists.txt +++ b/tests/dsp_tests/CMakeLists.txt @@ -9,6 +9,8 @@ setup_chowdsp_lib(dsp_tests_lib chowdsp_simd chowdsp_waveshapers ) +# Setting this to 1 can be useful for debugging! +target_compile_definitions(dsp_tests_lib PUBLIC CHOWDSP_JASSERT_IS_CASSERT=0) setup_juce_lib(dsp_juce_tests_lib juce::juce_dsp diff --git a/tests/dsp_tests/chowdsp_dsp_data_structures_test/BufferMultipleTest.cpp b/tests/dsp_tests/chowdsp_dsp_data_structures_test/BufferMultipleTest.cpp new file mode 100644 index 000000000..b27ee6fef --- /dev/null +++ b/tests/dsp_tests/chowdsp_dsp_data_structures_test/BufferMultipleTest.cpp @@ -0,0 +1,58 @@ +#include +#include + +#include + +TEST_CASE ("Buffer Multiple Test", "[dsp][data-structures]") +{ + static constexpr int numChannels = 2; + static constexpr int bufferSize = 64; + static constexpr int N = 1000; + + for (int multiple : std::initializer_list { 2, 3, 4, 5, 6, 7 }) + { + std::vector data (N, 0.0f); + std::iota (std::begin (data), std::end (data), 0.0f); + std::vector data2 { data.begin(), data.end() }; + float* dataPtrs[numChannels] = { data.data(), data2.data() }; + + chowdsp::BufferMultiple multipleProcessor {}; + int maxMultipleBufferSize = multipleProcessor.prepare ({ 0.0, (uint32_t) bufferSize, (uint32_t) numChannels }, multiple); + const auto mLatencySamples = multipleProcessor.getMultipleBufferLatency(); + const auto roundTripLatencySamples = multipleProcessor.getRoundTripLatency(); + + chowdsp::ArenaAllocator<> arena { numChannels * maxMultipleBufferSize * sizeof (float) }; + + int inOutCount = 0; + int mCount = 0; + for (int n : std::initializer_list { 6, 20, 1, 3, 15, 16, 5, 7, 18, bufferSize, bufferSize, bufferSize }) + { + chowdsp::BufferView buffer { dataPtrs, numChannels, n, inOutCount }; + const auto mBuffer = multipleProcessor.processBufferIn (arena, buffer); + + REQUIRE (mBuffer.getNumSamples() % multiple == 0); + REQUIRE (mBuffer.getNumSamples() <= maxMultipleBufferSize); + for (auto [ch, channelData] : chowdsp::buffer_iters::channels (mBuffer)) + { + for (const auto& [idx, x] : chowdsp::enumerate (channelData)) + { + REQUIRE (x == std::max (static_cast (mCount + (int) idx - mLatencySamples), 0.0f)); + x = -x; + } + } + + multipleProcessor.processBufferOut (mBuffer, buffer); + for (auto [ch, channelData] : chowdsp::buffer_iters::channels (buffer)) + { + for (const auto& [idx, x] : chowdsp::enumerate (channelData)) + { + REQUIRE (-x == std::max (static_cast (inOutCount + (int) idx - roundTripLatencySamples), 0.0f)); + } + } + + inOutCount += n; + mCount += mBuffer.getNumSamples(); + arena.clear(); + } + } +} diff --git a/tests/dsp_tests/chowdsp_dsp_data_structures_test/CMakeLists.txt b/tests/dsp_tests/chowdsp_dsp_data_structures_test/CMakeLists.txt index aaa984817..b22d6175a 100644 --- a/tests/dsp_tests/chowdsp_dsp_data_structures_test/CMakeLists.txt +++ b/tests/dsp_tests/chowdsp_dsp_data_structures_test/CMakeLists.txt @@ -6,4 +6,5 @@ target_sources(chowdsp_dsp_data_structures_test RebufferProcessorTest.cpp SmoothedBufferValueTest.cpp UIToAudioPipelineTest.cpp + BufferMultipleTest.cpp )