From d842ee2096e203921a737a422d44a6d2610eba3d Mon Sep 17 00:00:00 2001 From: Anthony Hall Date: Wed, 3 Sep 2025 17:35:15 -0700 Subject: [PATCH 1/5] Add polygon bitcrush, optimized but readable bulge --- Source/PluginProcessor.cpp | 2 ++ Source/audio/BulgeEffect.h | 9 +++-- Source/audio/PolygonBitCrushEffect.h | 52 ++++++++++++++++++++++++++++ osci-render.jucer | 2 ++ 4 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 Source/audio/PolygonBitCrushEffect.h diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 6e25aaec..26f306e3 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/PolygonBitCrushEffect.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(PolygonBitCrushEffect().build()); #endif auto scaleEffect = ScaleEffectApp().build(); diff --git a/Source/audio/BulgeEffect.h b/Source/audio/BulgeEffect.h index c15c9c15..4e075093 100644 --- a/Source/audio/BulgeEffect.h +++ b/Source/audio/BulgeEffect.h @@ -7,11 +7,10 @@ public: double value = values[0]; double translatedBulge = -value + 1; - double r = sqrt(pow(input.x, 2) + pow(input.y, 2)); - double theta = atan2(input.y, input.x); - double rn = pow(r, translatedBulge); - - return osci::Point(rn * cos(theta), rn * sin(theta), input.z); + double r = std::hypot(input.x, input.y); + double rn = std::pow(r, translatedBulge); + double scale = rn / r; + return osci::Point(scale * input.x, scale * input.y, input.z); } std::shared_ptr build() const override { diff --git a/Source/audio/PolygonBitCrushEffect.h b/Source/audio/PolygonBitCrushEffect.h new file mode 100644 index 00000000..05a19951 --- /dev/null +++ b/Source/audio/PolygonBitCrushEffect.h @@ -0,0 +1,52 @@ +#pragma once +#include +#include "../MathUtil.h" + +// Inspired by xenontesla122 +class PolygonBitCrushEffect : public osci::EffectApplication { +public: + osci::Point apply(int index, osci::Point input, const std::vector> &values, double sampleRate) override { + const double pi = juce::MathConstants::pi; + const double twoPi = juce::MathConstants::twoPi; + double effectScale = juce::jlimit(0.0, 1.0, values[0].load()); + double nSides = juce::jmax(2.0, values[1].load()); + double bandSize = juce::jmax(1e-4, values[2].load()); + double thetaOffset = values[3] * twoPi; + double rOffset = values[4]; + + osci::Point output(0); + if (input.x != 0 || input.y != 0) { + // Note 90 degree rotation: Theta is treated relative to -Y rather than +X + double r = std::hypot(input.x, input.y); + double theta = std::atan2(-input.x, input.y) - thetaOffset; + theta = MathUtil::wrapAngle(theta + pi) - pi; // Move branch cut to +/-pi after thetaOffset is applied + double regionTheta = std::round(theta * nSides / twoPi) / nSides * twoPi; + double localTheta = theta - regionTheta; + double dist = r * std::cos(localTheta); + double newDist = juce::jmax(0.0, (std::round(dist / bandSize - rOffset) + rOffset) * bandSize); + double scale = newDist / dist; + output.x = scale * input.x; + output.y = scale * input.y; + } + // Apply the same stripe quantization to abs(z) + if (input.z != 0) { + double signZ = input.z > 0 ? 1 : -1; + output.z = signZ * juce::jmax(0.0, (std::round(std::abs(input.z) / bandSize - rOffset) + rOffset) * bandSize); + } + 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("Polygon Bit Crush", "Constrains points to a polygon pattern.", "polygonBitCrushEnable", VERSION_HINT, 1.0, 0.0, 1.0), + new osci::EffectParameter("Sides", "Controls the number of sides of the polygon pattern.", "polygonSides", VERSION_HINT, 5.0, 3.0, 8.0), + new osci::EffectParameter("Stripe Size", "Controls the spacing between the stripes of the polygon pattern.", "polygonBandSize", VERSION_HINT, 0.15, 0.0, 0.5), + new osci::EffectParameter("Angle Offset", "Rotates the polygon pattern.", "polygonAngleOffset", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 0.1), + new osci::EffectParameter("Radial Offset", "Offsets the stripes of the polygon pattern.", "polygonROffset", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 2.0) + }); + eff->setIcon(BinaryData::diamond_svg); + return eff; + } +}; \ No newline at end of file diff --git a/osci-render.jucer b/osci-render.jucer index 98a4786c..ae0df17f 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -165,6 +165,8 @@ + From 29a7ddf93b14efe6d1e0aff78060126e004aae7f Mon Sep 17 00:00:00 2001 From: Anthony Hall Date: Thu, 4 Sep 2025 02:14:09 -0700 Subject: [PATCH 2/5] Rename bandSize to stripeSize --- Source/audio/PolygonBitCrushEffect.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/audio/PolygonBitCrushEffect.h b/Source/audio/PolygonBitCrushEffect.h index 05a19951..17cb3bb5 100644 --- a/Source/audio/PolygonBitCrushEffect.h +++ b/Source/audio/PolygonBitCrushEffect.h @@ -10,7 +10,7 @@ public: const double twoPi = juce::MathConstants::twoPi; double effectScale = juce::jlimit(0.0, 1.0, values[0].load()); double nSides = juce::jmax(2.0, values[1].load()); - double bandSize = juce::jmax(1e-4, values[2].load()); + double stripeSize = juce::jmax(1e-4, values[2].load()); double thetaOffset = values[3] * twoPi; double rOffset = values[4]; @@ -23,7 +23,7 @@ public: double regionTheta = std::round(theta * nSides / twoPi) / nSides * twoPi; double localTheta = theta - regionTheta; double dist = r * std::cos(localTheta); - double newDist = juce::jmax(0.0, (std::round(dist / bandSize - rOffset) + rOffset) * bandSize); + double newDist = juce::jmax(0.0, (std::round(dist / stripeSize - rOffset) + rOffset) * stripeSize); double scale = newDist / dist; output.x = scale * input.x; output.y = scale * input.y; @@ -31,7 +31,7 @@ public: // Apply the same stripe quantization to abs(z) if (input.z != 0) { double signZ = input.z > 0 ? 1 : -1; - output.z = signZ * juce::jmax(0.0, (std::round(std::abs(input.z) / bandSize - rOffset) + rOffset) * bandSize); + output.z = signZ * juce::jmax(0.0, (std::round(std::abs(input.z) / stripeSize - rOffset) + rOffset) * stripeSize); } return (1 - effectScale) * input + effectScale * output; } From 82f10238fba1de30b41ec13818ef73ef9425ea56 Mon Sep 17 00:00:00 2001 From: Anthony Hall Date: Sun, 7 Sep 2025 14:33:39 -0700 Subject: [PATCH 3/5] Improve stripe size range, remove accidental quantization of 2D shapes --- Source/audio/PolygonBitCrushEffect.h | 95 +++++++++++++++------------- 1 file changed, 52 insertions(+), 43 deletions(-) diff --git a/Source/audio/PolygonBitCrushEffect.h b/Source/audio/PolygonBitCrushEffect.h index 17cb3bb5..fa4cda3a 100644 --- a/Source/audio/PolygonBitCrushEffect.h +++ b/Source/audio/PolygonBitCrushEffect.h @@ -5,48 +5,57 @@ // Inspired by xenontesla122 class PolygonBitCrushEffect : public osci::EffectApplication { public: - osci::Point apply(int index, osci::Point input, const std::vector> &values, double sampleRate) override { - const double pi = juce::MathConstants::pi; - const double twoPi = juce::MathConstants::twoPi; - double effectScale = juce::jlimit(0.0, 1.0, values[0].load()); - double nSides = juce::jmax(2.0, values[1].load()); - double stripeSize = juce::jmax(1e-4, values[2].load()); - double thetaOffset = values[3] * twoPi; - double rOffset = values[4]; + osci::Point apply(int index, osci::Point input, const std::vector> &values, double sampleRate) override { + const double pi = juce::MathConstants::pi; + const double twoPi = juce::MathConstants::twoPi; + double effectScale = juce::jlimit(0.0, 1.0, values[0].load()); + double nSides = juce::jmax(2.0, values[1].load()); + double stripeSize = juce::jmax(1e-4, values[2].load()); + stripeSize = std::pow(stripeSize, 1.5); // Bias slightly toward smaller values + double thetaOffset = values[3] * twoPi; + double rOffset = values[4]; - osci::Point output(0); - if (input.x != 0 || input.y != 0) { - // Note 90 degree rotation: Theta is treated relative to -Y rather than +X - double r = std::hypot(input.x, input.y); - double theta = std::atan2(-input.x, input.y) - thetaOffset; - theta = MathUtil::wrapAngle(theta + pi) - pi; // Move branch cut to +/-pi after thetaOffset is applied - double regionTheta = std::round(theta * nSides / twoPi) / nSides * twoPi; - double localTheta = theta - regionTheta; - double dist = r * std::cos(localTheta); - double newDist = juce::jmax(0.0, (std::round(dist / stripeSize - rOffset) + rOffset) * stripeSize); - double scale = newDist / dist; - output.x = scale * input.x; - output.y = scale * input.y; - } - // Apply the same stripe quantization to abs(z) - if (input.z != 0) { - double signZ = input.z > 0 ? 1 : -1; - output.z = signZ * juce::jmax(0.0, (std::round(std::abs(input.z) / stripeSize - rOffset) + rOffset) * stripeSize); - } - return (1 - effectScale) * input + effectScale * output; - } + osci::Point output(0); + if (input.x != 0 || input.y != 0) { + // Note 90 degree rotation: Theta is treated relative to +Y rather than +X + double r = std::hypot(input.x, input.y); + double theta = std::atan2(-input.x, input.y) - thetaOffset; + theta = MathUtil::wrapAngle(theta + pi) - pi; // Move branch cut to +/-pi after thetaOffset is applied + double regionCenterTheta = std::round(theta * nSides / twoPi) / nSides * twoPi; + double localTheta = theta - regionCenterTheta; + double dist = r * std::cos(localTheta); + double newDist = juce::jmax(0.0, (std::round(dist / stripeSize - rOffset) + rOffset) * stripeSize); + double scale = newDist / dist; + output.x = scale * input.x; + output.y = scale * input.y; + } + // Apply the same stripe quantization to abs(z) if the input is 3D + double absZ = std::abs(input.z); + if (absZ > 0.0001) { + double signZ = input.z > 0 ? 1 : -1; + output.z = signZ * juce::jmax(0.0, (std::round(absZ / stripeSize - rOffset) + rOffset) * stripeSize); + } + 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("Polygon Bit Crush", "Constrains points to a polygon pattern.", "polygonBitCrushEnable", VERSION_HINT, 1.0, 0.0, 1.0), - new osci::EffectParameter("Sides", "Controls the number of sides of the polygon pattern.", "polygonSides", VERSION_HINT, 5.0, 3.0, 8.0), - new osci::EffectParameter("Stripe Size", "Controls the spacing between the stripes of the polygon pattern.", "polygonBandSize", VERSION_HINT, 0.15, 0.0, 0.5), - new osci::EffectParameter("Angle Offset", "Rotates the polygon pattern.", "polygonAngleOffset", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 0.1), - new osci::EffectParameter("Radial Offset", "Offsets the stripes of the polygon pattern.", "polygonROffset", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 2.0) - }); - eff->setIcon(BinaryData::diamond_svg); - return eff; - } -}; \ No newline at end of file + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter("Polygon Bit Crush", + "Constrains points to a polygon pattern.", + "polygonBitCrushEnable", VERSION_HINT, 1.0, 0.0, 1.0), + new osci::EffectParameter("Sides", "Controls the number of sides of the polygon pattern.", + "polygonSides", VERSION_HINT, 5.0, 3.0, 8.0), + new osci::EffectParameter("Stripe Size", + "Controls the spacing between the stripes of the polygon pattern.", + "polygonBandSize", VERSION_HINT, 0.15, 0.0, 0.5), + new osci::EffectParameter("Angle Offset", "Rotates the polygon pattern.", + "polygonAngleOffset", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 0.1), + new osci::EffectParameter("Radial Offset", "Offsets the stripes of the polygon pattern.", + "polygonROffset", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 2.0) + }); + eff->setIcon(BinaryData::diamond_svg); + return eff; + } +}; From 5009c9e7491eebb59eedb20a943e00f60cb2eb8b Mon Sep 17 00:00:00 2001 From: Anthony Hall Date: Tue, 9 Sep 2025 22:24:02 -0700 Subject: [PATCH 4/5] Transform stripe size slider, rename things, BulgeEffect guard origin --- Source/audio/BulgeEffect.h | 1 + Source/audio/PolygonBitCrushEffect.h | 35 ++++++++++++++-------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/Source/audio/BulgeEffect.h b/Source/audio/BulgeEffect.h index 4e075093..e9414c16 100644 --- a/Source/audio/BulgeEffect.h +++ b/Source/audio/BulgeEffect.h @@ -8,6 +8,7 @@ public: double translatedBulge = -value + 1; double r = std::hypot(input.x, input.y); + if (r == 0) return input; double rn = std::pow(r, translatedBulge); double scale = rn / r; return osci::Point(scale * input.x, scale * input.y, input.z); diff --git a/Source/audio/PolygonBitCrushEffect.h b/Source/audio/PolygonBitCrushEffect.h index fa4cda3a..b436537d 100644 --- a/Source/audio/PolygonBitCrushEffect.h +++ b/Source/audio/PolygonBitCrushEffect.h @@ -11,20 +11,20 @@ public: double effectScale = juce::jlimit(0.0, 1.0, values[0].load()); double nSides = juce::jmax(2.0, values[1].load()); double stripeSize = juce::jmax(1e-4, values[2].load()); - stripeSize = std::pow(stripeSize, 1.5); // Bias slightly toward smaller values - double thetaOffset = values[3] * twoPi; - double rOffset = values[4]; + stripeSize = std::pow(0.63 * stripeSize, 1.5); // Change range and bias toward smaller values + double rotation = values[3] * twoPi; + double stripePhase = values[4]; osci::Point output(0); if (input.x != 0 || input.y != 0) { // Note 90 degree rotation: Theta is treated relative to +Y rather than +X double r = std::hypot(input.x, input.y); - double theta = std::atan2(-input.x, input.y) - thetaOffset; - theta = MathUtil::wrapAngle(theta + pi) - pi; // Move branch cut to +/-pi after thetaOffset is applied + double theta = std::atan2(-input.x, input.y) - rotation; + theta = MathUtil::wrapAngle(theta + pi) - pi; // Move branch cut to +/-pi after angle offset is applied double regionCenterTheta = std::round(theta * nSides / twoPi) / nSides * twoPi; double localTheta = theta - regionCenterTheta; double dist = r * std::cos(localTheta); - double newDist = juce::jmax(0.0, (std::round(dist / stripeSize - rOffset) + rOffset) * stripeSize); + double newDist = juce::jmax(0.0, (std::round(dist / stripeSize - stripePhase) + stripePhase) * stripeSize); double scale = newDist / dist; output.x = scale * input.x; output.y = scale * input.y; @@ -33,7 +33,7 @@ public: double absZ = std::abs(input.z); if (absZ > 0.0001) { double signZ = input.z > 0 ? 1 : -1; - output.z = signZ * juce::jmax(0.0, (std::round(absZ / stripeSize - rOffset) + rOffset) * stripeSize); + output.z = signZ * juce::jmax(0.0, (std::round(absZ / stripeSize - stripePhase) + stripePhase) * stripeSize); } return (1 - effectScale) * input + effectScale * output; } @@ -42,19 +42,20 @@ public: auto eff = std::make_shared( std::make_shared(), std::vector{ - new osci::EffectParameter("Polygon Bit Crush", - "Constrains points to a polygon pattern.", - "polygonBitCrushEnable", VERSION_HINT, 1.0, 0.0, 1.0), + new osci::EffectParameter("Polygon Bit Crush", + "Constrains points to a polygon pattern.", + "polygonBitCrush", VERSION_HINT, 1.0, 0.0, 1.0), new osci::EffectParameter("Sides", "Controls the number of sides of the polygon pattern.", - "polygonSides", VERSION_HINT, 5.0, 3.0, 8.0), + "polygonBitCrushSides", VERSION_HINT, 5.0, 3.0, 8.0), new osci::EffectParameter("Stripe Size", "Controls the spacing between the stripes of the polygon pattern.", - "polygonBandSize", VERSION_HINT, 0.15, 0.0, 0.5), - new osci::EffectParameter("Angle Offset", "Rotates the polygon pattern.", - "polygonAngleOffset", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 0.1), - new osci::EffectParameter("Radial Offset", "Offsets the stripes of the polygon pattern.", - "polygonROffset", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 2.0) - }); + "polygonBitCrushStripeSize", VERSION_HINT, 0.5, 0.0, 1.0), + new osci::EffectParameter("Rotation", "Rotates the polygon pattern.", + "polygonBitCrushRotation", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 0.1), + new osci::EffectParameter("Stripe Phase", "Offsets the stripes of the polygon pattern.", + "polygonBitCrushStripePhase", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 2.0) + } + ); eff->setIcon(BinaryData::diamond_svg); return eff; } From 05ec83b0ccbc930b7623ba8ecb1fbe2f78fd22b5 Mon Sep 17 00:00:00 2001 From: James H Ball Date: Thu, 11 Sep 2025 22:20:44 +0100 Subject: [PATCH 5/5] Add icon --- Resources/svg/polygon_bitcrush.svg | 1 + Source/audio/PolygonBitCrushEffect.h | 2 +- osci-render.jucer | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 Resources/svg/polygon_bitcrush.svg diff --git a/Resources/svg/polygon_bitcrush.svg b/Resources/svg/polygon_bitcrush.svg new file mode 100644 index 00000000..38e9962f --- /dev/null +++ b/Resources/svg/polygon_bitcrush.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Source/audio/PolygonBitCrushEffect.h b/Source/audio/PolygonBitCrushEffect.h index b436537d..753add9b 100644 --- a/Source/audio/PolygonBitCrushEffect.h +++ b/Source/audio/PolygonBitCrushEffect.h @@ -56,7 +56,7 @@ public: "polygonBitCrushStripePhase", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 2.0) } ); - eff->setIcon(BinaryData::diamond_svg); + eff->setIcon(BinaryData::polygon_bitcrush_svg); return eff; } }; diff --git a/osci-render.jucer b/osci-render.jucer index 4d2b1a7f..6b462b7b 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -127,6 +127,8 @@ + @@ -914,4 +916,3 @@ -