Skip to content

Commit

Permalink
Merge pull request #4852 from davidchocholaty/ring_delay_buffer
Browse files Browse the repository at this point in the history
RingDelayBuffer: ring buffer for delay handling
  • Loading branch information
Swiftb0y authored Aug 29, 2022
2 parents c076a79 + b686546 commit c9d9581
Show file tree
Hide file tree
Showing 4 changed files with 408 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL
src/util/performancetimer.cpp
src/util/rangelist.cpp
src/util/readaheadsamplebuffer.cpp
src/util/ringdelaybuffer.cpp
src/util/rotary.cpp
src/util/runtimeloggingcategory.cpp
src/util/sample.cpp
Expand Down Expand Up @@ -1701,6 +1702,7 @@ add_executable(mixxx-test
src/test/replaygaintest.cpp
src/test/rescalertest.cpp
src/test/rgbcolor_test.cpp
src/test/ringdelaybuffer_test.cpp
src/test/samplebuffertest.cpp
src/test/sampleutiltest.cpp
src/test/schemamanager_test.cpp
Expand Down
176 changes: 176 additions & 0 deletions src/test/ringdelaybuffer_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Tests for ringdelaybuffer.h

#include "util/ringdelaybuffer.h"

#include <benchmark/benchmark.h>
#include <gtest/gtest.h>

#include <QTest>
#include <span>

#include "test/mixxxtest.h"
#include "util/sample.h"
#include "util/samplebuffer.h"
#include "util/span.h"
#include "util/types.h"

namespace {

class RingDelayBufferTest : public MixxxTest {
protected:
void SetUp() override {
m_pRingDelayBuffer = std::make_unique<RingDelayBuffer>(m_ringDelayBufferSize);
}

void TearDown() override {
}

void AssertIdenticalBufferEquals(const std::span<CSAMPLE> buffer,
const std::span<const CSAMPLE> referenceBuffer) {
ASSERT_EQ(buffer.size(), referenceBuffer.size());

for (std::span<CSAMPLE>::size_type i = 0; i < buffer.size(); ++i) {
EXPECT_FLOAT_EQ(buffer[i], referenceBuffer[i]);
}
}

std::unique_ptr<RingDelayBuffer> m_pRingDelayBuffer;
const SINT m_ringDelayBufferSize = 8;
};

TEST_F(RingDelayBufferTest, ReadWriteNoDelayTest) {
const SINT numSamplesHalf = 2;
const SINT numSamples = 4;
const CSAMPLE inputBuffer[] = {-100.0, 100.0, -99.0, 99.0};
const CSAMPLE inputBufferHalf[] = {-98.0, 98.0};
const CSAMPLE firstExpectedResult[] = {-50.0, 50.0, -99.0, 99.0};
const CSAMPLE secondExpectedResult[] = {-99.0, 99.0, -98.0, 98.0};
const CSAMPLE thirdExpectedResult[] = {-100.0, 100.0, -99.0, 99.0};

mixxx::SampleBuffer output(numSamples);

EXPECT_EQ(m_pRingDelayBuffer->write(
mixxx::spanutil::spanFromPtrLen(inputBuffer, numSamples)),
numSamples);
EXPECT_EQ(m_pRingDelayBuffer->read(
output.span(), 0),
numSamples);

AssertIdenticalBufferEquals(output.span(),
mixxx::spanutil::spanFromPtrLen(firstExpectedResult, numSamples));

EXPECT_EQ(m_pRingDelayBuffer->write(
mixxx::spanutil::spanFromPtrLen(inputBufferHalf, numSamplesHalf)),
numSamplesHalf);
EXPECT_EQ(m_pRingDelayBuffer->read(
output.span(), 0),
numSamples);

AssertIdenticalBufferEquals(output.span(),
mixxx::spanutil::spanFromPtrLen(secondExpectedResult, numSamples));

// Write and read over one ring.
EXPECT_EQ(m_pRingDelayBuffer->write(
mixxx::spanutil::spanFromPtrLen(inputBuffer, numSamples)),
numSamples);
EXPECT_EQ(m_pRingDelayBuffer->read(
output.span(), 0),
numSamples);

AssertIdenticalBufferEquals(output.span(),
mixxx::spanutil::spanFromPtrLen(thirdExpectedResult, numSamples));
}

TEST_F(RingDelayBufferTest, ReadWriteDelayTest) {
const SINT numSamples = 4;
const SINT firstDelaySize = 2;
const SINT secondDelaySize = 4;
const CSAMPLE inputBuffer[] = {-100.0, 100.0, -99.0, 99.0};
const CSAMPLE firstExpectedResult[] = {-50.0, 50.0, -99.0, 99.0};
const CSAMPLE secondExpectedResult[] = {0.0, 0.0, -50.0, 50.0};
const CSAMPLE thirdExpectedResult[] = {-50.0, 50.0, -99.0, 99.0};

mixxx::SampleBuffer output(numSamples);

// Read without delay.
EXPECT_EQ(m_pRingDelayBuffer->write(
mixxx::spanutil::spanFromPtrLen(inputBuffer, numSamples)),
numSamples);
EXPECT_EQ(m_pRingDelayBuffer->read(
output.span(), 0),
numSamples);

AssertIdenticalBufferEquals(output.span(),
mixxx::spanutil::spanFromPtrLen(firstExpectedResult, numSamples));

// Read with delay.
EXPECT_EQ(m_pRingDelayBuffer->read(
output.span(), firstDelaySize),
numSamples);

AssertIdenticalBufferEquals(output.span(),
mixxx::spanutil::spanFromPtrLen(secondExpectedResult, numSamples));

// Fill the second half of the delay buffer with the first input data.
EXPECT_EQ(m_pRingDelayBuffer->write(
mixxx::spanutil::spanFromPtrLen(inputBuffer, numSamples)),
numSamples);

// Read with delay (not circle around).
EXPECT_EQ(m_pRingDelayBuffer->read(
output.span(), secondDelaySize),
numSamples);

AssertIdenticalBufferEquals(output.span(),
mixxx::spanutil::spanFromPtrLen(thirdExpectedResult, numSamples));
}

static void BM_WriteReadWholeBufferNoDelay(benchmark::State& state) {
const SINT ringDelayBufferSize = static_cast<SINT>(state.range(0));
const SINT numSamples = ringDelayBufferSize / 2;

RingDelayBuffer m_ringDelayBuffer(ringDelayBufferSize);

mixxx::SampleBuffer input(numSamples);
mixxx::SampleBuffer output(numSamples);

input.fill(0.0f);

for (auto _ : state) {
state.PauseTiming();
m_ringDelayBuffer.clear();
state.ResumeTiming();

m_ringDelayBuffer.write(input.span());
m_ringDelayBuffer.read(output.span(), 0);
m_ringDelayBuffer.write(input.span());
m_ringDelayBuffer.read(output.span(), 0);
}
}
BENCHMARK(BM_WriteReadWholeBufferNoDelay)->Range(64, 4 << 10);

static void BM_WriteReadWholeBufferDelay(benchmark::State& state) {
const SINT ringDelayBufferSize = static_cast<SINT>(state.range(0));
const SINT numSamples = ringDelayBufferSize / 2;
const SINT delaySize = numSamples;

RingDelayBuffer m_ringDelayBuffer(ringDelayBufferSize);

mixxx::SampleBuffer input(numSamples);
mixxx::SampleBuffer output(numSamples);

input.fill(0.0f);

for (auto _ : state) {
state.PauseTiming();
m_ringDelayBuffer.clear();
state.ResumeTiming();

m_ringDelayBuffer.write(input.span());
m_ringDelayBuffer.read(output.span(), delaySize);
m_ringDelayBuffer.write(input.span());
m_ringDelayBuffer.read(output.span(), delaySize);
}
}
BENCHMARK(BM_WriteReadWholeBufferDelay)->Range(64, 4 << 10);
} // namespace
147 changes: 147 additions & 0 deletions src/util/ringdelaybuffer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#include "ringdelaybuffer.h"

#include <span>

#include "util/math.h"
#include "util/sample.h"

namespace {
// The helper function for copying data by using one ring buffer
// and one contiguous buffer or using two contiguous buffers.
// The situation working with two ring buffers is not allowed
// due to this situation is not possible with delay handling
// and for this situation, there is a need for three copies.
// If the copying will come across the upper bound of the ring buffer,
// the next items are copied from/to the start of the ring buffer.
SINT copyRing(const std::span<const CSAMPLE> sourceBuffer,
SINT sourcePos,
const std::span<CSAMPLE> destBuffer,
SINT destPos,
const SINT numItems) {
const unsigned int newSourcePos = sourcePos + numItems;
const unsigned int newDestPos = destPos + numItems;

SINT sourceRemainingItems = sourceBuffer.size() - sourcePos;
SINT destRemainingItems = destBuffer.size() - destPos;

// Do not allow the new positions will both cross the upper bound
// of their buffers. Based on that, the three copies will be needed,
// but for the delay handling use case of the ring buffer,
// this situation is not possible and is not allowed
// in this helper function.
VERIFY_OR_DEBUG_ASSERT(newSourcePos <= sourceBuffer.size() ||
newDestPos <= destBuffer.size()) {
return 0;
}

// Check to see if the copy is not contiguous.
if (newSourcePos > sourceBuffer.size() ||
newDestPos > destBuffer.size()) {
// Copy is not contiguous.
SINT firstDataBlockSize = math_min(sourceRemainingItems, destRemainingItems);

// Copy the first data part until the end of the destination
// or the source buffer.
SampleUtil::copy(destBuffer.last(destRemainingItems).data(),
sourceBuffer.last(sourceRemainingItems).data(),
firstDataBlockSize);

// Calculate new source and destination position.
sourcePos = (sourcePos + firstDataBlockSize) % sourceBuffer.size();
destPos = (destPos + firstDataBlockSize) % destBuffer.size();

// Based on the new source and destination positions recalculate
// the remaining items from the new positions.
sourceRemainingItems = sourceBuffer.size() - sourcePos;
destRemainingItems = destBuffer.size() - destPos;

// The second data part is the start of the source
// or destination buffer.
SampleUtil::copy(destBuffer.last(destRemainingItems).data(),
sourceBuffer.last(sourceRemainingItems).data(),
numItems - firstDataBlockSize);
} else {
// Copy is contiguous.
SampleUtil::copy(destBuffer.last(destRemainingItems).data(),
sourceBuffer.last(sourceRemainingItems).data(),
numItems);
}

return numItems;
}

} // anonymous namespace

RingDelayBuffer::RingDelayBuffer(SINT bufferSize)
: m_firstInputChunk(true),
m_writePos(0),
m_buffer(bufferSize) {
// Set the ring buffer items to 0.
m_buffer.fill(0);
}

SINT RingDelayBuffer::read(std::span<CSAMPLE> destinationBuffer, const SINT delayItems) {
const SINT itemsToRead = destinationBuffer.size();
const SINT shift = itemsToRead + delayItems;

// The reading position shift against the write position
// has to be smaller or equal to the ring buffer size.
VERIFY_OR_DEBUG_ASSERT(shift <= m_buffer.size()) {
return 0;
}

SINT readPos = m_writePos - shift;

// The reading position crossed the left bound of the ring buffer.
// Add the size of the ring buffer to move the position around and keep it
// in the valid index range.
if (readPos < 0) {
readPos = readPos + m_buffer.size();
}

return copyRing(mixxx::spanutil::spanFromPtrLen(m_buffer.data(), m_buffer.size()),
readPos,
destinationBuffer,
0,
itemsToRead);
}

SINT RingDelayBuffer::write(std::span<const CSAMPLE> sourceBuffer) {
const SINT itemsToWrite = sourceBuffer.size();

VERIFY_OR_DEBUG_ASSERT(itemsToWrite <= m_buffer.size()) {
return 0;
}

const SINT numItems = [&]() {
if (m_firstInputChunk) {
// If the first input chunk is written, the first sample is on the index 0.
// Based on the checking of an available number of samples, the situation,
// that the writing will be non-contiguous cannot occur.
SampleUtil::copyWithRampingGain(m_buffer.data(),
sourceBuffer.data(),
0.0f,
1.0f,
itemsToWrite);
m_firstInputChunk = false;
return itemsToWrite;
} else {
return copyRing(sourceBuffer,
0,
mixxx::spanutil::spanFromPtrLen(m_buffer.data(), m_buffer.size()),
m_writePos,
itemsToWrite);
}
}();

// Calculate the new write position. If the new write position
// is after the ring buffer end, move it around from the start
// of the ring buffer.
m_writePos = (m_writePos + numItems);

if (m_writePos >= m_buffer.size()) {
m_writePos = m_writePos - m_buffer.size();
}

return numItems;
}
Loading

0 comments on commit c9d9581

Please sign in to comment.