Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bfaf108
sync echo effect to BPM
Be-ing May 10, 2017
b6e3eeb
Merge remote-tracking branch 'upstream/master' into tempo_sync_echo
Be-ing May 10, 2017
032f7b6
rename Echo parameter Delay to Time, use 0-2 sec range without BPM
Be-ing May 11, 2017
c710ac8
add sync button to echo effect
Be-ing May 14, 2017
1c9c5a1
round synced echo time to 1/4 beats
Be-ing May 16, 2017
963c384
make roundToFraction round to nearest instead of clamp down
Be-ing May 28, 2017
4460823
echo effect: allow 1/8 beats as minimum delay time
Be-ing May 28, 2017
d75817b
echo effect: don't snap to 1/4 seconds without beat syncing
Be-ing May 28, 2017
d37ee1f
echo effect: restore feedback parameter range to 0 - 1
Be-ing May 29, 2017
8339962
increase size of Echo effect buffers
Be-ing Jul 4, 2017
b6e6291
Echo effect: fix artifact when feedback is 1
Be-ing Jul 16, 2017
9bcd2de
Echo: ramp send parameter
Be-ing Jul 31, 2017
196e6c2
Echo: move state maintenance code to end of process function
Be-ing Jul 31, 2017
e6ba450
Echo: add Triplet parameter
Be-ing Sep 11, 2017
2041c66
Echo: remove SYNC parameter
Be-ing Sep 11, 2017
23315bb
Echo: update Time parameter description
Be-ing Sep 14, 2017
d983c8a
Merge remote-tracking branch 'upstream/master' into tempo_sync_echo
Be-ing Sep 16, 2017
4f96174
Echo: fix Time depending on sample rate
Be-ing Sep 16, 2017
8f44d79
Echo: fix scaling of Time parameter
Be-ing Sep 16, 2017
7b05fbc
Echo: add Quantize parameter, change behavior of Triplet parameter
Be-ing Sep 18, 2017
4d5a61a
Merge remote-tracking branch 'upstream/master' into tempo_sync_echo
Be-ing Sep 18, 2017
43ad894
Echo: code cleanup
Be-ing Sep 22, 2017
15334f4
Echo: correct parameter descriptions
Be-ing Sep 22, 2017
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
106 changes: 78 additions & 28 deletions src/effects/native/echoeffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <QtDebug>

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

#define INCREMENT_RING(index, increment, length) index = (index + increment) % length

Expand All @@ -26,32 +27,34 @@ EffectManifest EchoEffect::getManifest() {

EffectManifestParameter* delay = manifest.addParameter();
delay->setId("delay_time");
delay->setName(QObject::tr("Delay"));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Delay is IMHO the correct name for this parameter and should be kept. Time is only the physical quantity, without further meaning.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think "Delay" is confusing because that often refers to a more complex effect:

The term “echo” was used more often in the early days, and is sometimes used today to refer to the distinct and distant repeats of a signal, while “delay” refers to anything from the same, to the short repeats heard as reverb, to the complex, long, manipulated repeats of an intricate digital delay line.

http://www.gibson.com/News-Lifestyle/Features/en-us/effects-explained-echo-delay.aspx

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OK.In this case "Time" is OK.

delay->setDescription(QObject::tr("Delay time (seconds)"));
delay->setName(QObject::tr("Time"));
delay->setDescription(QObject::tr("Delay time\n"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"Delay time", is redundant, because a delay is always a time.

"1/8 - 2 beats if tempo is detected (decks and samplers) \n"
"1/8 - 2 seconds if no tempo is detected (mic & aux inputs, master mix)"));
delay->setControlHint(EffectManifestParameter::ControlHint::KNOB_LINEAR);
delay->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN);
delay->setUnitsHint(EffectManifestParameter::UnitsHint::TIME);
delay->setMinimum(0.1);
delay->setDefault(1.0);
delay->setMaximum(EchoGroupState::kMaxDelaySeconds);
delay->setUnitsHint(EffectManifestParameter::UnitsHint::BEATS);
delay->setMinimum(0.0);
delay->setDefault(0.5);
delay->setMaximum(2.0);

EffectManifestParameter* feedback = manifest.addParameter();
feedback->setId("feedback_amount");
feedback->setName(QObject::tr("Feedback"));
feedback->setDescription(
QObject::tr("Amount the echo fades each time it loops"));
feedback->setControlHint(EffectManifestParameter::ControlHint::KNOB_LOGARITHMIC);
feedback->setControlHint(EffectManifestParameter::ControlHint::KNOB_LINEAR);
feedback->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN);
feedback->setUnitsHint(EffectManifestParameter::UnitsHint::UNKNOWN);
feedback->setMinimum(0.00);
feedback->setDefault(0.5);
feedback->setMaximum(1.0);
feedback->setDefault(0.75);
feedback->setMaximum(1.00);

