From 1c63a18bb5d8d6e31e52b8de0d29708865c6d606 Mon Sep 17 00:00:00 2001 From: Anthony Hall Date: Wed, 3 Sep 2025 03:33:19 -0700 Subject: [PATCH 1/4] Add spiral bitcrush effect --- Source/PluginProcessor.cpp | 2 + Source/audio/SpiralBitCrushEffect.h | 66 +++++++++++++++++++++++++++++ osci-render.jucer | 2 + 3 files changed, 70 insertions(+) create mode 100644 Source/audio/SpiralBitCrushEffect.h diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 6e25aaec..c132d4f3 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" @@ -54,6 +55,7 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse toggleableEffects.push_back(KaleidoscopeEffect(*this).build()); toggleableEffects.push_back(BounceEffect().build()); toggleableEffects.push_back(TwistEffect().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..2fce3be5 --- /dev/null +++ b/Source/audio/SpiralBitCrushEffect.h @@ -0,0 +1,66 @@ +#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.0001)); + double domainY = std::round(domainX * values[2].load()); + osci::Point offset(values[3].load(), values[4].load()); + + 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, logR); + logPolarCoords.rotate(0, 0, domainTheta); + logPolarCoords = logPolarCoords * scale - offset; + + // 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 + offset) / scale; + logPolarCoords.rotate(0, 0, -domainTheta); + double outR = std::exp(logPolarCoords.y); + double outTheta = logPolarCoords.x; + 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 * scale - offset.y; + logZ = std::round(logZ); + logZ = (logZ + offset.y) / scale; + 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.", "spiralBitCrushEnable", VERSION_HINT, 1.0, 0.0, 1.0), + new osci::EffectParameter("Spiral Density", "Controls the density of the spiral pattern.", "spiralDensity", VERSION_HINT, 13.0, 3.0, 30.0), + new osci::EffectParameter("Spiral Twist", "Controls how much the spiral pattern twists.", "spiralTwist", VERSION_HINT, -0.6, -1.0, 1.0), + new osci::EffectParameter("Angle Offset", "Rotates the spiral pattern.", "spiralOffsetX", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 0.4), + new osci::EffectParameter("Radial Offset", "Zooms the spiral pattern.", "spiralOffsetY", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 1.0) + }); + eff->setIcon(BinaryData::swirl_svg); + return eff; + } +}; \ No newline at end of file diff --git a/osci-render.jucer b/osci-render.jucer index 98a4786c..5ca81275 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -165,6 +165,8 @@ + From 00fbb4f1d7f2a2bc9a92e94d9892c57687609b42 Mon Sep 17 00:00:00 2001 From: Anthony Hall Date: Sun, 7 Sep 2025 04:00:12 -0700 Subject: [PATCH 2/4] trailing newline --- Source/audio/SpiralBitCrushEffect.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/audio/SpiralBitCrushEffect.h b/Source/audio/SpiralBitCrushEffect.h index 2fce3be5..b3fbe897 100644 --- a/Source/audio/SpiralBitCrushEffect.h +++ b/Source/audio/SpiralBitCrushEffect.h @@ -63,4 +63,4 @@ public: eff->setIcon(BinaryData::swirl_svg); return eff; } -}; \ No newline at end of file +}; From 473708a427288f4941d85cc0cefb0f5929a1cbae Mon Sep 17 00:00:00 2001 From: Anthony Hall Date: Tue, 9 Sep 2025 17:51:31 -0700 Subject: [PATCH 3/4] Overhaul zoom and rotation, modify parameter details --- Source/audio/SpiralBitCrushEffect.h | 52 ++++++++++++++++++----------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/Source/audio/SpiralBitCrushEffect.h b/Source/audio/SpiralBitCrushEffect.h index b3fbe897..713fda1a 100644 --- a/Source/audio/SpiralBitCrushEffect.h +++ b/Source/audio/SpiralBitCrushEffect.h @@ -5,11 +5,12 @@ 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.0001)); + 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()); - osci::Point offset(values[3].load(), values[4].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; @@ -22,17 +23,17 @@ public: 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, logR); + osci::Point logPolarCoords(theta - rotation, logR - zoom); logPolarCoords.rotate(0, 0, domainTheta); - logPolarCoords = logPolarCoords * scale - offset; + 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 + offset) / scale; + logPolarCoords = logPolarCoords / scale; logPolarCoords.rotate(0, 0, -domainTheta); - double outR = std::exp(logPolarCoords.y); - double outTheta = logPolarCoords.x; + double outR = std::exp(logPolarCoords.y + zoom); + double outTheta = logPolarCoords.x + rotation; output.x = outR * std::sin(outTheta); output.y = outR * -std::cos(outTheta); } @@ -42,24 +43,35 @@ public: if (input.z != 0) { double signZ = input.z > 0 ? 1.0 : -1.0; double logZ = std::log(std::abs(input.z)); - logZ = logZ * scale - offset.y; + logZ = (logZ - zoom) * scale; logZ = std::round(logZ); - logZ = (logZ + offset.y) / scale; + 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.", "spiralBitCrushEnable", VERSION_HINT, 1.0, 0.0, 1.0), - new osci::EffectParameter("Spiral Density", "Controls the density of the spiral pattern.", "spiralDensity", VERSION_HINT, 13.0, 3.0, 30.0), - new osci::EffectParameter("Spiral Twist", "Controls how much the spiral pattern twists.", "spiralTwist", VERSION_HINT, -0.6, -1.0, 1.0), - new osci::EffectParameter("Angle Offset", "Rotates the spiral pattern.", "spiralOffsetX", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 0.4), - new osci::EffectParameter("Radial Offset", "Zooms the spiral pattern.", "spiralOffsetY", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 1.0) - }); + 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, 1.0, 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.05), + 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::swirl_svg); return eff; } From d036d2b0afb7c4b6fbda6684a123fc8493420d27 Mon Sep 17 00:00:00 2001 From: James H Ball Date: Thu, 11 Sep 2025 20:53:28 +0100 Subject: [PATCH 4/4] Update spiral bitcrush default values and add icon --- Resources/svg/spiral_bitcrush.svg | 1 + Source/audio/SpiralBitCrushEffect.h | 6 +++--- osci-render.jucer | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 Resources/svg/spiral_bitcrush.svg 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/audio/SpiralBitCrushEffect.h b/Source/audio/SpiralBitCrushEffect.h index 713fda1a..5a7c0c3a 100644 --- a/Source/audio/SpiralBitCrushEffect.h +++ b/Source/audio/SpiralBitCrushEffect.h @@ -57,7 +57,7 @@ public: std::vector{ new osci::EffectParameter("Spiral Bit Crush", "Constrains points to a spiral pattern.", - "spiralBitCrush", VERSION_HINT, 1.0, 0.0, 1.0), + "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), @@ -66,13 +66,13 @@ public: "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.05), + "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::swirl_svg); + eff->setIcon(BinaryData::spiral_bitcrush_svg); return eff; } }; diff --git a/osci-render.jucer b/osci-render.jucer index 0bd03d70..2fa55d8c 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -142,6 +142,8 @@ +