Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EngineEffectsDelay: effects chain delay handling #4810

Merged
merged 40 commits into from
Aug 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
461f6f7
EngineEffectsDelay: effects chain delay handling
davidchocholaty Jun 18, 2022
ad76619
EngineEffectsDelay: frames instead of samples
davidchocholaty Jun 28, 2022
f40e796
EngineEffectsDelay: add negative delay handling
davidchocholaty Jun 29, 2022
93d2d19
EngineEffectsDelayTest: add delay handling tests
davidchocholaty Jun 29, 2022
97fdb39
EngineEffectsDelayTest: add stereo assertion
davidchocholaty Jul 1, 2022
a1456a6
EffectProcessor: fix audio signals documentation
davidchocholaty Jul 1, 2022
cc5abdd
PitchShiftEffect: move getter into the header
davidchocholaty Jul 1, 2022
e958a08
EngineEffectsDelay: convert ramping to float
davidchocholaty Jul 1, 2022
5d2d560
EngineEffectChain: avoid heap allocation
davidchocholaty Jul 2, 2022
4f7515b
EngineEffectsDelay: ramping for the whole buffer
davidchocholaty Jul 4, 2022
755c714
EngineEffectsDelay: inheritance from EngineObject
davidchocholaty Jul 5, 2022
164e200
EngineEffectsDelay: refactor variable names
davidchocholaty Jul 5, 2022
bc78344
EngineEffectsDelay: remove code duplication
davidchocholaty Jul 5, 2022
5f1ea36
EngineEffectsDelay: add benchmarks for testing
davidchocholaty Jul 6, 2022
1b88485
EngineEffectsDelay: remove M_RESTRICT macro
davidchocholaty Jul 8, 2022
af33185
EngineEffectsDelay: replace const with constexpr
davidchocholaty Jul 8, 2022
5892a7e
EngineEffectsDelay: remove unnecessary casts
davidchocholaty Jul 8, 2022
ea7d77c
Merge remote-tracking branch 'upstream/main' into fix_pitch_shift_lat…
davidchocholaty Jul 8, 2022
047c3e2
EngineEffectsDelayTest: avoid manual allocation
davidchocholaty Jul 8, 2022
1db08fd
EngineEffectsDelay: improve const correctness
davidchocholaty Jul 9, 2022
e84b096
EngineEffectsDelay: modulo calculation doc
davidchocholaty Jul 9, 2022
3da53da
refactor(util): add `std::span` getter over SampleBuffer
Swiftb0y Jul 4, 2022
b0ed62c
SpanUtil: add castToSizeType method
davidchocholaty Jul 18, 2022
ff2fefe
EngineEffectsDelayTest: add using of std::span
davidchocholaty Jul 18, 2022
7107c87
EngineEffectsDelay: rename kiMaxDelay to kMaxDelay
davidchocholaty Jul 18, 2022
ef31bfb
EngineEffectsDelayTest: manual iterators handling
davidchocholaty Jul 18, 2022
11a83a8
EngineEffectsDelayTest: use by-index for loop
davidchocholaty Jul 20, 2022
f40707c
SpanUtil: add checking for signed type
davidchocholaty Jul 21, 2022
42487b2
SpanUtil: replace class with namespace
davidchocholaty Jul 21, 2022
79e6681
SampleBuffer: remove an unnecessary line of code
davidchocholaty Jul 22, 2022
1e2d0f2
EngineEffectsDelayTest: replace with ASSERT
davidchocholaty Jul 22, 2022
8e19313
EngineEffectsDelay: remove magic constant
davidchocholaty Jul 22, 2022
f7a38c9
EngineEffectsDelay: clamp wrong delay values
davidchocholaty Jul 24, 2022
ad65691
EngineEffectsDelayTest: add value clamping comment
davidchocholaty Jul 24, 2022
2e14db1
EngineEffectsDelay: add kMaxDelayFrames constant
davidchocholaty Jul 25, 2022
7f00a1a
EngineEffectsDelay: store input for zero delay
davidchocholaty Jul 25, 2022
8945f3d
spanutil: templated functions as constexpr
davidchocholaty Aug 8, 2022
9ffdb11
SampleBuffer: fix const for span method
davidchocholaty Aug 8, 2022
80b9209
PitchShiftEffect: remove delay reporting
davidchocholaty Aug 8, 2022
5342307
spanutil: add unsigned type checking
davidchocholaty Aug 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions src/effects/backends/builtin/pitchshifteffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ void PitchShiftEffect::processChannel(
pInput,
engineParameters.framesPerBuffer());
pState->m_pRubberBand->process(
//static_cast<const float* const*>(pState->m_retrieveBuffer),
pState->m_retrieveBuffer,
engineParameters.framesPerBuffer(),
false);
Expand All @@ -107,7 +106,6 @@ void PitchShiftEffect::processChannel(
framesAvailable,
engineParameters.framesPerBuffer());
SINT receivedFrames = pState->m_pRubberBand->retrieve(
//static_cast<float* const*>(pState->m_retrieveBuffer),
pState->m_retrieveBuffer,
framesToRead);

Expand Down
1 change: 1 addition & 0 deletions src/effects/backends/builtin/pitchshifteffect.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "util/class.h"
#include "util/defs.h"
#include "util/sample.h"
#include "util/types.h"

namespace RubberBand {
class RubberBandStretcher;
Expand Down
18 changes: 18 additions & 0 deletions src/effects/backends/effectprocessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/engine/effects/engineeffect.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down
6 changes: 6 additions & 0 deletions src/engine/effects/engineeffectchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/engine/effects/engineeffectchain.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <QString>

#include "engine/channelhandle.h"
#include "engine/effects/engineeffectsdelay.h"
#include "engine/effects/groupfeaturestate.h"
#include "engine/effects/message.h"
#include "util/class.h"
Expand Down Expand Up @@ -81,6 +82,7 @@ class EngineEffectChain final : public EffectsRequestHandler {
mixxx::SampleBuffer m_buffer1;
mixxx::SampleBuffer m_buffer2;
ChannelHandleMap<ChannelHandleMap<ChannelStatus>> m_chainStatusForChannelMatrix;
EngineEffectsDelay m_effectsDelay;

DISALLOW_COPY_AND_ASSIGN(EngineEffectChain);
};
79 changes: 79 additions & 0 deletions src/engine/effects/engineeffectsdelay.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you feel lucky you may fiddle around to either use factorized functions or make this loop vetcorized.
This can be checked like described here:

// LOOP VECTORIZED below marks the loops that are processed with the 128 bit SSE

This requires that you chop the loop in chunks to get around the % kiMaxDelay on every sample.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how much vectorizing potential there actually is. I think the n % kiMaxDelay always breaks the autovectorizer because you can't use vectorizing instructions when n is at the wrap-around boundary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requires that you chop the loop in chunks to get around the % kiMaxDelay on every sample.

I missed that in my last note in this thread (which is why that note essentially repeated what you just said). The problem I see is that we simply can't do that. At least the current interface and implementation allows the delay to be any frame frame number. I don't think we can make the interface more granular so we'd have to find a workaround in the implementation.


} 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<CSAMPLE_GAIN> 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;
}
}
81 changes: 81 additions & 0 deletions src/engine/effects/engineeffectsdelay.h
Original file line number Diff line number Diff line change
@@ -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;
};
Loading