EffectManifestParameter* pingpong = manifest.addParameter();
pingpong->setId("pingpong_amount");
pingpong->setName(QObject::tr("PingPong"));
pingpong->setName(QObject::tr("Ping Pong"));
pingpong->setDescription(
QObject::tr("As the ping-pong amount increases, increasing amounts "
QObject::tr("As the ping pong amount increases, increasing amounts "
"of the echoed signal is bounced between the left and "
"right speakers."));
pingpong->setControlHint(EffectManifestParameter::ControlHint::KNOB_LINEAR);
Expand All @@ -74,14 +77,39 @@ EffectManifest EchoEffect::getManifest() {
send->setDefault(1.0);
send->setMaximum(1.0);

EffectManifestParameter* quantize = manifest.addParameter();
quantize->setId("quantize");
quantize->setName("Quantize");
quantize->setShortName("Quantize");
quantize->setDescription("Round the Time parameter to the nearest 1/4 beat.");
quantize->setControlHint(EffectManifestParameter::ControlHint::TOGGLE_STEPPING);
quantize->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN);
quantize->setUnitsHint(EffectManifestParameter::UnitsHint::UNKNOWN);
quantize->setDefault(1);
quantize->setMinimum(0);
quantize->setMaximum(1);

EffectManifestParameter* triplet = manifest.addParameter();
triplet->setId("triplet");
triplet->setName("Triplets");
triplet->setDescription("When the Quantize parameter is enabled, divide rounded 1/4 beats of Time parameter by 3.");
triplet->setControlHint(EffectManifestParameter::ControlHint::TOGGLE_STEPPING);
triplet->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN);
triplet->setUnitsHint(EffectManifestParameter::UnitsHint::UNKNOWN);
triplet->setDefault(0);
triplet->setMinimum(0);
triplet->setMaximum(1);

return manifest;
}

