diff --git a/Resources/svg/spiral_bitcrush.svg b/Resources/svg/spiral_bitcrush.svg new file mode 100644 index 00000000..978a696e --- /dev/null +++ b/Resources/svg/spiral_bitcrush.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 820d424c..7d9641d7 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -12,6 +12,7 @@ #include "audio/BitCrushEffect.h" #include "audio/BulgeEffect.h" #include "audio/TwistEffect.h" +#include "audio/SpiralBitCrushEffect.h" #include "audio/DistortEffect.h" #include "audio/KaleidoscopeEffect.h" #include "audio/MultiplexEffect.h" @@ -58,6 +59,7 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse toggleableEffects.push_back(BounceEffect().build()); toggleableEffects.push_back(TwistEffect().build()); toggleableEffects.push_back(SkewEffect().build()); + toggleableEffects.push_back(SpiralBitCrushEffect().build()); #endif auto scaleEffect = ScaleEffectApp().build(); diff --git a/Source/audio/SpiralBitCrushEffect.h b/Source/audio/SpiralBitCrushEffect.h new file mode 100644 index 00000000..5a7c0c3a --- /dev/null +++ b/Source/audio/SpiralBitCrushEffect.h @@ -0,0 +1,78 @@ +#pragma once +#include + +class SpiralBitCrushEffect : public osci::EffectApplication { +public: + osci::Point apply(int index, osci::Point input, const std::vector> &values, double sampleRate) override { + // Completing one revolution in input space traverses the hypotenuse of one "domain" in log-polar space + double effectScale = juce::jlimit(0.0, 1.0, values[0].load()); + double domainX = juce::jmax(2.0, std::floor(values[1].load() + 0.001)); + double domainY = std::round(domainX * values[2].load()); + double zoom = values[3].load() * juce::MathConstants::twoPi; // Use same scale as angle + double rotation = values[4].load() * juce::MathConstants::twoPi; + + double domainHypot = std::hypot(domainX, domainY); + double domainTheta = std::atan2(domainY, domainX); + double scale = domainHypot / juce::MathConstants::twoPi; + osci::Point output(0); + + // Round to log-polar grid + if (input.x != 0 || input.y != 0) { + // Convert input point to log-polar coordinates transformed based on domain and offset + // Note 90 degree rotation: Theta is treated relative to -Y rather than +X + double r = std::hypot(input.x, input.y); + double logR = std::log(r); + double theta = std::atan2(input.x, -input.y); + osci::Point logPolarCoords(theta - rotation, logR - zoom); + logPolarCoords.rotate(0, 0, domainTheta); + logPolarCoords = logPolarCoords * scale; + + // Round this point to the center of the log-polar cell the input lies in, convert back to Cartesian + logPolarCoords.x = std::round(logPolarCoords.x); + logPolarCoords.y = std::round(logPolarCoords.y); + logPolarCoords = logPolarCoords / scale; + logPolarCoords.rotate(0, 0, -domainTheta); + double outR = std::exp(logPolarCoords.y + zoom); + double outTheta = logPolarCoords.x + rotation; + output.x = outR * std::sin(outTheta); + output.y = outR * -std::cos(outTheta); + } + + // Round z in log space using same spacing as xy log-polar grid + // Use same offset as xy's radial offset to be consistent with the appearance of zooming + if (input.z != 0) { + double signZ = input.z > 0 ? 1.0 : -1.0; + double logZ = std::log(std::abs(input.z)); + logZ = (logZ - zoom) * scale; + logZ = std::round(logZ); + logZ = logZ / scale + zoom; + output.z = signZ * std::exp(logZ); + } + return (1 - effectScale) * input + effectScale * output; + } + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter("Spiral Bit Crush", + "Constrains points to a spiral pattern.", + "spiralBitCrush", VERSION_HINT, 0.4, 0.0, 1.0), + new osci::EffectParameter("Spiral Density", + "Controls the density of the spiral pattern.", + "spiralBitCrushDensity", VERSION_HINT, 13.0, 3.0, 30.0, 1.0), + new osci::EffectParameter("Spiral Twist", + "Controls how much the spiral pattern twists.", + "spiralBitCrushTwist", VERSION_HINT, 0.6, -1.0, 1.0), + new osci::EffectParameter("Zoom", + "Zooms the spiral pattern.", + "spiralBitCrushZoom", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 0.1), + new osci::EffectParameter("Rotation", + "Rotates the spiral pattern.", + "spiralBitCrushRotation", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::ReverseSawtooth, 0.02) + } + ); + eff->setIcon(BinaryData::spiral_bitcrush_svg); + return eff; + } +}; diff --git a/osci-render.jucer b/osci-render.jucer index 43b87678..2fa55d8c 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -142,6 +142,8 @@ + @@ -170,6 +172,8 @@ +