mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-26 18:03:33 -04:00
279 lines
9.1 KiB
C++
Executable File
279 lines
9.1 KiB
C++
Executable File
/*
|
|
* FrequencyShifter.cpp
|
|
*
|
|
* Copyright (c) 2025 Lost Robot <r94231/at/gmail/dot/com>
|
|
*
|
|
* This file is part of LMMS - https://lmms.io
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public
|
|
* License along with this program (see COPYING); if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#include "FrequencyShifterEffect.h"
|
|
|
|
#include "embed.h"
|
|
#include "plugin_export.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <numbers>
|
|
|
|
namespace lmms
|
|
{
|
|
|
|
extern "C"
|
|
{
|
|
Plugin::Descriptor PLUGIN_EXPORT frequencyshifter_plugin_descriptor =
|
|
{
|
|
LMMS_STRINGIFY(PLUGIN_NAME),
|
|
"Frequency Shifter",
|
|
QT_TRANSLATE_NOOP("PluginBrowser", "A frequency shifter (not a pitch shifter) and barberpole phaser plugin"),
|
|
"Lost Robot <r94231/at/gmail/dot/com>",
|
|
0x0100,
|
|
Plugin::Type::Effect,
|
|
new PixmapLoader("lmms-plugin-logo"),
|
|
nullptr,
|
|
nullptr,
|
|
};
|
|
PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* parent, void* data)
|
|
{
|
|
return new FrequencyShifterEffect(parent, static_cast<const Plugin::Descriptor::SubPluginFeatures::Key*>(data));
|
|
}
|
|
}// extern "C"
|
|
|
|
FrequencyShifterEffect::FrequencyShifterEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key) :
|
|
Effect(&frequencyshifter_plugin_descriptor, parent, key),
|
|
m_controls(this)
|
|
{
|
|
connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged,
|
|
this, &FrequencyShifterEffect::updateSampleRate);
|
|
updateSampleRate();
|
|
}
|
|
|
|
Effect::ProcessStatus FrequencyShifterEffect::processImpl(SampleFrame* buf, const fpp_t frames)
|
|
{
|
|
constexpr float twoPi = std::numbers::pi_v<float> * 2.0f;
|
|
|
|
const float mix = m_controls.m_mix.value() * wetLevel();
|
|
const float fs = m_controls.m_freqShift.value();
|
|
const float spread = m_controls.m_spreadShift.value();
|
|
const float ring = m_controls.m_ring.value();
|
|
const float feedback = m_controls.m_feedback.value();
|
|
const float delayLen = (m_controls.m_delayLengthLong.value() + m_controls.m_delayLengthShort.value()) * 0.001f * m_sampleRate;
|
|
const float delayDamp = m_controls.m_delayDamp.value();
|
|
const float delayGlide = m_controls.m_delayGlide.value();
|
|
const float lfoAmt = m_controls.m_lfoAmount.value();
|
|
const float lfoRate = (m_controls.m_lfoRate.value() / m_sampleRate) * twoPi;
|
|
const float lfoSt = m_controls.m_lfoStereoPhase.value() * twoPi;
|
|
const bool antireflect = m_controls.m_antireflect.value();
|
|
const int routeMode = m_controls.m_routeMode.value();
|
|
const float harmonics = m_controls.m_harmonics.value();
|
|
const float glide = m_controls.m_glide.value();
|
|
const float tone = m_controls.m_tone.value();
|
|
const float phase = m_controls.m_phase.value() * twoPi;
|
|
|
|
const bool resetShifterBtn = m_controls.m_resetShifter.value();
|
|
const bool resetLfoBtn = m_controls.m_resetLfo.value();
|
|
|
|
if (!m_prevResetShifter && resetShifterBtn)
|
|
{
|
|
m_phase[0] = 0.f;
|
|
m_phase[1] = 0.f;
|
|
}
|
|
if (!m_prevResetLfo && resetLfoBtn) { m_lfoPhase = 0.f; }
|
|
m_prevResetShifter = resetShifterBtn;
|
|
m_prevResetLfo = resetLfoBtn;
|
|
|
|
const float invRing = 1.f - ring;
|
|
const bool parallelFB = (routeMode >= 1);
|
|
const bool routeAdd = (routeMode == 1);
|
|
|
|
const bool doHarm = (harmonics > 0.f);
|
|
const float harmFactor = harmonics * 20.f + 1.f;
|
|
const float harmDiv = 1.f / (harmonics * 0.3f + 1.f);
|
|
|
|
const float dampCoeff = std::exp(-m_twoPiOverSr * delayDamp);
|
|
const float toneCoeff = std::exp(-m_twoPiOverSr * tone);
|
|
const float delayGlideCoeff = delayGlide ? std::exp(-1.f / (delayGlide * m_sampleRate)) : 0.f;
|
|
const float glideCoeff = glide ? std::exp(-1.f / (glide * m_sampleRate)) : 0.f;
|
|
|
|
// we only bother with wrapping phases once per buffer
|
|
m_lfoPhase = std::fmod(m_lfoPhase, twoPi);
|
|
for (int ch = 0; ch < 2; ++ch)
|
|
{
|
|
m_phase[ch] = std::fmod(m_phase[ch], twoPi);
|
|
}
|
|
|
|
for (size_t i = 0; i < frames; ++i)
|
|
{
|
|
float lfo0;
|
|
float lfo1;
|
|
if (lfoAmt > 0.f)
|
|
{
|
|
lfo0 = std::sin(m_lfoPhase) * lfoAmt;
|
|
lfo1 = std::sin(m_lfoPhase + lfoSt) * lfoAmt;
|
|
}
|
|
else
|
|
{
|
|
lfo0 = 0.f;
|
|
lfo1 = 0.f;
|
|
}
|
|
m_lfoPhase += lfoRate;
|
|
|
|
// parameter interpolation (glide)
|
|
const float base0 = fs - spread;
|
|
const float base1 = fs + spread;
|
|
m_trueShift[0] = (1.f - glideCoeff) * base0 + glideCoeff * m_trueShift[0];
|
|
m_trueShift[1] = (1.f - glideCoeff) * base1 + glideCoeff * m_trueShift[1];
|
|
m_trueDelay = std::max((1.f - delayGlideCoeff) * delayLen + delayGlideCoeff * m_trueDelay, 1.f);
|
|
m_truePhase = (1.f - glideCoeff) * phase + glideCoeff * m_truePhase;
|
|
|
|
// delay line with 4-point hermite interpolation
|
|
float readIndex = static_cast<float>(m_writeIndex) - m_trueDelay;
|
|
if (readIndex < 0.f) { readIndex += static_cast<float>(m_ringBufSize); }
|
|
const int indexFloor = static_cast<int>(readIndex);
|
|
const float frac = readIndex - static_cast<float>(indexFloor);
|
|
const std::array<float, 2> dly = getHermiteSample(indexFloor, frac);
|
|
if (++m_writeIndex == m_ringBufSize) { m_writeIndex = 0; }
|
|
|
|
// routing stuff
|
|
const float inL = buf[i][0];
|
|
const float inR = buf[i][1];
|
|
const float fxInL = parallelFB ? (dly[0] * feedback) : (inL + dly[0] * feedback);
|
|
const float fxInR = parallelFB ? (dly[1] * feedback) : (inR + dly[1] * feedback);
|
|
|
|
// delta phase
|
|
const float dPh0 = (m_trueShift[0] + lfo0) * m_twoPiOverSr;
|
|
const float dPh1 = (m_trueShift[1] + lfo1) * m_twoPiOverSr;
|
|
|
|
float outL;
|
|
float outR;
|
|
|
|
{
|
|
float fxIn[2] = {fxInL, fxInR};
|
|
float dPh[2] = {dPh0, dPh1};
|
|
float out[2];
|
|
|
|
for (int ch = 0; ch < 2; ++ch)
|
|
{
|
|
const float phaseValue = m_phase[ch] + m_truePhase;
|
|
|
|
float sinP = std::sin(phaseValue);
|
|
float cosP = std::cos(phaseValue);
|
|
|
|
if (doHarm)
|
|
{
|
|
// arbitrary distortion function, crossfaded with original signal
|
|
const float xc = std::clamp(harmFactor * cosP, -3.f, 3.f);
|
|
const float xs = std::clamp(harmFactor * sinP, -3.f, 3.f);
|
|
const float xc2 = xc * xc;
|
|
const float xs2 = xs * xs;
|
|
const float tc = xc * (27.f + xc2) / (27.f + 9.f * xc2);
|
|
const float ts = xs * (27.f + xs2) / (27.f + 9.f * xs2);
|
|
cosP = std::lerp(cosP, tc * harmDiv, harmonics);
|
|
sinP = std::lerp(sinP, ts * harmDiv, harmonics);
|
|
}
|
|
|
|
float analytic1[2];
|
|
m_hilbert1.processReal(fxIn[ch], ch, analytic1);
|
|
|
|
// ring modulation frequency shifts both downward and upward simultaneously
|
|
// oscI alongside the hilbert-transformed signal cancels out one of those two sidebands
|
|
// so fading it out will bring us closer to ring modulation
|
|
const float oscR = cosP;
|
|
const float oscI = sinP * invRing;
|
|
|
|
const float modR = analytic1[0] * oscR - analytic1[1] * oscI;
|
|
const float modI = analytic1[0] * oscI + analytic1[1] * oscR;
|
|
|
|
float shiftedR;
|
|
|
|
if (antireflect)
|
|
{
|
|
// use a second hilbert transform on the complex signal
|
|
// in order to remove negative frequencies to
|
|
// prevent aliasing through 0 Hz and Nyquist
|
|
float mod[2] = {modR, modI};
|
|
float analytic2[2];
|
|
m_hilbert2.processComplex(mod, ch, analytic2);
|
|
shiftedR = analytic2[0] * 0.5f;
|
|
}
|
|
else
|
|
{
|
|
shiftedR = modR;
|
|
}
|
|
|
|
m_phase[ch] += dPh[ch];
|
|
out[ch] = shiftedR;
|
|
}
|
|
|
|
outL = out[0];
|
|
outR = out[1];
|
|
}
|
|
|
|
float delayInL = outL + (parallelFB ? inL : 0.f);
|
|
float delayInR = outR + (parallelFB ? inR : 0.f);
|
|
|
|
// saturate feedback loop to ensure it doesn't explode
|
|
constexpr float FbSaturation = 16.f;
|
|
delayInL = (FbSaturation * delayInL) / (FbSaturation + std::fabs(delayInL));
|
|
delayInR = (FbSaturation * delayInR) / (FbSaturation + std::fabs(delayInR));
|
|
|
|
// 1-pole lowpass in feedback loop
|
|
m_dampState[0] = (1.f - dampCoeff) * delayInL + dampCoeff * m_dampState[0];
|
|
m_dampState[1] = (1.f - dampCoeff) * delayInR + dampCoeff * m_dampState[1];
|
|
m_ringBuf[m_writeIndex][0] = m_dampState[0];
|
|
m_ringBuf[m_writeIndex][1] = m_dampState[1];
|
|
|
|
// 1-pole lowpass on entire signal
|
|
m_toneState[0] = (1.f - toneCoeff) * outL + toneCoeff * m_toneState[0];
|
|
m_toneState[1] = (1.f - toneCoeff) * outR + toneCoeff * m_toneState[1];
|
|
outL = m_toneState[0];
|
|
outR = m_toneState[1];
|
|
|
|
if (routeAdd)
|
|
{
|
|
buf[i][0] = inL + mix * outL;
|
|
buf[i][1] = inR + mix * outR;
|
|
}
|
|
else
|
|
{
|
|
const float dry = 1.f - mix;
|
|
buf[i][0] = dry * inL + mix * outL;
|
|
buf[i][1] = dry * inR + mix * outR;
|
|
}
|
|
}
|
|
|
|
return ProcessStatus::ContinueIfNotQuiet;
|
|
}
|
|
|
|
void FrequencyShifterEffect::updateSampleRate()
|
|
{
|
|
m_sampleRate = Engine::audioEngine()->outputSampleRate();
|
|
|
|
constexpr float twoPi = std::numbers::pi_v<float> * 2.0f;
|
|
m_twoPiOverSr = twoPi / m_sampleRate;
|
|
|
|
m_hilbert1 = HilbertIIRFloat<2>(m_sampleRate, 2.0f);
|
|
m_hilbert2 = HilbertIIRFloat<2>(m_sampleRate, 2.0f);
|
|
|
|
// +6 provides space for interpolation
|
|
m_ringBufSize = (m_controls.m_delayLengthLong.maxValue() + m_controls.m_delayLengthShort.maxValue()) * 0.001f * m_sampleRate + 6.f;
|
|
m_ringBuf.resize(m_ringBufSize);
|
|
}
|
|
|
|
} // namespace lmms
|
|
|