Skip to content

Commit

Permalink
Adding BufferMultiple processor (Chowdhury-DSP#563)
Browse files Browse the repository at this point in the history
* Starting on Buffer Multiple processor

* Buffer multiple passes tests

* Add comments

* Cmake

* Apply clang-format

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
jatinchowdhury18 and github-actions[bot] committed Nov 5, 2024
1 parent e0c8ba0 commit 53df542
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
#pragma once

#include <cassert>

/**
* 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)

Expand Down
4 changes: 4 additions & 0 deletions modules/common/chowdsp_core/chowdsp_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions modules/dsp/chowdsp_buffers/Buffers/chowdsp_BufferView.h
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ class BufferView
std::enable_if_t<! std::is_const_v<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;
}
Expand All @@ -255,7 +255,7 @@ class BufferView
std::enable_if_t<std::is_const_v<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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<float> 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 <typename T>
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<int> (maxMultiple));
M = multiple;
numChannels = static_cast<int> (spec.numChannels);

reset();

return Math::round_to_next_multiple (Math::round_to_next_multiple (static_cast<int> (spec.maximumBlockSize), M),
static_cast<int> (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<T> processBufferIn (ArenaAllocatorView arena, const BufferView<const T>& 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<T> (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<const T>& input, const BufferView<T>& 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<T> getLeftoversIn (int ch)
{
return { leftoverDataIn.data() + ch * M, static_cast<size_t> (M) };
}

nonstd::span<T> getLeftoversOut (int ch)
{
return { leftoverDataOut.data() + ch * M, static_cast<size_t> (M) };
}

int M = 0;
int numChannels = 0;
int numSamplesLeftoverIn = 0;
int numSamplesLeftoverOut = -1;

static constexpr size_t maxMultiple = 8;
std::array<T, CHOWDSP_BUFFER_MAX_NUM_CHANNELS * maxMultiple> leftoverDataIn {};
std::array<T, CHOWDSP_BUFFER_MAX_NUM_CHANNELS * maxMultiple> leftoverDataOut {};

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BufferMultiple)
};
} // namespace chowdsp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
2 changes: 2 additions & 0 deletions tests/dsp_tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#include <CatchUtils.h>
#include <chowdsp_dsp_data_structures/chowdsp_dsp_data_structures.h>

#include <iostream>

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<int> { 2, 3, 4, 5, 6, 7 })
{
std::vector<float> data (N, 0.0f);
std::iota (std::begin (data), std::end (data), 0.0f);
std::vector<float> data2 { data.begin(), data.end() };
float* dataPtrs[numChannels] = { data.data(), data2.data() };

chowdsp::BufferMultiple<float> 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<int> { 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<float> (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<float> (inOutCount + (int) idx - roundTripLatencySamples), 0.0f));
}
}

inOutCount += n;
mCount += mBuffer.getNumSamples();
arena.clear();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ target_sources(chowdsp_dsp_data_structures_test
RebufferProcessorTest.cpp
SmoothedBufferValueTest.cpp
UIToAudioPipelineTest.cpp
BufferMultipleTest.cpp
)

0 comments on commit 53df542

Please sign in to comment.