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

Add Granular Pitch Shifter effect #7328

Merged
merged 22 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions cmake/modules/PluginList.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ SET(LMMS_PLUGIN_LIST
DynamicsProcessor
Eq
Flanger
GranularPitchShifter
HydrogenImport
LadspaBrowser
LadspaEffect
Expand Down
3 changes: 3 additions & 0 deletions plugins/GranularPitchShifter/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
INCLUDE(BuildPlugin)

BUILD_PLUGIN(granularpitchshifter GranularPitchShifter.cpp GranularPitchShifterControls.cpp GranularPitchShifterControlDialog.cpp MOCFILES GranularPitchShifterControls.h GranularPitchShifterControlDialog.h EMBEDDED_RESOURCES *.png)
269 changes: 269 additions & 0 deletions plugins/GranularPitchShifter/GranularPitchShifter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
/*
* 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 "GranularPitchShifter.h"

#include <cmath>
#include "embed.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();
}


bool GranularPitchShifterEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames)
{
if (!isEnabled() || !isRunning()) { return false; }

const float d = dryLevel();
const float w = wetLevel();

const ValueBuffer * pitchBuf = m_granularpitchshifterControls.m_pitchModel.valueBuffer();
Copy link
Contributor

@Rossmaxx Rossmaxx Jun 18, 2024

Choose a reason for hiding this comment

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

Just so you know, valuebuffer is on it's way out in #7297 . You don't need to worry for now. I'll intervene for fixing this PR up if that one gets merged.

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() * densityInvRoot;
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 = std::exp(-1 / (glide * m_sampleRate));
}

const float shapeK = cosWindowApproxK(shape);
const int sizeSamples = static_cast<int>((m_sampleRate / size) * 0.5) * 2;
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();
const double pitchSpread = (pitchSpreadBuf ? pitchSpreadBuf->value(f) : m_granularpitchshifterControls.m_pitchSpreadModel.value()) * 0.5f;

// 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]) < GPS_GLIDE_SNAG_RADIUS) { m_truePitch[i] = targetVal; }
}

// this stuff is computationally expensive, so we should only do it when necessary
if (m_updatePitches)
{
m_updatePitches = false;

const double speed[2] = {std::exp2(m_truePitch[0] * (1. / 12.)), std::exp2(m_truePitch[1] * (1. / 12.))};
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].m_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].m_grainSpeed[j] > 1)
{
double distance = m_writePoint - m_grains[i].m_readPoint[j] - GPS_SAFETY_LATENCY;
if (distance <= 0) { distance += m_ringBufLength; }
double grainSpeedRequired = ((m_grains[i].m_grainSpeed[j] - 1.) / distance) * (1. - m_grains[i].m_phase);
m_grains[i].m_phaseSpeed[j] = std::max(m_grains[i].m_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) * GPS_PREFILTER_BW);
m_prefilter[1].setCoefs(m_sampleRate, std::min(m_nyquist / static_cast<float>(speed[1]), m_nyquist) * GPS_PREFILTER_BW);
}

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;
double randThing = (fast_rand()/static_cast<double>(FAST_RAND_MAX) * 2. - 1.);
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] = (fast_rand() / static_cast<float>(FAST_RAND_MAX)) * spray * m_sampleRate;
sprayResult[1] = linearInterpolate(sprayResult[0],
(fast_rand() / static_cast<float>(FAST_RAND_MAX)) * spray * m_sampleRate,
spraySpread);
}

std::array<int, 2> readPoint;
int latency = std::max(int(std::max(sizeSamples * (std::max(m_speed[0], m_speed[1]) * grainSpeed - 1.), 0.) + GPS_SAFETY_LATENCY), 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(GranularPitchShifterGrain(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].m_phase += std::max(m_grains[i].m_phaseSpeed[0], m_grains[i].m_phaseSpeed[1]);
if (m_grains[i].m_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].m_readPoint[0] += m_grains[i].m_grainSpeed[0];
m_grains[i].m_readPoint[1] += m_grains[i].m_grainSpeed[1];
if (m_grains[i].m_readPoint[0] >= m_ringBufLength) { m_grains[i].m_readPoint[0] -= m_ringBufLength; }
if (m_grains[i].m_readPoint[1] >= m_ringBufLength) { m_grains[i].m_readPoint[1] -= m_ringBufLength; }

const float fadePos = std::clamp((-std::fabs(-2.f * static_cast<float>(m_grains[i].m_phase) + 1.f) + 0.5f) * fadeLength + 0.5f, 0.f, 1.f);
const float windowVal = cosHalfWindowApprox(fadePos, shapeK);
s[0] += getHermiteSample(m_grains[i].m_readPoint[0], 0) * windowVal;
s[1] += getHermiteSample(m_grains[i].m_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;

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 isRunning();
}

void GranularPitchShifterEffect::changeSampleRate()
{

Check notice on line 234 in plugins/GranularPitchShifter/GranularPitchShifter.cpp

View check run for this annotation

codefactor.io / CodeFactor

plugins/GranularPitchShifter/GranularPitchShifter.cpp#L65-L234

Complex Method
const int range = m_granularpitchshifterControls.m_rangeModel.value();
const float ringBufLength = GPS_RANGE_SECONDS[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 < 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
}


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
Loading
Loading