diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a2a0e29432..22174b2ac24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -628,6 +628,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/engine/controls/ratecontrol.cpp src/engine/effects/engineeffect.cpp src/engine/effects/engineeffectchain.cpp + src/engine/effects/engineeffectsdelay.cpp src/engine/effects/engineeffectsmanager.cpp src/engine/enginebuffer.cpp src/engine/enginedelay.cpp @@ -1645,6 +1646,7 @@ add_executable(mixxx-test #src/test/effectchainslottest.cpp src/test/enginebufferscalelineartest.cpp src/test/enginebuffertest.cpp + src/test/engineeffectsdelay_test.cpp src/test/enginefilterbiquadtest.cpp src/test/enginemastertest.cpp src/test/enginemicrophonetest.cpp diff --git a/src/effects/backends/builtin/pitchshifteffect.cpp b/src/effects/backends/builtin/pitchshifteffect.cpp index c64076cc471..7065ce0c1b0 100644 --- a/src/effects/backends/builtin/pitchshifteffect.cpp +++ b/src/effects/backends/builtin/pitchshifteffect.cpp @@ -97,7 +97,6 @@ void PitchShiftEffect::processChannel( pInput, engineParameters.framesPerBuffer()); pState->m_pRubberBand->process( - //static_cast(pState->m_retrieveBuffer), pState->m_retrieveBuffer, engineParameters.framesPerBuffer(), false); @@ -107,7 +106,6 @@ void PitchShiftEffect::processChannel( framesAvailable, engineParameters.framesPerBuffer()); SINT receivedFrames = pState->m_pRubberBand->retrieve( - //static_cast(pState->m_retrieveBuffer), pState->m_retrieveBuffer, framesToRead); diff --git a/src/effects/backends/builtin/pitchshifteffect.h b/src/effects/backends/builtin/pitchshifteffect.h index ba36a95d8c5..0069e57cd28 100644 --- a/src/effects/backends/builtin/pitchshifteffect.h +++ b/src/effects/backends/builtin/pitchshifteffect.h @@ -10,6 +10,7 @@ #include "util/class.h" #include "util/defs.h" #include "util/sample.h" +#include "util/types.h" namespace RubberBand { class RubberBandStretcher; diff --git a/src/effects/backends/effectprocessor.h b/src/effects/backends/effectprocessor.h index 837da2b9104..7fb8c039be3 100644 --- a/src/effects/backends/effectprocessor.h +++ b/src/effects/backends/effectprocessor.h @@ -98,6 +98,18 @@ class EffectProcessor { const mixxx::EngineParameters& engineParameters, const EffectEnableState enableState, const GroupFeatureState& groupFeatures) = 0; + + /// This method is used for obtaining the delay of the output buffer + /// compared to the input buffer based on the internal effect processing. + /// The method returns the number of frames by which the dry signal + /// needs to be delayed so that buffers for the dry and wet signal (output + /// of the effect) overlap. The return value represents the current effect + /// latency. The value is used in the EngineEffectChain::process method + /// to calculate the resulting latency of the effect chain. Based + /// on the sum of the delay value of every effect in the effect chain, + /// the dry signal is delayed to overlap with the output wet signal + /// after processing all effects in the effects chain. + virtual SINT getGroupDelayFrames() = 0; }; /// EffectProcessorImpl manages a separate EffectState for every combination of @@ -149,6 +161,12 @@ class EffectProcessorImpl : public EffectProcessor { const EffectEnableState enableState, const GroupFeatureState& groupFeatures) = 0; + /// By default, the group delay for every effect is zero. The effect implementation + /// can override this method and set actual number of frames for the effect delay. + virtual SINT getGroupDelayFrames() override { + return 0; + } + void process(const ChannelHandle& inputHandle, const ChannelHandle& outputHandle, const CSAMPLE* pInput, diff --git a/src/engine/effects/engineeffect.h b/src/engine/effects/engineeffect.h index ea83a6fbc15..c9409e74e07 100644 --- a/src/engine/effects/engineeffect.h +++ b/src/engine/effects/engineeffect.h @@ -15,6 +15,7 @@ #include "engine/effects/groupfeaturestate.h" #include "engine/effects/message.h" #include "util/memory.h" +#include "util/types.h" /// EngineEffect is a generic wrapper around an EffectProcessor which intermediates /// between an EffectSlot and the EffectProcessor. It implements the logic to handle @@ -63,6 +64,10 @@ class EngineEffect final : public EffectsRequestHandler { return m_pManifest->name(); } + SINT getGroupDelayFrames() { + return m_pProcessor->getGroupDelayFrames(); + } + private: QString debugString() const { return QString("EngineEffect(%1)").arg(m_pManifest->name()); diff --git a/src/engine/effects/engineeffectchain.cpp b/src/engine/effects/engineeffectchain.cpp index 5aea2ad2b02..8fd321c9d77 100644 --- a/src/engine/effects/engineeffectchain.cpp +++ b/src/engine/effects/engineeffectchain.cpp @@ -265,6 +265,7 @@ bool EngineEffectChain::process(const ChannelHandle& inputHandle, // requires that the input buffer does not get modified. CSAMPLE* pIntermediateInput = pIn; CSAMPLE* pIntermediateOutput; + SINT effectChainGroupDelayFrames = 0; bool firstAddDryToWetEffectProcessed = false; for (EngineEffect* pEffect : qAsConst(m_effects)) { @@ -308,12 +309,17 @@ bool EngineEffectChain::process(const ChannelHandle& inputHandle, } processingOccured = true; + effectChainGroupDelayFrames += pEffect->getGroupDelayFrames(); + // Output of this effect becomes the input of the next effect pIntermediateInput = pIntermediateOutput; } } } + m_effectsDelay.setDelayFrames(effectChainGroupDelayFrames); + m_effectsDelay.process(pIn, numSamples); + if (processingOccured) { // pIntermediateInput is the output of the last processed effect. It would be the // intermediate input of the next effect if there was one. diff --git a/src/engine/effects/engineeffectchain.h b/src/engine/effects/engineeffectchain.h index 9bb5aa6ac35..533c5f0eec2 100644 --- a/src/engine/effects/engineeffectchain.h +++ b/src/engine/effects/engineeffectchain.h @@ -4,6 +4,7 @@ #include #include "engine/channelhandle.h" +#include "engine/effects/engineeffectsdelay.h" #include "engine/effects/groupfeaturestate.h" #include "engine/effects/message.h" #include "util/class.h" @@ -81,6 +82,7 @@ class EngineEffectChain final : public EffectsRequestHandler { mixxx::SampleBuffer m_buffer1; mixxx::SampleBuffer m_buffer2; ChannelHandleMap> m_chainStatusForChannelMatrix; + EngineEffectsDelay m_effectsDelay; DISALLOW_COPY_AND_ASSIGN(EngineEffectChain); }; diff --git a/src/engine/effects/engineeffectsdelay.cpp b/src/engine/effects/engineeffectsdelay.cpp new file mode 100644 index 00000000000..69f8aa80325 --- /dev/null +++ b/src/engine/effects/engineeffectsdelay.cpp @@ -0,0 +1,79 @@ +#include "engine/effects/engineeffectsdelay.h" + +#include "util/rampingvalue.h" +#include "util/sample.h" + +EngineEffectsDelay::EngineEffectsDelay() + : m_currentDelaySamples(0), + m_prevDelaySamples(0), + m_delayBufferWritePos(0) { + m_pDelayBuffer = SampleUtil::alloc(kDelayBufferSize); + SampleUtil::clear(m_pDelayBuffer, kDelayBufferSize); +} + +void EngineEffectsDelay::process(CSAMPLE* pInOut, + const int iBufferSize) { + if (m_prevDelaySamples == 0 && m_currentDelaySamples == 0) { + for (int i = 0; i < iBufferSize; ++i) { + // Put samples into delay buffer. + m_pDelayBuffer[m_delayBufferWritePos] = pInOut[i]; + m_delayBufferWritePos = (m_delayBufferWritePos + 1) % kDelayBufferSize; + } + + return; + } + + // The "+ kDelayBufferSize" addition ensures positive values for the modulo calculation. + // From a mathematical point of view, this addition can be removed. Anyway, + // from the cpp point of view, the modulo operator for negative values + // (for example, x % y, where x is a negative value) produces negative results + // (but in math the result value is positive). + int delaySourcePos = + (m_delayBufferWritePos + kDelayBufferSize - m_currentDelaySamples) % + kDelayBufferSize; + + if (m_prevDelaySamples == m_currentDelaySamples) { + for (int i = 0; i < iBufferSize; ++i) { + // Put samples into delay buffer. + m_pDelayBuffer[m_delayBufferWritePos] = pInOut[i]; + m_delayBufferWritePos = (m_delayBufferWritePos + 1) % kDelayBufferSize; + + // Take a delayed sample from the delay buffer + // and copy it to the destination buffer. + pInOut[i] = m_pDelayBuffer[delaySourcePos]; + delaySourcePos = (delaySourcePos + 1) % kDelayBufferSize; + } + + } else { + // The "+ kDelayBufferSize" addition ensures positive values for the modulo calculation. + // From a mathematical point of view, this addition can be removed. Anyway, + // from the cpp point of view, the modulo operator for negative values + // (for example, x % y, where x is a negative value) produces negative results + // (but in math the result value is positive). + int oldDelaySourcePos = + (m_delayBufferWritePos + kDelayBufferSize - m_prevDelaySamples) % + kDelayBufferSize; + + const RampingValue delayChangeRamped(0.0f, 1.0f, iBufferSize); + + for (int i = 0; i < iBufferSize; ++i) { + // Put samples into delay buffer. + m_pDelayBuffer[m_delayBufferWritePos] = pInOut[i]; + m_delayBufferWritePos = (m_delayBufferWritePos + 1) % kDelayBufferSize; + + // Take delayed samples from the delay buffer + // and with the use of ramping (cross-fading), + // calculate the result sample value + // and put it into the dest buffer. + CSAMPLE_GAIN crossMix = delayChangeRamped.getNth(i); + + pInOut[i] = m_pDelayBuffer[oldDelaySourcePos] * (1.0f - crossMix); + pInOut[i] += m_pDelayBuffer[delaySourcePos] * crossMix; + + oldDelaySourcePos = (oldDelaySourcePos + 1) % kDelayBufferSize; + delaySourcePos = (delaySourcePos + 1) % kDelayBufferSize; + } + + m_prevDelaySamples = m_currentDelaySamples; + } +} diff --git a/src/engine/effects/engineeffectsdelay.h b/src/engine/effects/engineeffectsdelay.h new file mode 100644 index 00000000000..0900f4c42d3 --- /dev/null +++ b/src/engine/effects/engineeffectsdelay.h @@ -0,0 +1,81 @@ +#pragma once + +#include "engine/engine.h" +#include "engine/engineobject.h" +#include "util/assert.h" +#include "util/sample.h" +#include "util/types.h" + +namespace { +static constexpr int kMaxDelayFrames = + mixxx::audio::SampleRate::kValueMax - 1; +static constexpr int kDelayBufferSize = + mixxx::audio::SampleRate::kValueMax * mixxx::kEngineChannelCount; +} // anonymous namespace + +/// The effect can produce the output signal with a specific delay caused +/// by the inner effect processing. Based on that the signal on the input +/// of the effect does not overlap with the signal on the effect output. +/// For using the effect in the Dry/Wet or Dry+Wet mode the dry signal +/// and wet signal do not overlap and the effect latency is recognizable. +/// +/// The EngineEffectsDelay offers a solution to this situation by delaying +/// the input dry signal by a specific amount of delay. The signal delaying +/// handles the class method EngineEffectsDelay::process. The method +/// can be used for the one specific effect same as the effect chain if +/// the set delay is the delay of the whole effect chain (sum of the delay +/// of all effects in the effect chain). +/// +/// After delaying the non-delayed signal, both signals (delayed +/// and non-delayed) can be mixed and used together. +class EngineEffectsDelay final : public EngineObject { + public: + EngineEffectsDelay(); + + virtual ~EngineEffectsDelay(){}; + + /// Called from the audio thread + + /// The method sets the number of frames of which will + /// the EngineEffectsDelay::process method delays the input signal. + /// By default, the EngineEffectsDelay::process method works with + /// a zero delay. When is the delay set, the EngineEffectsDelay::process + /// method works with this set delay value until the value is changed. + void setDelayFrames(SINT delayFrames) { + VERIFY_OR_DEBUG_ASSERT(delayFrames >= 0) { + delayFrames = 0; + } + VERIFY_OR_DEBUG_ASSERT(delayFrames <= kMaxDelayFrames) { + delayFrames = kMaxDelayFrames; + } + + // Delay is reported from the effect chain by a number of frames + // to aware problems with a number of channels. The inner + // EngineEffectsDelay structure works with delay samples, so the value + // is recalculated for the EngineEffectsDelay usage. + m_currentDelaySamples = delayFrames * mixxx::kEngineChannelCount; + } + + /// The method delays the input buffer by the set number of samples + /// and returns the result in the output buffer. The input buffer + /// is not changed. For zero delay the input buffer is copied into + /// the output buffer. For non-zero delay, the output signal is returned + /// with the delay. + /// + /// If the number of delay samples hasn't changed, between two + /// EngineEffectsDelay::process method calls, the delayed signal buffers + /// directly follow each other as an input buffer, however, with a delay. + /// + /// If the number of delayed samples has changed between two + /// EngineEffectsDelay::process method calls, the new delay value is set + /// as actual and the output buffer is filled using cross-fading + /// of the presumed output buffer for the previous delay value + /// and of the output buffer created using the new delay value. + void process(CSAMPLE* pInOut, const int iBufferSize) override; + + private: + SINT m_currentDelaySamples; + SINT m_prevDelaySamples; + SINT m_delayBufferWritePos; + CSAMPLE* m_pDelayBuffer; +}; diff --git a/src/test/engineeffectsdelay_test.cpp b/src/test/engineeffectsdelay_test.cpp new file mode 100644 index 00000000000..6dda7f043d4 --- /dev/null +++ b/src/test/engineeffectsdelay_test.cpp @@ -0,0 +1,447 @@ +// Tests for engineeffectsdelay.cpp + +// Premise: internal Mixxx structure works with a stereo signal. +// If the mixxx::kEngineChannelCount wouldn't be a stereo in the future, +// tests have to be updated. + +#include "engine/effects/engineeffectsdelay.h" + +#include +#include + +#include +#include +#include + +#include "engine/engine.h" +#include "test/mixxxtest.h" +#include "util/sample.h" +#include "util/samplebuffer.h" +#include "util/types.h" + +namespace { + +static_assert(mixxx::kEngineChannelCount == mixxx::audio::ChannelCount::stereo(), + "EngineEffectsDelayTest requires stereo input signal."); + +class EngineEffectsDelayTest : public MixxxTest { + protected: + void AssertIdenticalBufferEquals(const std::span buffer, + const std::span referenceBuffer) { + ASSERT_EQ(buffer.size(), referenceBuffer.size()); + + for (std::span::size_type i = 0; i < buffer.size(); ++i) { + EXPECT_FLOAT_EQ(buffer[i], referenceBuffer[i]); + } + } + + EngineEffectsDelay m_effectsDelay; +}; + +//Test's purpose is to test clamping of the delay value in setter (lower bound). +TEST_F(EngineEffectsDelayTest, NegativeDelayValue) { +#ifdef MIXXX_DEBUG_ASSERTIONS_ENABLED + // Set thread safe for EXPECT_DEATH. + GTEST_FLAG_SET(death_test_style, "threadsafe"); + + EXPECT_DEATH({ + // Set negative delay value. + m_effectsDelay.setDelayFrames(-1); + }, + "delayFrames >= 0"); +#else + const SINT numSamples = 4; + + // Set negative delay value. + m_effectsDelay.setDelayFrames(-1); + + const CSAMPLE inputBuffer[] = {-100.0, 100.0, -99.0, 99.0}; + const CSAMPLE expectedResult[] = {-100.0, 100.0, -99.0, 99.0}; + + mixxx::SampleBuffer pInOut(numSamples); + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), expectedResult); +#endif +} + +//Test's purpose is to test clamping of the delay value in setter (upper bound). +TEST_F(EngineEffectsDelayTest, DelayGreaterThanDelayBufferSize) { + const SINT numDelayFrames = mixxx::audio::SampleRate::kValueMax + 2; + +#ifdef MIXXX_DEBUG_ASSERTIONS_ENABLED + // Set thread safe for EXPECT_DEATH. + GTEST_FLAG_SET(death_test_style, "threadsafe"); + + EXPECT_DEATH({ + // Set delay greater than the size of the delay buffer. + m_effectsDelay.setDelayFrames(numDelayFrames); + }, + "delayFrames <= kMaxDelayFrames"); +#else + const SINT numSamples = 4; + + // Set delay greater than the size of the delay buffer. + m_effectsDelay.setDelayFrames(numDelayFrames); + + const CSAMPLE inputBuffer[] = {-100.0, 100.0, -99.0, 99.0}; + const CSAMPLE expectedResult[] = {-100.0, 75.0, -49.5, 24.75}; + + mixxx::SampleBuffer pInOut(numSamples); + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), expectedResult); +#endif +} + +TEST_F(EngineEffectsDelayTest, WholeBufferDelay) { + const SINT numDelayFrames = 2; + const SINT numSamples = 4; + + // Set delay same as the size of the input buffer. + m_effectsDelay.setDelayFrames(numDelayFrames); + + const CSAMPLE inputBuffer[] = {-100.0, 100.0, -99.0, 99.0}; + const CSAMPLE zeroBuffer[] = {0.0, 0.0, 0.0, 0.0}; + const CSAMPLE firstExpectedResult[] = {-100.0, 75.0, -49.5, 24.75}; + + const CSAMPLE secondExpectedResult[] = {-100.0, 100.0, -99.0, 99.0}; + const CSAMPLE thirdExpectedResult[] = {0.0, 0.0, 0.0, 0.0}; + + mixxx::SampleBuffer pInOut(numSamples); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), firstExpectedResult); + + SampleUtil::copy(pInOut.data(), zeroBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), secondExpectedResult); + + SampleUtil::copy(pInOut.data(), zeroBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), thirdExpectedResult); +} + +TEST_F(EngineEffectsDelayTest, HalfBufferDelay) { + const SINT numDelayFrames = 1; + const SINT numSamples = 4; + + // Set delay size of half of the input buffer. + m_effectsDelay.setDelayFrames(numDelayFrames); + + const CSAMPLE inputBuffer[] = {-100.0, 100.0, -99.0, 99.0}; + const CSAMPLE zeroBuffer[] = {0.0, 0.0, 0.0, 0.0}; + const CSAMPLE firstExpectedResult[] = {-100.0, 75.0, -99.5, 99.75}; + const CSAMPLE secondExpectedResult[] = {-99.0, 99.0, -100.0, 100.0}; + const CSAMPLE thirdExpectedResult[] = {-99.0, 99.0, 0.0, 0.0}; + + mixxx::SampleBuffer pInOut(numSamples); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), firstExpectedResult); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), secondExpectedResult); + + SampleUtil::copy(pInOut.data(), zeroBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), thirdExpectedResult); +} + +TEST_F(EngineEffectsDelayTest, MisalignedDelayAccordingToBuffer) { + const SINT numDelayFrames = 3; + const SINT numSamples = 8; + + // Set the number of delay frames different from the input buffer size + // or half of the input buffer size. + m_effectsDelay.setDelayFrames(numDelayFrames); + + const CSAMPLE inputBuffer[] = { + -100.0, 100.0, -99.0, 99.0, -98.0, 98.0, -97.0, 97.0}; + const CSAMPLE zeroBuffer[] = { + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + const CSAMPLE firstExpectedResult[] = { + -100.0, 87.5, -74.25, 61.875, -49.0, 36.75, -99.25, 99.625}; + const CSAMPLE secondExpectedResult[] = { + -99.0, 99.0, -98.0, 98.0, -97.0, 97.0, -100.0, 100.0}; + const CSAMPLE thirdExpectedResult[] = { + -99.0, 99.0, -98.0, 98.0, -97.0, 97.0, 0.0, 0.0}; + + mixxx::SampleBuffer pInOut(numSamples); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), firstExpectedResult); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), secondExpectedResult); + + SampleUtil::copy(pInOut.data(), zeroBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), thirdExpectedResult); +} + +TEST_F(EngineEffectsDelayTest, CrossfadeBetweenTwoNonZeroDelays) { + SINT numDelayFrames = 2; + const SINT numSamples = 8; + + // Set the number of delay frames as half of the input buffer size. + m_effectsDelay.setDelayFrames(numDelayFrames); + + const CSAMPLE inputBuffer[] = { + -100.0, 100.0, -99.0, 99.0, -98.0, 98.0, -97.0, 97.0}; + + const CSAMPLE firstExpectedResult[] = { + -100.0, 87.5, -74.25, 61.875, -99.0, 99.25, -98.5, 98.75}; + const CSAMPLE secondExpectedResult[] = { + -98.0, 98.0, -97.0, 97.0, -100.0, 100.0, -99.0, 99.0}; + const CSAMPLE thirdExpectedResult[] = { + -98.0, 98.25, -97.5, 97.75, -99.0, 98.75, -97.5, 97.25}; + const CSAMPLE fourthExpectedResult[] = { + -100.0, 100.0, -99.0, 99.0, -98.0, 98.0, -97.0, 97.0}; + + mixxx::SampleBuffer pInOut(numSamples); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), firstExpectedResult); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), secondExpectedResult); + + // Set the number of delay frames as the size of the input buffer. + numDelayFrames = 4; + m_effectsDelay.setDelayFrames(numDelayFrames); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), thirdExpectedResult); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), fourthExpectedResult); +} + +TEST_F(EngineEffectsDelayTest, CrossfadeSecondDelayGreaterThanInputBufferSize) { + SINT numDelayFrames = 2; + const SINT numSamples = 8; + + m_effectsDelay.setDelayFrames(numDelayFrames); + + const CSAMPLE inputBuffer[] = { + -100.0, 100.0, -99.0, 99.0, -98.0, 98.0, -97.0, 97.0}; + + const CSAMPLE firstExpectedResult[] = { + -100.0, 87.5, -74.25, 61.875, -99.0, 99.25, -98.5, 98.75}; + const CSAMPLE secondExpectedResult[] = { + -98.0, 98.0, -97.0, 97.0, -100.0, 100.0, -99.0, 99.0}; + const CSAMPLE thirdExpectedResult[] = { + -98.0, 98.125, -97.25, 97.375, -98.5, 98.125, -99.75, 99.875}; + const CSAMPLE fourthExpectedResult[] = { + -99.0, 99.0, -98.0, 98.0, -97.0, 97.0, -100.0, 100.0}; + + mixxx::SampleBuffer pInOut(numSamples); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), firstExpectedResult); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), secondExpectedResult); + + // Set the number of frames greater than the size of the input buffer. + numDelayFrames = 7; + m_effectsDelay.setDelayFrames(numDelayFrames); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), thirdExpectedResult); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), fourthExpectedResult); +} + +TEST_F(EngineEffectsDelayTest, CrossfadeBetweenThreeNonZeroDelays) { + SINT numDelayFrames = 3; + const SINT numSamples = 8; + + m_effectsDelay.setDelayFrames(numDelayFrames); + + const CSAMPLE inputBuffer[] = { + -100.0, 100.0, -99.0, 99.0, -98.0, 98.0, -97.0, 97.0}; + + const CSAMPLE firstExpectedResult[] = { + -100.0, 87.5, -74.25, 61.875, -49.0, 36.75, -99.25, 99.625}; + const CSAMPLE secondExpectedResult[] = { + -99.0, 99.0, -98.0, 98.0, -97.0, 97.0, -100.0, 100.0}; + const CSAMPLE thirdExpectedResult[] = { + -99.0, 98.75, -98.5, 98.75, -98.0, 98.25, -98.5, 98.25}; + const CSAMPLE fourthExpectedResult[] = { + -97.0, 97.0, -100.0, 100.0, -99.0, 99.0, -98.0, 98.0}; + const CSAMPLE fifthExpectedResult[] = { + -97.0, 97.25, -99.5, 99.25, -98.0, 97.75, -99.5, 99.75}; + const CSAMPLE sixthExpectedResult[] = { + -99.0, 99.0, -98.0, 98.0, -97.0, 97.0, -100.0, 100.0}; + + mixxx::SampleBuffer pInOut(numSamples); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), firstExpectedResult); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), secondExpectedResult); + + numDelayFrames = 1; + m_effectsDelay.setDelayFrames(numDelayFrames); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), thirdExpectedResult); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), fourthExpectedResult); + + // Set the number of frames greater than the size of the input buffer. + numDelayFrames = 7; + m_effectsDelay.setDelayFrames(numDelayFrames); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), fifthExpectedResult); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), sixthExpectedResult); +} + +TEST_F(EngineEffectsDelayTest, CopyWholeBufferForZeroDelay) { + const SINT numSamples = 4; + + const CSAMPLE inputBuffer[] = {-100.0, 100.0, -99.0, 99.0}; + const CSAMPLE zeroBuffer[] = {0.0, 0.0, 0.0, 0.0}; + const CSAMPLE firstExpectedResult[] = {-100.0, 100.0, -99.0, 99.0}; + const CSAMPLE secondExpectedResult[] = {0.0, 0.0, 0.0, 0.0}; + + mixxx::SampleBuffer pInOut(numSamples); + + SampleUtil::copy(pInOut.data(), inputBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), firstExpectedResult); + + SampleUtil::copy(pInOut.data(), zeroBuffer, numSamples); + m_effectsDelay.process(pInOut.data(), numSamples); + AssertIdenticalBufferEquals(pInOut.span(), secondExpectedResult); +} + +static void BM_ZeroDelay(benchmark::State& state) { + const SINT bufferSizeInSamples = static_cast(state.range(0)); + + EngineEffectsDelay effectsDelay; + + mixxx::SampleBuffer pInOut(bufferSizeInSamples); + SampleUtil::fill(pInOut.data(), 0.0f, bufferSizeInSamples); + + for (auto _ : state) { + effectsDelay.process(pInOut.data(), bufferSizeInSamples); + } +} +BENCHMARK(BM_ZeroDelay)->Range(64, 4 << 10); + +static void BM_DelaySmallerThanBufferSize(benchmark::State& state) { + const SINT bufferSizeInSamples = static_cast(state.range(0)); + const SINT bufferSizeInFrames = bufferSizeInSamples / mixxx::kEngineChannelCount; + + // The delay is half of the buffer size. + const SINT delayFrames = bufferSizeInFrames / 2; + + EngineEffectsDelay effectsDelay; + + mixxx::SampleBuffer pInOut(bufferSizeInSamples); + SampleUtil::fill(pInOut.data(), 0.0f, bufferSizeInSamples); + + effectsDelay.setDelayFrames(delayFrames); + + for (auto _ : state) { + effectsDelay.process(pInOut.data(), bufferSizeInSamples); + } +} +BENCHMARK(BM_DelaySmallerThanBufferSize)->Range(64, 4 << 10); + +static void BM_DelayGreaterThanBufferSize(benchmark::State& state) { + const SINT bufferSizeInSamples = static_cast(state.range(0)); + const SINT bufferSizeInFrames = bufferSizeInSamples / mixxx::kEngineChannelCount; + + // The delay is the same as twice of buffer size. + const SINT delayFrames = bufferSizeInFrames * 2; + + EngineEffectsDelay effectsDelay; + + mixxx::SampleBuffer pInOut(bufferSizeInSamples); + SampleUtil::fill(pInOut.data(), 0.0f, bufferSizeInSamples); + + effectsDelay.setDelayFrames(delayFrames); + + for (auto _ : state) { + effectsDelay.process(pInOut.data(), bufferSizeInSamples); + } +} +BENCHMARK(BM_DelayGreaterThanBufferSize)->Range(64, 4 << 10); + +static void BM_DelayCrossfading(benchmark::State& state) { + const SINT bufferSizeInSamples = static_cast(state.range(0)); + const SINT bufferSizeInFrames = bufferSizeInSamples / mixxx::kEngineChannelCount; + + // The first delay is half of the buffer size. + const SINT firstDelayFrames = bufferSizeInFrames / 2; + + // The second delay is the same as twice of buffer size. + const SINT secondDelayFrames = bufferSizeInFrames * 2; + + EngineEffectsDelay effectsDelay; + + mixxx::SampleBuffer pInOut(bufferSizeInSamples); + SampleUtil::fill(pInOut.data(), 0.0f, bufferSizeInSamples); + + for (auto _ : state) { + effectsDelay.setDelayFrames(firstDelayFrames); + effectsDelay.process(pInOut.data(), bufferSizeInSamples); + effectsDelay.setDelayFrames(secondDelayFrames); + effectsDelay.process(pInOut.data(), bufferSizeInSamples); + } +} +BENCHMARK(BM_DelayCrossfading)->Range(64, 4 << 10); + +static void BM_DelayNoCrossfading(benchmark::State& state) { + const SINT bufferSizeInSamples = static_cast(state.range(0)); + const SINT bufferSizeInFrames = bufferSizeInSamples / mixxx::kEngineChannelCount; + + // The delay is half of the buffer size. + const SINT delayFrames = bufferSizeInFrames / 2; + + EngineEffectsDelay effectsDelay; + + mixxx::SampleBuffer pInOut(bufferSizeInSamples); + SampleUtil::fill(pInOut.data(), 0.0f, bufferSizeInSamples); + + for (auto _ : state) { + effectsDelay.setDelayFrames(delayFrames); + effectsDelay.process(pInOut.data(), bufferSizeInSamples); + effectsDelay.setDelayFrames(delayFrames); + effectsDelay.process(pInOut.data(), bufferSizeInSamples); + } +} +BENCHMARK(BM_DelayNoCrossfading)->Range(64, 4 << 10); + +} // namespace diff --git a/src/util/samplebuffer.h b/src/util/samplebuffer.h index d7ecebd64d8..e3cb833a79c 100644 --- a/src/util/samplebuffer.h +++ b/src/util/samplebuffer.h @@ -2,7 +2,9 @@ #include // std::swap +#include +#include "util/span.h" #include "util/types.h" @@ -72,6 +74,13 @@ class SampleBuffer final { return m_data + offset; } + std::span span() { + return mixxx::spanutil::spanFromPtrLen(m_data, m_size); + } + std::span span() const { + return mixxx::spanutil::spanFromPtrLen(m_data, m_size); + } + CSAMPLE& operator[](SINT index) { return *data(index); } diff --git a/src/util/span.h b/src/util/span.h new file mode 100644 index 00000000000..1df7704d6b8 --- /dev/null +++ b/src/util/span.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include "util/assert.h" + +namespace mixxx { + +/// Offers a group of utilities (functions) for working with std::span. +namespace spanutil { +/// The function casts data type of size to data type of size_type from std::span. +/// At the same time, the function provides appropriate lower bound checking +/// for signed data types. +template::size_type> +constexpr T2 castToSizeType(S size) { + if constexpr (std::is_signed_v && std::is_unsigned_v) { + VERIFY_OR_DEBUG_ASSERT(size >= 0) { + size = 0; + } + } + + return static_cast(size); +} + +/// The function creates std::span from pointer and size. +/// In most cases, the pointer to the raw data of a data structure +/// is used, and the size of the data structure. +template +constexpr std::span spanFromPtrLen(T* ptr, S size) { + return std::span{ptr, mixxx::spanutil::castToSizeType(size)}; +} + +} // namespace spanutil + +} // namespace mixxx