-
-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Changes from 5 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
65fed2d
Add Granular Pitch Shifter effect
LostRobotMusic 15dbc8b
Fix inaccuracy in documentation
LostRobotMusic 4f81b74
fix/adjust Range options
LostRobotMusic 62d9a84
Appease the CodeFactor theocracy
LostRobotMusic 409991f
windows hates M_PI apparently
LostRobotMusic 9e667a1
grapefruit
LostRobotMusic e2153d5
fix fade length zero division
LostRobotMusic cf1e566
fix bugged automation ranges
LostRobotMusic 3c745ce
h
LostRobotMusic ed1d1e0
update artwork
LostRobotMusic 56b3100
code style
LostRobotMusic a4bb80d
code style 2: electric boogaloo
LostRobotMusic 70b93ba
code style 3: revenge of the synth
LostRobotMusic 58088f0
code style 4 and knuckles
LostRobotMusic 71a817e
code style 5: meakashi
LostRobotMusic f0ba517
code style 6: the number of grapefruits i ate this week
LostRobotMusic ff9b7b0
yum i am so full of msvc-exclusive compilation errors
LostRobotMusic dced163
gui alignment adjustments
LostRobotMusic ab3f5ed
literally 1984
LostRobotMusic a96666d
dc removal and safety saturation and more
LostRobotMusic 0a60b72
fix magic numbers
LostRobotMusic 50cd9d8
yum a second helping of msvc-exclusive compilation errors
LostRobotMusic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
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; | ||
LostRobotMusic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
tresf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
m_updatePitches = false; | ||
|
||
const double speed[2] = {std::exp2(m_truePitch[0] * (1. / 12.)), std::exp2(m_truePitch[1] * (1. / 12.))}; | ||
LostRobotMusic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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() | ||
{ | ||
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 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
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.