EchoEffect::EchoEffect(EngineEffect* pEffect, const EffectManifest& manifest)
: m_pDelayParameter(pEffect->getParameterById("delay_time")),
m_pSendParameter(pEffect->getParameterById("send_amount")),
m_pFeedbackParameter(pEffect->getParameterById("feedback_amount")),
m_pPingPongParameter(pEffect->getParameterById("pingpong_amount")) {
m_pPingPongParameter(pEffect->getParameterById("pingpong_amount")),
m_pQuantizeParameter(pEffect->getParameterById("quantize")),
m_pTripletParameter(pEffect->getParameterById("triplet")) {
Q_UNUSED(manifest);
}

Expand All @@ -95,47 +123,65 @@ void EchoEffect::processChannel(const ChannelHandle& handle, EchoGroupState* pGr
const EffectProcessor::EnableState enableState,
const GroupFeatureState& groupFeatures) {
Q_UNUSED(handle);
Q_UNUSED(groupFeatures);

DEBUG_ASSERT(0 == (numSamples % EchoGroupState::kChannelCount));
EchoGroupState& gs = *pGroupState;
double delay_time = m_pDelayParameter->value();
// The minimum of the parameter is zero so the exact center of the knob is 1 beat.
double period = m_pDelayParameter->value();
double send_amount = m_pSendParameter->value();
double feedback_amount = m_pFeedbackParameter->value();
double pingpong_frac = m_pPingPongParameter->value();

int delay_samples = EchoGroupState::kChannelCount * delay_time * sampleRate;
int delay_samples;
if (groupFeatures.has_beat_length_sec) {
// period is a number of beats
if (m_pQuantizeParameter->toBool()) {
period = std::max(roundToFraction(period, 4), 1/8.0);
if (m_pTripletParameter->toBool()) {
period /= 3.0;
}
} else if (period < 1/8.0) {
period = 1/8.0;
}
delay_samples = period * groupFeatures.beat_length_sec
* sampleRate * EchoGroupState::kChannelCount;
} else {
// period is a number of seconds
period = std::max(period, 1/8.0);
delay_samples = period * sampleRate * EchoGroupState::kChannelCount;
}
VERIFY_OR_DEBUG_ASSERT(delay_samples > 0) {
delay_samples = 1;
}
VERIFY_OR_DEBUG_ASSERT(delay_samples <= gs.delay_buf.size()) {
delay_samples = gs.delay_buf.size();
}

if (delay_time < gs.prev_delay_time) {
if (period < gs.prev_period) {
// If the delay time has shrunk, we may need to wrap the write position.
gs.write_position = gs.write_position % delay_samples;
} else if (delay_time > gs.prev_delay_time) {
} else if (period > gs.prev_period) {
// If the delay time has grown, we need to zero out the new portion
// of the buffer we are using.
SampleUtil::applyGain(gs.delay_buf.data(gs.prev_delay_samples), 0,
gs.delay_buf.size() - gs.prev_delay_samples);
}

int read_position = gs.write_position;
gs.prev_delay_time = delay_time;
gs.prev_delay_samples = delay_samples;

// Feedback the delay buffer and then add the new input.
const CSAMPLE_GAIN send_delta = (send_amount - gs.prev_send) /
(numSamples / EchoGroupState::kChannelCount);
const CSAMPLE_GAIN send_start = send_amount + send_delta;
for (unsigned int i = 0; i < numSamples; i += EchoGroupState::kChannelCount) {
// Ramp the beginning and end of the delay buffer to prevent clicks.
double write_ramper = 1.0;
if (gs.write_position < EchoGroupState::kRampLength) {
write_ramper = static_cast<double>(gs.write_position) / EchoGroupState::kRampLength;
} else if (gs.write_position > delay_samples - EchoGroupState::kRampLength) {
write_ramper = static_cast<double>(delay_samples - gs.write_position)
/ EchoGroupState::kRampLength;
CSAMPLE_GAIN send_ramped = send_start;
if (send_delta > 0.0) {
send_ramped += send_delta * i / EchoGroupState::kChannelCount;
}
gs.delay_buf[gs.write_position] *= feedback_amount;
gs.delay_buf[gs.write_position + 1] *= feedback_amount;
gs.delay_buf[gs.write_position] += pInput[i] * send_amount * write_ramper;
gs.delay_buf[gs.write_position + 1] += pInput[i + 1] * send_amount * write_ramper;
gs.delay_buf[gs.write_position] += pInput[i] * send_ramped;
gs.delay_buf[gs.write_position + 1] += pInput[i + 1] * send_ramped;
// Actual delays distort and saturate, so clamp the buffer here.
gs.delay_buf[gs.write_position] =
SampleUtil::clampSample(gs.delay_buf[gs.write_position]);
Expand Down Expand Up @@ -179,4 +225,8 @@ void EchoEffect::processChannel(const ChannelHandle& handle, EchoGroupState* pGr
if (enableState == EffectProcessor::DISABLING) {
gs.delay_buf.clear();
}

gs.prev_period = period;
gs.prev_send = send_amount;
gs.prev_delay_samples = delay_samples;
}
13 changes: 9 additions & 4 deletions src/effects/native/echoeffect.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
#include "util/types.h"

struct EchoGroupState {
// 2 seconds max.
static constexpr int kMaxDelaySeconds = 2;
// 3 seconds max. This supports the full range of 2 beats for tempos down to
// 40 BPM.
static constexpr int kMaxDelaySeconds = 3;
// TODO(XXX): When we move from stereo to multi-channel this needs updating.
static constexpr int kChannelCount = mixxx::AudioSignal::kChannelCountStereo;
// Ramp length in samples when we are at the start of an echo.
Expand All @@ -26,14 +27,16 @@ struct EchoGroupState {
: delay_buf(mixxx::AudioSignal::kSamplingRateMax * kMaxDelaySeconds *
kChannelCount) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

With kMaxDelaySeconds as 2, this will not be big enough when the Time parameter is at its maximum and the BPM is below 60. Should kMaxDelaySeconds be increased? Is that edge case worth the higher memory consumption? These buffers consume a lot of memory, especially with #1254.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll increase it now that I've come up with a solution for the ridiculous memory usage in #1254.

delay_buf.clear();
prev_delay_time = 0.0;
prev_period = 0.0;
prev_send = 0.0;
prev_delay_samples = 0;
write_position = 0;
ping_pong_left = true;
}

SampleBuffer delay_buf;
double prev_delay_time;
double prev_period;
CSAMPLE_GAIN prev_send;
int prev_delay_samples;
int write_position;
bool ping_pong_left;
Expand Down Expand Up @@ -65,6 +68,8 @@ class EchoEffect : public PerChannelEffectProcessor<EchoGroupState> {
EngineEffectParameter* m_pSendParameter;
EngineEffectParameter* m_pFeedbackParameter;
EngineEffectParameter* m_pPingPongParameter;
EngineEffectParameter* m_pQuantizeParameter;
EngineEffectParameter* m_pTripletParameter;

DISALLOW_COPY_AND_ASSIGN(EchoEffect);
};
Expand Down
7 changes: 7 additions & 0 deletions src/util/math.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ inline int roundUpToPowerOf2(int v) {
return power;
}

inline double roundToFraction(double value, int denominator) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we should round to the nearest value, otherwise all values have a "snaping region", but not the topmost value,
2 in this case, is only adopted on the very maximum.

int wholePart = value;
double fractionPart = value - wholePart;
int numerator = std::lround(fractionPart * denominator);
return wholePart + (double) numerator / (double) denominator;
}

template <typename T>
inline const T ratio2db(const T a) {
return log10(a) * 20;
Expand Down