diff --git a/Resources/lua/hypercube.lua b/Resources/lua/hypercube.lua
index 211c2bd2..259b5054 100644
--- a/Resources/lua/hypercube.lua
+++ b/Resources/lua/hypercube.lua
@@ -1,5 +1,6 @@
local SCALE = 1.0
local ANIMATION_SPEED = 1.0
+local FOV_4D = math.rad(80)
local function rotate4D_XY(x, y, z, w, angle)
return x * math.cos(angle) - y * math.sin(angle),
@@ -23,8 +24,9 @@ local function rotate4D_XW(x, y, z, w, angle)
end
local function project4Dto3D(x, y, z, w)
- local w_factor = 1 / (3 - w)
- return x * w_factor, y * w_factor, z * w_factor
+ local camera_w = -1 / math.sin(0.5 * FOV_4D)
+ local scale = 1 / (w - camera_w) / math.tan(0.5 * FOV_4D)
+ return x * scale, y * scale, z * scale
end
local vertices = {
@@ -36,36 +38,37 @@ local vertices = {
{-1, -1, 1, 1}, {1, -1, 1, 1}, {1, 1, 1, 1}, {-1, 1, 1, 1}
}
-local edges = {
- -- Inner cube edges
- {1, 2}, {2, 3}, {3, 4}, {4, 1},
- {5, 6}, {6, 7}, {7, 8}, {8, 5},
- {1, 5}, {2, 6}, {3, 7}, {4, 8},
- -- Outer cube edges
- {9, 10}, {10, 11}, {11, 12}, {12, 9},
- {13, 14}, {14, 15}, {15, 16}, {16, 13},
- {9, 13}, {10, 14}, {11, 15}, {12, 16},
- -- Connections between cubes
- {1, 9}, {2, 10}, {3, 11}, {4, 12},
- {5, 13}, {6, 14}, {7, 15}, {8, 16}
+-- Eulerian cycle through vertices
+local path = {
+ 1, 5, 8, 4, 12, 16, 15, 11,
+ 3, 7, 6, 14, 15, 7, 8, 16,
+ 13, 5, 6, 2, 10, 9, 13, 14,
+ 10, 11, 12, 9, 1, 2, 3, 4
}
-local NUM_EDGES = #edges
+local PATH_LENGTH = #path
-local railroad_switch = NUM_EDGES * phase / (2 * math.pi)
-local current_edge = math.floor(railroad_switch) + 1
+local railroad_switch = PATH_LENGTH * phase / (2 * math.pi)
+local current_vertex = math.floor(railroad_switch) + 1
+local next_vertex = current_vertex % PATH_LENGTH + 1
local edge_phase = railroad_switch % 1
local time = step/sample_rate * ANIMATION_SPEED
-if current_edge <= NUM_EDGES then
- local v1 = vertices[edges[current_edge][1]]
- local v2 = vertices[edges[current_edge][2]]
+if current_vertex <= PATH_LENGTH then
+ local v1 = vertices[path[current_vertex]]
+ local v2 = vertices[path[next_vertex]]
local x = v1[1] + (v2[1] - v1[1]) * edge_phase
local y = v1[2] + (v2[2] - v1[2]) * edge_phase
local z = v1[3] + (v2[3] - v1[3]) * edge_phase
local w = v1[4] + (v2[4] - v1[4]) * edge_phase
+
+ -- Normalize
+ x = x * 0.5
+ y = y * 0.5
+ z = z * 0.5
+ w = w * 0.5
local fold_angle = math.sin(time) * math.pi / 2
@@ -82,4 +85,4 @@ if current_edge <= NUM_EDGES then
return {x, y, z}
end
-return {0, 0}
\ No newline at end of file
+return {0, 0}
diff --git a/Resources/lua/mushroom.lua b/Resources/lua/mushroom.lua
index a28d5335..dc56c211 100644
--- a/Resources/lua/mushroom.lua
+++ b/Resources/lua/mushroom.lua
@@ -1,9 +1,8 @@
-local X_SIZE = 0.35
+local LOOPS = 30
+local CAP_WIDTH = 0.35
local STEM_WIDTH = 0.05
-local Y_SIZE = 0.02
local HEIGHT = 2.0
local OFFSET_Y = -1.0
-local LOOPS = 50
local SHROOMS = 3 -- can be 1-4
local MOVE_AMOUNT = 0.4
@@ -13,27 +12,34 @@ local SPREAD = 0.2
local railroad_switch = SHROOMS * phase / (2 * math.pi)
local drawing_phase = (phase * SHROOMS) % (2 * math.pi)
-local x = X_SIZE * math.cos(LOOPS * drawing_phase)
-local y = Y_SIZE * math.sin(LOOPS * drawing_phase)
+local x = CAP_WIDTH * math.cos(LOOPS * drawing_phase)
+local z = CAP_WIDTH * math.sin(LOOPS * drawing_phase)
local sawtooth = (drawing_phase % (2 * math.pi)) / (2 * math.pi)
-y = y + (HEIGHT * sawtooth) + OFFSET_Y
+local y = HEIGHT * sawtooth + OFFSET_Y
local sine_mod = math.sin(2 * math.pi * sawtooth)
if sawtooth < 0.75 then
- sine_mod = 1
- x = x * (STEM_WIDTH / X_SIZE)
+ sine_mod = STEM_WIDTH / CAP_WIDTH
end
x = x * sine_mod
+z = z * sine_mod
local base_time = step/sample_rate * MOVE_FREQ
local current_mushroom = math.floor(railroad_switch)
local phase_offset = current_mushroom / SHROOMS
-local wiggle = math.sin(2 * math.pi * (sawtooth + base_time + phase_offset)) * sawtooth
-local outer_drift = SPREAD * sawtooth * wiggle
-x = x + (MOVE_AMOUNT * wiggle + outer_drift)
+local wiggle_x = math.cos(2 * math.pi * (sawtooth + base_time + phase_offset)) * sawtooth
+local wiggle_z = math.sin(2 * math.pi * (sawtooth + base_time + phase_offset)) * sawtooth
+local outer_drift_x = SPREAD * sawtooth * wiggle_x
+local outer_drift_z = SPREAD * sawtooth * wiggle_z
+x = x + (MOVE_AMOUNT * wiggle_x + outer_drift_x)
+z = z + (MOVE_AMOUNT * wiggle_z + outer_drift_z)
-return {x, y}
\ No newline at end of file
+-- normalize for default HEIGHT and OFFSET_Y
+local max_xz = math.abs(MOVE_AMOUNT) + math.abs(SPREAD)
+local scale = 1 / math.sqrt(1 + max_xz * max_xz)
+
+return {x * scale, y * scale, z * scale}
diff --git a/Resources/lua/spiral.lua b/Resources/lua/spiral.lua
index 9ed752fc..f1b66f2d 100644
--- a/Resources/lua/spiral.lua
+++ b/Resources/lua/spiral.lua
@@ -34,7 +34,7 @@ dir = dir or 1
t = t or 0
-- This is the correct increment for t to use such
-- that we hear the right frequency.
-increment = 2 * math.pi * frequency / sample_rate
+increment = 4 * math.pi * frequency / sample_rate
-- If we get to the end of the spiral, flip the
-- direction to go back.
@@ -51,4 +51,4 @@ t = t + dir * increment
return {
0.1 * t * math.sin(spiral_length * t),
0.1 * t * math.cos(spiral_length * t)
-}
\ No newline at end of file
+}
diff --git a/Resources/svg/duplicator.svg b/Resources/svg/duplicator.svg
new file mode 100644
index 00000000..b7482d2d
--- /dev/null
+++ b/Resources/svg/duplicator.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/god_ray.svg b/Resources/svg/god_ray.svg
new file mode 100644
index 00000000..19d69788
--- /dev/null
+++ b/Resources/svg/god_ray.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
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 0950dd5a..eca28158 100644
--- a/Source/PluginProcessor.cpp
+++ b/Source/PluginProcessor.cpp
@@ -12,11 +12,13 @@
#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"
#include "audio/SmoothEffect.h"
#include "audio/WobbleEffect.h"
+#include "audio/DuplicatorEffect.h"
#include "audio/DashedLineEffect.h"
#include "audio/VectorCancellingEffect.h"
#include "audio/ScaleEffect.h"
@@ -27,6 +29,7 @@
#include "audio/BounceEffect.h"
#include "audio/SkewEffect.h"
#include "audio/RadialWrapEffect.h"
+#include "audio/GodRayEffect.h"
#include "parser/FileParser.h"
#include "parser/FrameProducer.h"
@@ -50,6 +53,7 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse
toggleableEffects.push_back(DashedLineEffect(*this).build());
toggleableEffects.push_back(TraceEffect(*this).build());
toggleableEffects.push_back(WobbleEffect(*this).build());
+ toggleableEffects.push_back(DuplicatorEffect(*this).build());
#if OSCI_PREMIUM
toggleableEffects.push_back(MultiplexEffect(*this).build());
@@ -58,6 +62,8 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse
toggleableEffects.push_back(TwistEffect().build());
toggleableEffects.push_back(SkewEffect().build());
toggleableEffects.push_back(RadialWrapEffect().build());
+ toggleableEffects.push_back(GodRayEffect().build());
+ toggleableEffects.push_back(SpiralBitCrushEffect().build());
#endif
auto scaleEffect = ScaleEffectApp().build();
diff --git a/Source/audio/DuplicatorEffect.h b/Source/audio/DuplicatorEffect.h
new file mode 100644
index 00000000..f516f924
--- /dev/null
+++ b/Source/audio/DuplicatorEffect.h
@@ -0,0 +1,49 @@
+#pragma once
+#include
+#include "../PluginProcessor.h"
+
+class DuplicatorEffect : public osci::EffectApplication {
+public:
+ DuplicatorEffect(OscirenderAudioProcessor& p) : audioProcessor(p) {}
+
+ osci::Point apply(int index, osci::Point input, const std::vector>& values, double sampleRate) override {
+ const double twoPi = juce::MathConstants::twoPi;
+ double copies = juce::jmax(1.0, values[0].load());
+ double spread = juce::jlimit(0.0, 1.0, values[1].load());
+ double angleOffset = values[2].load() * juce::MathConstants::twoPi;
+
+ // Offset moves each time the input shape is drawn once
+ double theta = std::floor(framePhase * copies) / copies * twoPi + angleOffset;
+ osci::Point offset(std::cos(theta), std::sin(theta), 0.0);
+
+ double freqDivisor = std::ceil(copies - 1e-3);
+ framePhase += audioProcessor.frequency / freqDivisor / sampleRate;
+ framePhase = framePhase - std::floor(framePhase);
+
+ return (1 - spread) * input + spread * offset;
+ }
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(audioProcessor),
+ std::vector{
+ new osci::EffectParameter("Duplicator Copies",
+ "Controls the number of copies of the input shape to draw. Splitting the shape into multiple copies creates audible harmony.",
+ "duplicatorCopies", VERSION_HINT, 3.0, 1.0, 6.0),
+ new osci::EffectParameter("Spread",
+ "Controls the spread between copies of the input shape.",
+ "duplicatorSpread", VERSION_HINT, 0.4, 0.0, 1.0),
+ new osci::EffectParameter("Angle Offset",
+ "Rotates the offsets between copies without rotating the input shape.",
+ "duplicatorAngle", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 0.1)
+ }
+ );
+ eff->setName("Duplicator");
+ eff->setIcon(BinaryData::duplicator_svg);
+ return eff;
+ }
+
+private:
+ OscirenderAudioProcessor &audioProcessor;
+ double framePhase = 0.0; // [0, 1]
+};
diff --git a/Source/audio/GodRayEffect.h b/Source/audio/GodRayEffect.h
new file mode 100644
index 00000000..c035235c
--- /dev/null
+++ b/Source/audio/GodRayEffect.h
@@ -0,0 +1,37 @@
+#pragma once
+#include
+
+class GodRayEffect : public osci::EffectApplication {
+public:
+ osci::Point apply(int index, osci::Point input, const std::vector> &values, double sampleRate) override {
+ double noiseAmp = juce::jmax(0.0, values[0].load());
+ double bias = values[1];
+ double biasExponent = std::pow(12.0, std::abs(bias));
+
+ double noise = (double)std::rand() / RAND_MAX;
+ // Bias values toward 0 or 1 based on sign
+ if (bias > 0.0) {
+ noise = std::pow(noise, biasExponent);
+ }
+ else {
+ noise = 1 - std::pow(1 - noise, biasExponent);
+ }
+ double scale = (1 - noiseAmp) + noise * noiseAmp;
+ return input * scale;
+ }
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ std::vector{
+ new osci::EffectParameter("God Ray Strength",
+ "Creates a god ray effect by adding noise. This slider controls the size of the rays. Looks best with higher sample rates.",
+ "godRayStrength", VERSION_HINT, 0.5, 0.0, 1.0),
+ new osci::EffectParameter("God Ray Position",
+ "Controls whether the rays appear to be radiating inward or outward.",
+ "godRayPosition", VERSION_HINT, 0.8, -1.0, 1.0)
+ });
+ eff->setIcon(BinaryData::god_ray_svg);
+ return eff;
+ }
+};
\ No newline at end of file
diff --git a/Source/audio/KaleidoscopeEffect.h b/Source/audio/KaleidoscopeEffect.h
index 2ac29c5a..7d662b41 100644
--- a/Source/audio/KaleidoscopeEffect.h
+++ b/Source/audio/KaleidoscopeEffect.h
@@ -22,18 +22,18 @@ public:
int fullSegments = (int)std::floor(segments);
double fractionalPart = segments - fullSegments; // in [0,1)
+ fullSegments = fractionalPart > 1e-3 ? fullSegments : fullSegments - 1;
phase = nextPhase(audioProcessor.frequency / (fullSegments + 1), sampleRate) / (2.0 * std::numbers::pi);
// Use 'segments' for timing so partial segment gets proportionally shorter time.
double currentSegmentFloat = phase * segments; // [0, segments)
int currentSegmentIndex = (int)std::floor(currentSegmentFloat);
- int maxIndex = fractionalPart > 1e-9 ? fullSegments : fullSegments - 1; // include partial index if exists
- if (currentSegmentIndex > maxIndex) currentSegmentIndex = maxIndex; // safety
+ if (currentSegmentIndex > fullSegments) currentSegmentIndex = fullSegments; // safety
// Base full wedge angle (all full wedges) and size of partial wedge
double baseWedgeAngle = juce::MathConstants::twoPi / segments; // size of a "unit" wedge
- double partialScale = (currentSegmentIndex == fullSegments && fractionalPart > 1e-9) ? fractionalPart : 1.0;
+ double partialScale = (currentSegmentIndex == fullSegments && fractionalPart > 1e-3) ? fractionalPart : 1.0;
double wedgeAngle = baseWedgeAngle * partialScale;
// Normalize theta to [0,1) for compression
diff --git a/Source/audio/MultiplexEffect.h b/Source/audio/MultiplexEffect.h
index 4bef9f91..fdb9328b 100644
--- a/Source/audio/MultiplexEffect.h
+++ b/Source/audio/MultiplexEffect.h
@@ -11,9 +11,9 @@ public:
osci::Point apply(int index, osci::Point input, const std::vector>& values, double sampleRate) override {
jassert(values.size() == 5);
- double gridX = values[0].load() + 0.0001;
- double gridY = values[1].load() + 0.0001;
- double gridZ = values[2].load() + 0.0001;
+ double gridX = values[0].load();
+ double gridY = values[1].load();
+ double gridZ = values[2].load();
double interpolation = values[3].load();
double gridDelay = values[4].load();
@@ -24,7 +24,9 @@ public:
buffer[head] = input;
osci::Point grid = osci::Point(gridX, gridY, gridZ);
- osci::Point gridFloor = osci::Point(std::floor(gridX), std::floor(gridY), std::floor(gridZ));
+ osci::Point gridFloor = osci::Point(std::floor(gridX + 1e-3),
+ std::floor(gridY + 1e-3),
+ std::floor(gridZ + 1e-3));
gridFloor.x = std::max(gridFloor.x, 1.0);
gridFloor.y = std::max(gridFloor.y, 1.0);
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 1a8c6714..33fb14f1 100644
--- a/osci-render.jucer
+++ b/osci-render.jucer
@@ -95,9 +95,11 @@
+
+
@@ -141,6 +143,8 @@
+
@@ -168,7 +172,12 @@
+
+
+
@@ -845,7 +854,7 @@
-
+