Files
lmms/plugins/GranularPitchShifter/GranularPitchShifterEffect.cpp
Fawn e328a136fc Use C++20 std::numbers, std::lerp() (#7696)
* use c++20 concepts and numbers for lmms_constants.h

* replace lmms::numbers::sqrt2 with std::numbers::sqrt2

* replace lmms::numbers::e with std::numbers::e
Also replace the only use of lmms::numbers::inv_e with a local constexpr instead

* remove lmms::numbers::pi_half and lmms::numbers::pi_sqr
They were only used in one or two places each

* replace lmms::numbers::pi with std::numbers::pi

* add #include <numbers> to every file touched so far
This is probably not needed for some of these files. I'll remove those later

* Remove lmms::numbers

Rest in peace lmms::numbers::tau, my beloved

* Add missing #include <numbers>

* replace stray use of F_EPSILON with approximatelyEqual()

* make many constants inline constexpr
A lot of the remaining constants in lmms_constants.h are specific to
SaProcessor. If they are only used there, shouldn't they be in SaProcessor.h?

* ok then, it's allowed to be signed

* remove #include "lmms_constants.h" for files that don't need it
- And also move F_EPSILON into lmms_math.h
- And also add an overload for fast_rand() to specify a higher and lower bound
- And a bunch of other nonsense

* ok then, it's allowed to be inferred

* ok then, it can accept an integral

* fix typo

* appease msvc

* appease msvc again

* Replace linearInterpolate with std::lerp()

As well as time travel to undo several foolish decisions and squash tiny
commits together

* Fix msvc constexpr warnings

* Fix msvc float to double truncation warning

* Apply two suggestions from code review

Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com>

* Apply suggestions from code review

Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com>

* fix silly mistake

* Remove SlicerT's dependence on lmms_math.h

* Allow more type inference on fastRand() and fastPow10f()

* Apply suggestions from code review

Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com>

* Clean up fastRand() a little bit more

---------

Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com>
2025-02-13 13:15:08 -05:00

293 lines
10 KiB
C++
Executable File

/*
* GranularPitchShifter.cpp
*
* Copyright (c) 2024 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 "GranularPitchShifterEffect.h"
#include <cmath>
#include "embed.h"
#include "lmms_math.h"
#include "plugin_export.h"
namespace lmms
{
extern "C"
{
Plugin::Descriptor PLUGIN_EXPORT granularpitchshifter_plugin_descriptor =
{
LMMS_STRINGIFY(PLUGIN_NAME),
"Granular Pitch Shifter",
QT_TRANSLATE_NOOP("PluginBrowser", "Granular pitch shifter"),
"Lost Robot <r94231/at/gmail/dot/com>",
0x0100,
Plugin::Type::Effect,
new PluginPixmapLoader("logo"),
nullptr,
nullptr,
} ;
}
GranularPitchShifterEffect::GranularPitchShifterEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key) :
Effect(&granularpitchshifter_plugin_descriptor, parent, key),
m_granularpitchshifterControls(this),
m_prefilter({PrefilterLowpass(), PrefilterLowpass()})
{
autoQuitModel()->setValue(autoQuitModel()->maxValue());
changeSampleRate();
}
Effect::ProcessStatus GranularPitchShifterEffect::processImpl(SampleFrame* buf, const fpp_t frames)
{
const float d = dryLevel();
const float w = wetLevel();
const ValueBuffer* pitchBuf = m_granularpitchshifterControls.m_pitchModel.valueBuffer();
const ValueBuffer* pitchSpreadBuf = m_granularpitchshifterControls.m_pitchSpreadModel.valueBuffer();
const float size = m_granularpitchshifterControls.m_sizeModel.value();
const float shape = m_granularpitchshifterControls.m_shapeModel.value();
const float jitter = m_granularpitchshifterControls.m_jitterModel.value();
const float twitch = m_granularpitchshifterControls.m_twitchModel.value();
const float spray = m_granularpitchshifterControls.m_sprayModel.value();
const float spraySpread = m_granularpitchshifterControls.m_spraySpreadModel.value();
const float density = m_granularpitchshifterControls.m_densityModel.value();
const float glide = m_granularpitchshifterControls.m_glideModel.value();
const int minLatency = m_granularpitchshifterControls.m_minLatencyModel.value() * m_sampleRate;
const float densityInvRoot = std::sqrt(1.f / density);
const float feedback = m_granularpitchshifterControls.m_feedbackModel.value();
const float fadeLength = 1.f / m_granularpitchshifterControls.m_fadeLengthModel.value();
const bool prefilter = m_granularpitchshifterControls.m_prefilterModel.value();
if (glide != m_oldGlide)
{
m_oldGlide = glide;
m_glideCoef = glide > 0 ? std::exp(-1 / (glide * m_sampleRate)) : 0;
}
const float shapeK = cosWindowApproxK(shape);
const int sizeSamples = m_sampleRate / size;
const float waitMult = sizeSamples / (density * 2);
for (fpp_t f = 0; f < frames; ++f)
{
const double pitch = (pitchBuf ? pitchBuf->value(f) : m_granularpitchshifterControls.m_pitchModel.value()) * (1. / 12.);
const double pitchSpread = (pitchSpreadBuf ? pitchSpreadBuf->value(f) : m_granularpitchshifterControls.m_pitchSpreadModel.value()) * (1. / 24.);
// interpolate pitch depending on glide
for (int i = 0; i < 2; ++i)
{
double targetVal = pitch + pitchSpread * (i ? 1. : -1.);
if (targetVal == m_truePitch[i]) { continue; }
m_updatePitches = true;
m_truePitch[i] = m_glideCoef * m_truePitch[i] + (1. - m_glideCoef) * targetVal;
// we crudely lock the pitch to the target value once it gets close enough, so we can save on CPU
if (std::abs(targetVal - m_truePitch[i]) < GlideSnagRadius) { m_truePitch[i] = targetVal; }
}
// this stuff is computationally expensive, so we should only do it when necessary
if (m_updatePitches)
{
m_updatePitches = false;
std::array<double, 2> speed = {
std::exp2(m_truePitch[0]),
std::exp2(m_truePitch[1])
};
std::array<double, 2> ratio = {
speed[0] / m_speed[0],
speed[1] / m_speed[1]
};
for (int i = 0; i < m_grainCount; ++i)
{
for (int j = 0; j < 2; ++j)
{
m_grains[i].grainSpeed[j] *= ratio[j];
// we unfortunately need to do extra stuff to ensure these don't shoot past the write index...
if (m_grains[i].grainSpeed[j] > 1)
{
double distance = m_writePoint - m_grains[i].readPoint[j] - SafetyLatency;
if (distance <= 0) { distance += m_ringBufLength; }
double grainSpeedRequired = ((m_grains[i].grainSpeed[j] - 1.) / distance) * (1. - m_grains[i].phase);
m_grains[i].phaseSpeed[j] = std::max(m_grains[i].phaseSpeed[j], grainSpeedRequired);
}
}
}
m_speed[0] = speed[0];
m_speed[1] = speed[1];
// prevent aliasing by lowpassing frequencies that the pitch shifting would push above nyquist
m_prefilter[0].setCoefs(m_sampleRate, std::min(m_nyquist / static_cast<float>(speed[0]), m_nyquist) * PrefilterBandwidth);
m_prefilter[1].setCoefs(m_sampleRate, std::min(m_nyquist / static_cast<float>(speed[1]), m_nyquist) * PrefilterBandwidth);
}
std::array<float, 2> s = {0, 0};
std::array<float, 2> filtered = {buf[f][0], buf[f][1]};
// spawn a new grain if it's time
if (++m_timeSinceLastGrain >= m_nextWaitRandomization * waitMult)
{
m_timeSinceLastGrain = 0;
auto randThing = fastRand<double>(-1.0, +1.0);
m_nextWaitRandomization = std::exp2(randThing * twitch);
double grainSpeed = 1. / std::exp2(randThing * jitter);
std::array<float, 2> sprayResult = {0, 0};
if (spray > 0)
{
sprayResult[0] = fastRand(spray * m_sampleRate);
sprayResult[1] = std::lerp(sprayResult[0], fastRand(spray * m_sampleRate), spraySpread);
}
std::array<int, 2> readPoint;
int latency = std::max(static_cast<int>(std::max(sizeSamples * (std::max(m_speed[0], m_speed[1]) * grainSpeed - 1.), 0.) + SafetyLatency), minLatency);
for (int i = 0; i < 2; ++i)
{
readPoint[i] = m_writePoint - latency - sprayResult[i];
if (readPoint[i] < 0) { readPoint[i] += m_ringBufLength; }
}
const double phaseInc = 1. / sizeSamples;
m_grains.push_back(Grain(grainSpeed * m_speed[0], grainSpeed * m_speed[1], phaseInc, phaseInc, readPoint[0], readPoint[1]));
++m_grainCount;
}
for (int i = 0; i < m_grainCount; ++i)
{
m_grains[i].phase += std::max(m_grains[i].phaseSpeed[0], m_grains[i].phaseSpeed[1]);
if (m_grains[i].phase >= 1)
{
// grain is done, delete it
std::swap(m_grains[i], m_grains[m_grainCount-1]);
m_grains.pop_back();
--i;
--m_grainCount;
continue;
}
m_grains[i].readPoint[0] += m_grains[i].grainSpeed[0];
m_grains[i].readPoint[1] += m_grains[i].grainSpeed[1];
if (m_grains[i].readPoint[0] >= m_ringBufLength) { m_grains[i].readPoint[0] -= m_ringBufLength; }
if (m_grains[i].readPoint[1] >= m_ringBufLength) { m_grains[i].readPoint[1] -= m_ringBufLength; }
const float fadePos = std::clamp((-std::abs(-2.f * static_cast<float>(m_grains[i].phase) + 1.f) + 0.5f) * fadeLength + 0.5f, 0.f, 1.f);
const float windowVal = cosHalfWindowApprox(fadePos, shapeK);
s[0] += getHermiteSample(m_grains[i].readPoint[0], 0) * windowVal;
s[1] += getHermiteSample(m_grains[i].readPoint[1], 1) * windowVal;
}
// note that adding two signals together, when uncorrelated, results in a signal power multiplication of sqrt(2), not 2
s[0] *= densityInvRoot;
s[1] *= densityInvRoot;
// 1-pole highpass for DC offset removal, to make feedback safer
s[0] -= (m_dcVal[0] = (1.f - m_dcCoeff) * s[0] + m_dcCoeff * m_dcVal[0]);
s[1] -= (m_dcVal[1] = (1.f - m_dcCoeff) * s[1] + m_dcCoeff * m_dcVal[1]);
// cheap safety saturator to protect against infinite feedback
if (feedback > 0)
{
s[0] = safetySaturate(s[0]);
s[1] = safetySaturate(s[1]);
}
if (++m_writePoint >= m_ringBufLength)
{
m_writePoint = 0;
}
if (prefilter)
{
filtered[0] = m_prefilter[0].process(filtered[0]);
filtered[1] = m_prefilter[1].process(filtered[1]);
}
m_ringBuf[m_writePoint][0] = filtered[0] + s[0] * feedback;
m_ringBuf[m_writePoint][1] = filtered[1] + s[1] * feedback;
buf[f][0] = d * buf[f][0] + w * s[0];
buf[f][1] = d * buf[f][1] + w * s[1];
}
if (m_sampleRateNeedsUpdate)
{
m_sampleRateNeedsUpdate = false;
changeSampleRate();
}
return Effect::ProcessStatus::ContinueIfNotQuiet;
}
void GranularPitchShifterEffect::changeSampleRate()
{
const int range = m_granularpitchshifterControls.m_rangeModel.value();
const float ringBufLength = RangeSeconds[range];
m_sampleRate = Engine::audioEngine()->outputSampleRate();
m_nyquist = m_sampleRate / 2;
m_ringBufLength = m_sampleRate * ringBufLength;
m_ringBuf.resize(m_ringBufLength);
for (size_t i = 0; i < static_cast<std::size_t>(m_ringBufLength); ++i)
{
m_ringBuf[i][0] = 0;
m_ringBuf[i][1] = 0;
}
m_writePoint = 0;
m_oldGlide = -1;
m_updatePitches = true;
m_grains.clear();
m_grainCount = 0;
m_grains.reserve(8);// arbitrary
m_dcCoeff = std::exp(-2 * std::numbers::pi_v<float> * DcRemovalHz / m_sampleRate);
const double pitch = m_granularpitchshifterControls.m_pitchModel.value() * (1. / 12.);
const double pitchSpread = m_granularpitchshifterControls.m_pitchSpreadModel.value() * (1. / 24.);
m_truePitch[0] = pitch - pitchSpread;
m_truePitch[1] = pitch + pitchSpread;
m_speed[0] = std::exp2(m_truePitch[0]);
m_speed[1] = std::exp2(m_truePitch[1]);
}
extern "C"
{
// necessary for getting instance out of shared lib
PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* parent, void* data)
{
return new GranularPitchShifterEffect(parent, static_cast<const Plugin::Descriptor::SubPluginFeatures::Key*>(data));
}
}
} // namespace lmms