From 5ff9fde8f911e94a22da009251c78285005417cd Mon Sep 17 00:00:00 2001 From: Anthony Hall Date: Sun, 7 Sep 2025 21:11:29 -0700 Subject: [PATCH] Harmonic dupe integer freq, kaleidoscope boundary fix --- Source/audio/HarmonicDuplicatorEffect.h | 25 +++++++++++++++++-------- Source/audio/KaleidoscopeEffect.h | 6 +++--- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Source/audio/HarmonicDuplicatorEffect.h b/Source/audio/HarmonicDuplicatorEffect.h index 5c3f50d9..7b2bfa67 100644 --- a/Source/audio/HarmonicDuplicatorEffect.h +++ b/Source/audio/HarmonicDuplicatorEffect.h @@ -9,29 +9,38 @@ public: 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 dist = juce::jlimit(0.0, 1.0, values[1].load()); + double spread = juce::jlimit(0.0, 1.0, values[1].load()); double angleOffset = values[2].load() * juce::MathConstants::twoPi; + // Ensure values extremely close to integer don't get rounded up + double fractionalPart = copies - std::floor(copies); + double ceilCopies = fractionalPart > 1e-4 ? std::ceil(copies) : std::floor(copies); + double theta = std::floor(framePhase * copies) / copies * twoPi + angleOffset; osci::Point offset(std::cos(theta), std::sin(theta), 0.0); - framePhase += audioProcessor.frequency / copies / sampleRate; + framePhase += audioProcessor.frequency / ceilCopies / sampleRate; framePhase = framePhase - std::floor(framePhase); - return (1 - dist) * input + dist * offset; + return (1 - spread) * input + spread * offset; } std::shared_ptr build() const override { auto eff = std::make_shared( std::make_shared(audioProcessor), std::vector{ - // TODO ID strings - new osci::EffectParameter("Copies", "Controls the number of copies of the input shape to draw.", "rdCopies", VERSION_HINT, 3.0, 1.0, 10.0), - new osci::EffectParameter("Distance", "Controls the distance between copies of the input shape.", "rdMix", VERSION_HINT, 0.5, 0.0, 1.0), - new osci::EffectParameter("Angle Offset", "Rotates the offsets between copies without rotating the input shape.", "rdAngle", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 1.0) + new osci::EffectParameter("Copies", + "Controls the number of copies of the input shape to draw. Splitting the shape into multiple copies creates audible harmony.", + "harmonicDuplicatorCopies", VERSION_HINT, 3.0, 1.0, 6.0), + new osci::EffectParameter("Spread", + "Controls the spread between copies of the input shape.", + "harmonicDuplicatorSpread", VERSION_HINT, 0.4, 0.0, 1.0), + new osci::EffectParameter("Angle Offset", + "Rotates the offsets between copies without rotating the input shape.", + "harmonicDuplicatorAngle", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001, osci::LfoType::Sawtooth, 0.2) } ); - eff->setName("Radial Duplicator"); + eff->setName("Harmonic Duplicator"); eff->setIcon(BinaryData::kaleidoscope_svg); return eff; } diff --git a/Source/audio/KaleidoscopeEffect.h b/Source/audio/KaleidoscopeEffect.h index 2ac29c5a..be76c0bb 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-4 ? 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-4) ? fractionalPart : 1.0; double wedgeAngle = baseWedgeAngle * partialScale; // Normalize theta to [0,1) for compression