-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
sync echo effect to BPM #1256
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
sync echo effect to BPM #1256
Changes from all commits
bfaf108
b6e3eeb
032f7b6
c710ac8
1c9c5a1
963c384
4460823
d75817b
d37ee1f
8339962
b6e6291
9bcd2de
196e6c2
e6ba450
2041c66
23315bb
d983c8a
4f96174
8f44d79
7b05fbc
4d5a61a
43ad894
15334f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| #include <QtDebug> | ||
|
|
||
| #include "util/sample.h" | ||
| #include "util/math.h" | ||
|
|
||
| #define INCREMENT_RING(index, increment, length) index = (index + increment) % length | ||
|
|
||
|
|
@@ -26,32 +27,34 @@ EffectManifest EchoEffect::getManifest() { | |
|
|
||
| EffectManifestParameter* delay = manifest.addParameter(); | ||
| delay->setId("delay_time"); | ||
| delay->setName(QObject::tr("Delay")); | ||
| delay->setDescription(QObject::tr("Delay time (seconds)")); | ||
| delay->setName(QObject::tr("Time")); | ||
| delay->setDescription(QObject::tr("Delay time\n" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
|
@@ -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); | ||
| } | ||
|
|
||
|
|
@@ -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]); | ||
|
|
@@ -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; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
|
@@ -26,14 +27,16 @@ struct EchoGroupState { | |
| : delay_buf(mixxx::AudioSignal::kSamplingRateMax * kMaxDelaySeconds * | ||
| kChannelCount) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
|
@@ -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); | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,13 @@ inline int roundUpToPowerOf2(int v) { | |
| return power; | ||
| } | ||
|
|
||
| inline double roundToFraction(double value, int denominator) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, |
||
| 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; | ||
|
|
||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
http://www.gibson.com/News-Lifestyle/Features/en-us/effects-explained-echo-delay.aspx
There was a problem hiding this comment.
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.