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/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 08033ef1..6d2679de 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -18,6 +18,7 @@ #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" @@ -50,6 +51,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()); 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/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/osci-render.jucer b/osci-render.jucer index f0ae7245..49224489 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -95,6 +95,7 @@ + @@ -167,6 +168,8 @@ +