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

Fix quantization in the effect engine (metronome effect) #13636

Merged
merged 19 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
27baedd
Skip copy if source is equal the destination the copy is unnecessary …
daschuer Sep 1, 2024
77cdbac
Rename beat_fraction to beat_fraction_buffer_end to clarify that the …
daschuer Sep 1, 2024
92ffa72
Avoid unnecessary seconds round-trip in GroupFeatureState
daschuer Sep 2, 2024
22b7c29
Remove unused include
daschuer Sep 3, 2024
808c09c
GroupFeatureState: std::optional
daschuer Sep 3, 2024
c0cbd6a
Add scratch_rate to GroupFeatureState to allow to quantize effects du…
daschuer Sep 3, 2024
8454cce
Fix quantization with the metronome effect. It was one buffer off.
daschuer Sep 3, 2024
6c67cd8
Introduce m_actual_speed with is what the scalers actually did.
daschuer Sep 6, 2024
885e9ef
Don't snap "beat_next" and "beat_prev" to the nearest beat. This was …
daschuer Sep 6, 2024
8ea3457
Fix the metronome effect during scratching using scratch_rate.
daschuer Sep 6, 2024
c3f084d
Don't use std::optional::value introduced in macOS 10.13
daschuer Sep 9, 2024
e5ff7f3
default GroupFeatureState ctor
daschuer Sep 9, 2024
1fecf39
Improve readability in meronomeeffect.cpp
daschuer Sep 9, 2024
11af751
Fix use of GroupFeatureState in LoudnessContourEffect
daschuer Sep 9, 2024
9293b51
refactor: move metronome sampledata into separate translation unit
Swiftb0y Sep 11, 2024
5e74ec6
Fix metronome without a sync enabled
daschuer Sep 12, 2024
26d3029
Avoid to fade in the metronome effect, because this would mangle the …
daschuer Sep 13, 2024
2081952
Combine frames and scratch_rate into one optional because they are al…
daschuer Sep 13, 2024
f019874
refactor: use higher-level `std::span` based logic
Swiftb0y Sep 11, 2024
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
4 changes: 2 additions & 2 deletions src/effects/backends/builtin/autopaneffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ void AutoPanEffect::processChannel(
double period = m_pPeriodParameter->value();
const auto smoothing = static_cast<float>(0.5 - m_pSmoothingParameter->value());

if (groupFeatures.has_beat_length_sec) {
if (groupFeatures.beat_length_frames.has_value()) {
// period is a number of beats
double beats = std::max(roundToFraction(period, 2), 0.25);
period = beats * groupFeatures.beat_length_sec * engineParameters.sampleRate();
period = beats * groupFeatures.beat_length_frames.value();

// TODO(xxx) sync phase
//if (groupFeatures.has_beat_fraction) {
Expand Down
2 changes: 1 addition & 1 deletion src/effects/backends/builtin/biquadfullkilleqeffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ void BiquadFullKillEQEffect::processChannel(
pState->m_highKill->pauseFilter();
}

if (activeFilters == 0) {
if (activeFilters == 0 && pOutput != pInput) {
SampleUtil::copy(pOutput, pInput, engineParameters.samplesPerBuffer());
}

Expand Down
8 changes: 6 additions & 2 deletions src/effects/backends/builtin/distortioneffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ void DistortionEffect::processChannel(
CSAMPLE driveParam = static_cast<CSAMPLE>(m_pDrive->value());

if (driveParam < 0.01) {
SampleUtil::copy(pOutput, pInput, numSamples);
if (pOutput != pInput) {
SampleUtil::copy(pOutput, pInput, numSamples);
}
return;
}

Expand All @@ -122,7 +124,9 @@ void DistortionEffect::processChannel(

default:
// We should never enter here, but we act as a noop effect just in case.
SampleUtil::copy(pOutput, pInput, numSamples);
if (pOutput != pInput) {
SampleUtil::copy(pOutput, pInput, numSamples);
}
return;
}
}
5 changes: 2 additions & 3 deletions src/effects/backends/builtin/echoeffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ void EchoEffect::processChannel(
const auto pingpong_frac = static_cast<CSAMPLE_GAIN>(m_pPingPongParameter->value());

int delay_frames;
if (groupFeatures.has_beat_length_sec) {
if (groupFeatures.beat_length_frames.has_value()) {
// period is a number of beats
if (m_pQuantizeParameter->toBool()) {
period = std::max(roundToFraction(period, 4), 1 / 8.0);
Expand All @@ -142,8 +142,7 @@ void EchoEffect::processChannel(
} else if (period < 1 / 8.0) {
period = 1 / 8.0;
}
delay_frames = static_cast<int>(period * groupFeatures.beat_length_sec *
engineParameters.sampleRate());
delay_frames = static_cast<int>(period * groupFeatures.beat_length_frames.value());
} else {
// period is a number of seconds
period = std::max(period, 1 / 8.0);
Expand Down
5 changes: 2 additions & 3 deletions src/effects/backends/builtin/flangereffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,13 @@ void FlangerEffect::processChannel(
const GroupFeatureState& groupFeatures) {
double lfoPeriodParameter = m_pSpeedParameter->value();
double lfoPeriodFrames;
if (groupFeatures.has_beat_length_sec) {
if (groupFeatures.beat_length_frames.has_value()) {
// lfoPeriodParameter is a number of beats
lfoPeriodParameter = std::max(roundToFraction(lfoPeriodParameter, 2.0), kMinLfoBeats);
if (m_pTripletParameter->toBool()) {
lfoPeriodParameter /= 3.0;
}
lfoPeriodFrames = lfoPeriodParameter * groupFeatures.beat_length_sec *
engineParameters.sampleRate();
lfoPeriodFrames = lfoPeriodParameter * groupFeatures.beat_length_frames.value();
} else {
// lfoPeriodParameter is a number of seconds
lfoPeriodFrames = std::max(lfoPeriodParameter, kMinLfoBeats) *
Expand Down
8 changes: 5 additions & 3 deletions src/effects/backends/builtin/loudnesscontoureffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ void LoudnessContourEffect::processChannel(
auto gain = static_cast<CSAMPLE_GAIN>(pState->m_oldGain);

if (enableState != EffectEnableState::Disabling) {
bool useGain = m_pUseGain->toBool() && groupFeatures.has_gain;
bool useGain = m_pUseGain->toBool() && groupFeatures.gain.has_value();
double loudness = m_pLoudness->value();
double gainKnob = groupFeatures.gain;
double gainKnob = groupFeatures.gain.value();
Swiftb0y marked this conversation as resolved.
Show resolved Hide resolved

filterGainDb = loudness;

Expand Down Expand Up @@ -141,7 +141,9 @@ void LoudnessContourEffect::processChannel(
if (filterGainDb == 0) {
pState->m_low->pauseFilter();
pState->m_high->pauseFilter();
SampleUtil::copy(pOutput, pInput, engineParameters.samplesPerBuffer());
if (pOutput != pInput) {
SampleUtil::copy(pOutput, pInput, engineParameters.samplesPerBuffer());
}
} else {
pState->m_low->process(pInput, pOutput, engineParameters.samplesPerBuffer());
pState->m_high->process(pOutput, pState->m_pBuf, engineParameters.samplesPerBuffer());
Expand Down
99 changes: 62 additions & 37 deletions src/effects/backends/builtin/metronomeeffect.cpp
Copy link
Member

Choose a reason for hiding this comment

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

the changes here are really hard to follow because they mix offset and timing calculations with buffer indexing (mixing levels of abstraction) and thus I'm neither confident about the reliability nor safety of the code...

Do you think its feasible to reorganize it a little so its easier to understand?

Copy link
Member Author

Choose a reason for hiding this comment

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

I have tried to fix that by calculate everything in frames. Maybe I was not too successful or comments are missing.
Can you give some hints what can be improved?

Copy link
Member

Choose a reason for hiding this comment

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

I will rework it a little and open a PR to your branch ASAP.

Copy link
Member

Choose a reason for hiding this comment

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

Please take a look daschuer#97

Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ void MetronomeEffect::processChannel(
const GroupFeatureState& groupFeatures) {
Q_UNUSED(pInput);

Swiftb0y marked this conversation as resolved.
Show resolved Hide resolved
MetronomeGroupState* gs = pGroupState;

if (enableState == EffectEnableState::Disabled) {
gs->m_framesSinceClickStart = 0;
// assume click is fully played
return;
}

MetronomeGroupState* gs = pGroupState;

SINT clickSize = kClickSize44100;
const CSAMPLE* click = kClick44100;
if (engineParameters.sampleRate() >= 96000) {
Expand All @@ -76,52 +76,77 @@ void MetronomeEffect::processChannel(
click = kClick48000;
}

SINT maxFrames;
if (m_pSyncParameter->toBool() && groupFeatures.has_beat_length_sec) {
maxFrames = static_cast<SINT>(
engineParameters.sampleRate() * groupFeatures.beat_length_sec);
if (groupFeatures.has_beat_fraction) {
const auto currentFrame = static_cast<SINT>(
maxFrames * groupFeatures.beat_fraction);
if (maxFrames > clickSize &&
currentFrame > clickSize &&
currentFrame < maxFrames - clickSize &&
gs->m_framesSinceClickStart > clickSize) {
// plays a single click on low speed
gs->m_framesSinceClickStart = currentFrame;
}
}
} else {
maxFrames = static_cast<SINT>(
engineParameters.sampleRate() * 60 / m_pBpmParameter->value());
if (pOutput != pInput) {
SampleUtil::copy(pOutput, pInput, engineParameters.samplesPerBuffer());
}

SampleUtil::copy(pOutput, pInput, engineParameters.samplesPerBuffer());
if (enableState == EffectEnableState::Enabling) {
if (m_pSyncParameter->toBool() && groupFeatures.beat_fraction_buffer_end.has_value()) {
// Skip first click and sync phase
gs->m_framesSinceClickStart = clickSize;
} else {
// click right away after enabling
gs->m_framesSinceClickStart = 0;
}
}

if (gs->m_framesSinceClickStart < clickSize) {
// still in click region, write remaining click frames.
// In click region, write remaining click frames.
const SINT copyFrames =
math_min(engineParameters.framesPerBuffer(),
clickSize - gs->m_framesSinceClickStart);
SampleUtil::addMonoToStereo(pOutput, &click[gs->m_framesSinceClickStart], copyFrames);
}

gs->m_framesSinceClickStart += engineParameters.framesPerBuffer();
double bufferEnd = gs->m_framesSinceClickStart + engineParameters.framesPerBuffer();

double nextClickStart = bufferEnd; // default to "no new click";
if (m_pSyncParameter->toBool() && groupFeatures.beat_length_frames.has_value()) {
if (groupFeatures.beat_fraction_buffer_end.has_value()) {
double beatLength = groupFeatures.beat_length_frames.value();
if (groupFeatures.scratch_rate.has_value()) {
if (groupFeatures.scratch_rate.value() != 0.0) {
beatLength /= groupFeatures.scratch_rate.value();
} else {
beatLength = 0;
}
}

double beatToBufferEnd;
if (beatLength > 0) {
beatToBufferEnd =
beatLength *
groupFeatures.beat_fraction_buffer_end.value();
} else {
beatToBufferEnd =
beatLength * -1 *
(1 - groupFeatures.beat_fraction_buffer_end.value());
}

if (gs->m_framesSinceClickStart > maxFrames) {
// overflow, all overflowed frames are the start of a new click sound
gs->m_framesSinceClickStart -= maxFrames;
while (gs->m_framesSinceClickStart > engineParameters.framesPerBuffer()) {
// loop into a valid region, this happens if the maxFrames was lowered
gs->m_framesSinceClickStart -= engineParameters.framesPerBuffer();
if (bufferEnd > beatToBufferEnd) {
// We have a new beat before the current buffer ends
nextClickStart = bufferEnd - beatToBufferEnd;
}
} else {
nextClickStart = groupFeatures.beat_length_frames.value();
}
} else {
nextClickStart = engineParameters.sampleRate() * 60 / m_pBpmParameter->value();
}

// gs->m_framesSinceClickStart matches now already at the first frame
// of next pOutput.
const unsigned int outputOffset =
engineParameters.framesPerBuffer() - gs->m_framesSinceClickStart;
const unsigned int copyFrames =
math_min(gs->m_framesSinceClickStart, clickSize);
SampleUtil::addMonoToStereo(&pOutput[outputOffset * 2], click, copyFrames);
if (bufferEnd > nextClickStart) {
// We need to start a new click
SINT outputOffset = static_cast<SINT>(nextClickStart) - gs->m_framesSinceClickStart;
if (outputOffset > 0) {
if (outputOffset < engineParameters.framesPerBuffer()) {
Swiftb0y marked this conversation as resolved.
Show resolved Hide resolved
const SINT copyFrames =
math_min(engineParameters.framesPerBuffer() - outputOffset, clickSize);
SampleUtil::addMonoToStereo(&pOutput[outputOffset * 2], &click[0], copyFrames);
gs->m_framesSinceClickStart = -outputOffset;
}
}
}
if (gs->m_framesSinceClickStart < engineParameters.framesPerBuffer() + clickSize) {
gs->m_framesSinceClickStart += engineParameters.framesPerBuffer();
}
}
4 changes: 3 additions & 1 deletion src/effects/backends/builtin/parametriceqeffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,9 @@ void ParametricEQEffect::processChannel(
pState->m_bands[1]->process(pInput, pOutput, engineParameters.samplesPerBuffer());
} else {
pState->m_bands[1]->pauseFilter();
SampleUtil::copy(pOutput, pInput, engineParameters.samplesPerBuffer());
if (pOutput != pInput) {
SampleUtil::copy(pOutput, pInput, engineParameters.samplesPerBuffer());
}
}
}

Expand Down
5 changes: 2 additions & 3 deletions src/effects/backends/builtin/phasereffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,13 @@ void PhaserEffect::processChannel(

double periodParameter = m_pLFOPeriodParameter->value();
double periodSamples;
if (groupFeatures.has_beat_length_sec) {
if (groupFeatures.beat_length_frames.has_value()) {
// periodParameter is a number of beats
periodParameter = std::max(roundToFraction(periodParameter, 2.0), 1 / 4.0);
if (m_pTripletParameter->toBool()) {
periodParameter /= 3.0;
}
periodSamples = periodParameter * groupFeatures.beat_length_sec *
engineParameters.sampleRate();
periodSamples = periodParameter * groupFeatures.beat_length_frames.value();
} else {
// periodParameter is a number of seconds
periodSamples = std::max(periodParameter, 1 / 4.0) * engineParameters.sampleRate();
Expand Down
4 changes: 3 additions & 1 deletion src/effects/backends/builtin/threebandbiquadeqeffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,9 @@ void ThreeBandBiquadEQEffect::processChannel(
}

if (activeFilters == 0) {
SampleUtil::copy(pOutput, pInput, engineParameters.samplesPerBuffer());
if (pOutput != pInput) {
SampleUtil::copy(pOutput, pInput, engineParameters.samplesPerBuffer());
}
}

if (enableState == EffectEnableState::Disabling) {
Expand Down
11 changes: 5 additions & 6 deletions src/effects/backends/builtin/tremoloeffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ void TremoloEffect::processChannel(
bool tripletDisabling = pState->tripletEnabled && !m_pTripletParameter->toBool();

if (enableState == EffectEnableState::Enabling || quantizeEnabling || tripletDisabling) {
if (gf.has_beat_length_sec && gf.has_beat_fraction) {
currentFrame = static_cast<unsigned int>(gf.beat_fraction *
gf.beat_length_sec * engineParameters.sampleRate());
if (gf.beat_length_frames.has_value() && gf.beat_fraction_buffer_end.has_value()) {
currentFrame = static_cast<unsigned int>(gf.beat_fraction_buffer_end.value() *
gf.beat_length_frames.value());
} else {
currentFrame = 0;
}
Expand All @@ -149,7 +149,7 @@ void TremoloEffect::processChannel(

int framePerPeriod;
double rate = m_pRateParameter->value();
if (gf.has_beat_length_sec && gf.has_beat_fraction) {
if (gf.beat_length_frames.has_value() && gf.beat_fraction_buffer_end.has_value()) {
if (m_pQuantizeParameter->toBool()) {
const auto divider = static_cast<int>(log2(rate));
rate = pow(2, divider);
Expand All @@ -158,8 +158,7 @@ void TremoloEffect::processChannel(
rate *= 3.0;
}
}
const auto framePerBeat = static_cast<int>(
gf.beat_length_sec * engineParameters.sampleRate());
const auto framePerBeat = static_cast<int>(gf.beat_length_frames.value());
framePerPeriod = static_cast<int>(framePerBeat / rate);
} else {
framePerPeriod = static_cast<int>(engineParameters.sampleRate() / rate);
Expand Down
16 changes: 6 additions & 10 deletions src/engine/controls/bpmcontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1085,7 +1085,7 @@ void BpmControl::resetSyncAdjustment() {
m_resetSyncAdjustment = true;
}

void BpmControl::collectFeatures(GroupFeatureState* pGroupFeatures) const {
void BpmControl::collectFeatures(GroupFeatureState* pGroupFeatures, double speed) const {
// Without a beatgrid we don't know any beat details.
FrameInfo info = frameInfo();
if (!info.sampleRate.isValid() || !m_pBeats) {
Expand All @@ -1106,16 +1106,12 @@ void BpmControl::collectFeatures(GroupFeatureState* pGroupFeatures) const {
nextBeatPosition,
&beatLengthFrames,
&beatFraction)) {
const double framesPerSecond = info.sampleRate * m_pRateRatio->get();
if (framesPerSecond != 0.0) {
pGroupFeatures->beat_length_sec = beatLengthFrames / framesPerSecond;
pGroupFeatures->has_beat_length_sec = true;
} else {
pGroupFeatures->has_beat_length_sec = false;
const double rateRatio = m_pRateRatio->get();
if (rateRatio != 0.0) {
pGroupFeatures->beat_length_frames = beatLengthFrames / rateRatio;
pGroupFeatures->scratch_rate = speed / rateRatio;
}

pGroupFeatures->has_beat_fraction = true;
pGroupFeatures->beat_fraction = beatFraction;
pGroupFeatures->beat_fraction_buffer_end = beatFraction;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/engine/controls/bpmcontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class BpmControl : public EngineControl {
/// override is used for seeks.
double updateBeatDistance(mixxx::audio::FramePos playpos);

void collectFeatures(GroupFeatureState* pGroupFeatures) const;
void collectFeatures(GroupFeatureState* pGroupFeatures, double speed) const;

// Calculates contextual information about beats: the previous beat, the
// next beat, the current beat length, and the beat ratio (how far dPosition
Expand Down
4 changes: 1 addition & 3 deletions src/engine/controls/quantizecontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,6 @@ void QuantizeControl::playPosChanged(mixxx::audio::FramePos position) {
// We only need to update the prev or next if the current sample is
// out of range of the existing beat positions or if we've been forced to
// do so.
// NOTE: This bypasses the epsilon calculation, but is there a way
// that could actually cause a problem?
const auto prevBeatPosition =
mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid(
m_pCOPrevBeat->get());
Expand All @@ -88,7 +86,7 @@ void QuantizeControl::lookupBeatPositions(mixxx::audio::FramePos position) {
if (pBeats) {
mixxx::audio::FramePos prevBeatPosition;
mixxx::audio::FramePos nextBeatPosition;
pBeats->findPrevNextBeats(position, &prevBeatPosition, &nextBeatPosition, true);
pBeats->findPrevNextBeats(position, &prevBeatPosition, &nextBeatPosition, false);
// FIXME: -1.0 is a valid frame position, should we set the COs to NaN?
m_pCOPrevBeat->set(prevBeatPosition.toEngineSamplePosMaybeInvalid());
m_pCONextBeat->set(nextBeatPosition.toEngineSamplePosMaybeInvalid());
Expand Down
Loading
Loading