diff --git a/Resources/svg/bit-crush.svg b/Resources/svg/bit-crush.svg
new file mode 100644
index 0000000..ca26ce1
--- /dev/null
+++ b/Resources/svg/bit-crush.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/bounce.svg b/Resources/svg/bounce.svg
new file mode 100644
index 0000000..c68238b
--- /dev/null
+++ b/Resources/svg/bounce.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/bulge.svg b/Resources/svg/bulge.svg
new file mode 100644
index 0000000..b1104a2
--- /dev/null
+++ b/Resources/svg/bulge.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/close.svg b/Resources/svg/close.svg
new file mode 100644
index 0000000..096198c
--- /dev/null
+++ b/Resources/svg/close.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/dash.svg b/Resources/svg/dash.svg
new file mode 100644
index 0000000..dd789d2
--- /dev/null
+++ b/Resources/svg/dash.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/delay.svg b/Resources/svg/delay.svg
new file mode 100644
index 0000000..c45a2d8
--- /dev/null
+++ b/Resources/svg/delay.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/distort.svg b/Resources/svg/distort.svg
new file mode 100644
index 0000000..e50f267
--- /dev/null
+++ b/Resources/svg/distort.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/kaleidoscope.svg b/Resources/svg/kaleidoscope.svg
new file mode 100644
index 0000000..fb970f0
--- /dev/null
+++ b/Resources/svg/kaleidoscope.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/Resources/svg/lua.svg b/Resources/svg/lua.svg
new file mode 100644
index 0000000..895ffbf
--- /dev/null
+++ b/Resources/svg/lua.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/multiplex.svg b/Resources/svg/multiplex.svg
new file mode 100644
index 0000000..3874d78
--- /dev/null
+++ b/Resources/svg/multiplex.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/ripple.svg b/Resources/svg/ripple.svg
new file mode 100644
index 0000000..2a9c193
--- /dev/null
+++ b/Resources/svg/ripple.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/rotate.svg b/Resources/svg/rotate.svg
new file mode 100644
index 0000000..ef55c71
--- /dev/null
+++ b/Resources/svg/rotate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/scale.svg b/Resources/svg/scale.svg
new file mode 100644
index 0000000..1256369
--- /dev/null
+++ b/Resources/svg/scale.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/smoothing.svg b/Resources/svg/smoothing.svg
new file mode 100644
index 0000000..c3278a7
--- /dev/null
+++ b/Resources/svg/smoothing.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/swirl.svg b/Resources/svg/swirl.svg
new file mode 100644
index 0000000..31d8cda
--- /dev/null
+++ b/Resources/svg/swirl.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/Resources/svg/trace.svg b/Resources/svg/trace.svg
new file mode 100644
index 0000000..e7c85c8
--- /dev/null
+++ b/Resources/svg/trace.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/translate.svg b/Resources/svg/translate.svg
new file mode 100644
index 0000000..b59c7a0
--- /dev/null
+++ b/Resources/svg/translate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/twist.svg b/Resources/svg/twist.svg
new file mode 100644
index 0000000..36d697b
--- /dev/null
+++ b/Resources/svg/twist.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/vector-cancelling.svg b/Resources/svg/vector-cancelling.svg
new file mode 100644
index 0000000..3b48420
--- /dev/null
+++ b/Resources/svg/vector-cancelling.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/svg/wobble.svg b/Resources/svg/wobble.svg
new file mode 100644
index 0000000..aa7f4f6
--- /dev/null
+++ b/Resources/svg/wobble.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Source/CommonPluginEditor.cpp b/Source/CommonPluginEditor.cpp
index f7ecdf1..af2e605 100644
--- a/Source/CommonPluginEditor.cpp
+++ b/Source/CommonPluginEditor.cpp
@@ -60,7 +60,7 @@ CommonPluginEditor::CommonPluginEditor(CommonAudioProcessor& p, juce::String app
setResizeLimits(250, 250, 999999, 999999);
tooltipDropShadow.setOwner(&tooltipWindow.get());
- tooltipWindow->setMillisecondsBeforeTipAppears(0);
+ tooltipWindow->setMillisecondsBeforeTipAppears(100);
updateTitle();
diff --git a/Source/EffectPluginEditor.cpp b/Source/EffectPluginEditor.cpp
index d5aada0..71a4b76 100644
--- a/Source/EffectPluginEditor.cpp
+++ b/Source/EffectPluginEditor.cpp
@@ -32,7 +32,7 @@ EffectPluginEditor::EffectPluginEditor(EffectAudioProcessor& p)
setResizable(false, false);
tooltipDropShadow.setOwner(&tooltipWindow.get());
- tooltipWindow->setMillisecondsBeforeTipAppears(0);
+ tooltipWindow->setMillisecondsBeforeTipAppears(100);
audioProcessor.bitCrush->addListener(0, this);
}
diff --git a/Source/EffectPluginProcessor.h b/Source/EffectPluginProcessor.h
index c1f6e6d..73bbfeb 100644
--- a/Source/EffectPluginProcessor.h
+++ b/Source/EffectPluginProcessor.h
@@ -40,19 +40,9 @@ public:
int getCurrentProgram() override;
void setCurrentProgram(int index) override;
const juce::String getProgramName(int index) override;
- void changeProgramName(int index, const juce::String& newName) override; std::shared_ptr bitCrush = std::make_shared(
- std::make_shared(),
- new osci::EffectParameter("Bit Crush", "Limits the resolution of points drawn to the screen, making the object look pixelated, and making the audio sound more 'digital' and distorted.", "bitCrush", VERSION_HINT, 0.0, 0.0, 1.0)
- );
+ void changeProgramName(int index, const juce::String& newName) override; std::shared_ptr bitCrush = BitCrushEffect().build();
- std::shared_ptr autoGain = std::make_shared(
- std::make_shared(),
- std::vector{
- new osci::EffectParameter("Intensity", "Controls how aggressively the gain adjustment is applied", "agcIntensity", VERSION_HINT, 1.0, 0.0, 1.0),
- new osci::EffectParameter("Target Level", "Target output level for the automatic gain control", "agcTarget", VERSION_HINT, 0.6, 0.0, 1.0),
- new osci::EffectParameter("Response", "How quickly the effect responds to level changes (lower is slower)", "agcResponse", VERSION_HINT, 0.0001, 0.0, 1.0)
- }
- );
+ std::shared_ptr autoGain = AutoGainControlEffect().build();
VisualiserParameters visualiserParameters;
diff --git a/Source/EffectsComponent.cpp b/Source/EffectsComponent.cpp
index 69b9148..0ae8fe4 100644
--- a/Source/EffectsComponent.cpp
+++ b/Source/EffectsComponent.cpp
@@ -2,7 +2,8 @@
#include "audio/BitCrushEffect.h"
#include "PluginEditor.h"
-EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), itemData(p, editor), listBoxModel(listBox, itemData) {
+EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor)
+ : audioProcessor(p), itemData(p, editor), listBoxModel(listBox, itemData), grid(p) {
setText("Audio Effects");
addAndMakeVisible(frequency);
@@ -11,14 +12,6 @@ EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioP
frequency.slider.setTextValueSuffix("Hz");
frequency.slider.setValue(audioProcessor.frequencyEffect->getValue(), juce::dontSendNotification);
- /*addBtn.setButtonText("Add Item...");
- addBtn.onClick = [this]()
- {
- itemData.data.push_back(juce::String("Item " + juce::String(1 + itemData.getNumItems())));
- listBox.updateContent();
- };
- addAndMakeVisible(addBtn);*/
-
addAndMakeVisible(randomiseButton);
randomiseButton.setTooltip("Randomise all effect parameter values, randomise which effects are enabled, and randomise their order.");
@@ -33,8 +26,93 @@ EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioP
audioProcessor.broadcaster.addChangeListener(this);
}
+ // Wire list model to notify when user wants to add
+ itemData.onAddNewEffectRequested = [this]() {
+ showingGrid = true;
+ grid.setVisible(true);
+ grid.refreshDisabledStates();
+ listBox.setVisible(false);
+ resized();
+ repaint();
+ };
+
+ // Decide initial view: show grid only if there are no selected effects
+ bool anySelected = false;
+ {
+ juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
+ for (const auto& eff : audioProcessor.toggleableEffects) {
+ const bool isSelected = (eff->selected == nullptr) ? true : eff->selected->getBoolValue();
+ if (isSelected) { anySelected = true; break; }
+ }
+ }
+ showingGrid = !anySelected;
+ grid.onEffectSelected = [this](const juce::String& effectId) {
+ {
+ juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
+ // Mark the chosen effect as selected and enabled, and move it to the end
+ std::shared_ptr chosen;
+ for (auto& eff : audioProcessor.toggleableEffects) {
+ if (eff->getId() == effectId) {
+ eff->selected->setBoolValueNotifyingHost(true);
+ eff->enabled->setBoolValueNotifyingHost(true);
+ chosen = eff;
+ break;
+ }
+ }
+ // Place chosen effect at the end of the visible (selected) list and update precedence
+ if (chosen != nullptr) {
+ int idx = 0;
+ for (auto& e : itemData.data) {
+ if (e != chosen) e->setPrecedence(idx++);
+ }
+ chosen->setPrecedence(idx++);
+ audioProcessor.updateEffectPrecedence();
+ }
+ }
+ // Refresh list content to include newly selected
+ itemData.resetData();
+ listBox.updateContent();
+ showingGrid = false;
+ listBox.setVisible(true);
+ grid.setVisible(false);
+ resized();
+ repaint();
+ };
+ grid.onCanceled = [this]() {
+ // If canceled while default grid, just show list
+ showingGrid = false;
+ listBox.setVisible(true);
+ grid.setVisible(false);
+ resized();
+ repaint();
+ };
+
listBox.setModel(&listBoxModel);
addAndMakeVisible(listBox);
+ // Add a small top spacer so the drop indicator can be visible above the first row
+ {
+ auto spacer = std::make_unique();
+ spacer->setSize(1, LIST_SPACER); // top padding
+ listBox.setHeaderComponent(std::move(spacer));
+ }
+ // Setup scroll fade mixin
+ initScrollFade(*this);
+ attachToListBox(listBox);
+ // Wire "+ Add new effect" button below the list
+ addEffectButton.onClick = [this]() {
+ if (itemData.onAddNewEffectRequested) itemData.onAddNewEffectRequested();
+ };
+ addAndMakeVisible(addEffectButton);
+ addAndMakeVisible(grid);
+ // Keep disabled states in sync whenever grid is shown
+ if (showingGrid) {
+ grid.setVisible(true);
+ grid.refreshDisabledStates();
+ listBox.setVisible(false);
+ } else {
+ grid.setVisible(false);
+ listBox.setVisible(true);
+ }
}
EffectsComponent::~EffectsComponent() {
@@ -52,10 +130,28 @@ void EffectsComponent::resized() {
frequency.setBounds(area.removeFromTop(30));
area.removeFromTop(6);
- listBox.setBounds(area);
+ if (showingGrid) {
+ grid.setBounds(area);
+ addEffectButton.setVisible(false);
+ // Hide fade when grid is shown
+ setScrollFadeVisible(false);
+ } else {
+ // Reserve space at bottom for the add button
+ auto addBtnHeight = 44;
+ auto listArea = area;
+ auto buttonArea = listArea.removeFromBottom(addBtnHeight);
+ listBox.setBounds(listArea);
+ // Layout bottom fade overlay; visible if list is scrollable
+ layoutScrollFade(listArea.withTrimmedTop(LIST_SPACER), true, 48);
+ addEffectButton.setVisible(true);
+ addEffectButton.setBounds(buttonArea.reduced(0, 4));
+ }
}
void EffectsComponent::changeListenerCallback(juce::ChangeBroadcaster* source) {
itemData.resetData();
listBox.updateContent();
+ // Re-layout scroll fades after content changes
+ if (! showingGrid)
+ layoutScrollFade(listBox.getBounds().withTrimmedTop(LIST_SPACER), true, 48);
}
diff --git a/Source/EffectsComponent.h b/Source/EffectsComponent.h
index 16c917f..18d7cd1 100644
--- a/Source/EffectsComponent.h
+++ b/Source/EffectsComponent.h
@@ -6,9 +6,11 @@
#include "PluginProcessor.h"
#include "components/DraggableListBox.h"
#include "components/EffectsListComponent.h"
+#include "components/ScrollFadeMixin.h"
+#include "components/EffectTypeGridComponent.h"
class OscirenderAudioProcessorEditor;
-class EffectsComponent : public juce::GroupComponent, public juce::ChangeListener {
+class EffectsComponent : public juce::GroupComponent, public juce::ChangeListener, private ScrollFadeMixin {
public:
EffectsComponent(OscirenderAudioProcessor&, OscirenderAudioProcessorEditor&);
~EffectsComponent() override;
@@ -26,6 +28,11 @@ private:
AudioEffectListBoxItemData itemData;
EffectsListBoxModel listBoxModel;
DraggableListBox listBox;
+ juce::TextButton addEffectButton { "+ Add new effect" }; // Separate button under the list
+ EffectTypeGridComponent grid { audioProcessor };
+ bool showingGrid = true; // show grid by default
+
+ const int LIST_SPACER = 4; // Space above the list to show drop indicator
EffectComponent frequency = EffectComponent(*audioProcessor.frequencyEffect, false);
diff --git a/Source/LookAndFeel.cpp b/Source/LookAndFeel.cpp
index 3e1c1a6..b16e212 100644
--- a/Source/LookAndFeel.cpp
+++ b/Source/LookAndFeel.cpp
@@ -24,7 +24,7 @@ OscirenderLookAndFeel::OscirenderLookAndFeel() {
setColour(juce::PopupMenu::backgroundColourId, Colours::darker);
setColour(juce::PopupMenu::highlightedBackgroundColourId, Colours::grey);
setColour(juce::TooltipWindow::backgroundColourId, Colours::darker);
- setColour(juce::TooltipWindow::outlineColourId, juce::Colours::white);
+ setColour(juce::TooltipWindow::outlineColourId, Colours::darker);
setColour(juce::TextButton::buttonOnColourId, Colours::darker);
setColour(juce::AlertWindow::outlineColourId, Colours::darker);
setColour(juce::AlertWindow::backgroundColourId, Colours::darker);
diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp
index c369d51..50b144b 100644
--- a/Source/PluginEditor.cpp
+++ b/Source/PluginEditor.cpp
@@ -17,7 +17,7 @@ void OscirenderAudioProcessorEditor::registerFileRemovedCallback() {
});
}
-OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioProcessor& p) : CommonPluginEditor(p, "osci-render", "osci", 1100, 750), audioProcessor(p), collapseButton("Collapse", juce::Colours::white, juce::Colours::white, juce::Colours::white) {
+OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioProcessor& p) : CommonPluginEditor(p, "osci-render", "osci", 1100, 770), audioProcessor(p), collapseButton("Collapse", juce::Colours::white, juce::Colours::white, juce::Colours::white) {
// Register the file removal callback
registerFileRemovedCallback();
diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp
index ad26577..e1730c5 100644
--- a/Source/PluginProcessor.cpp
+++ b/Source/PluginProcessor.cpp
@@ -13,11 +13,18 @@
#include "audio/BulgeEffect.h"
#include "audio/TwistEffect.h"
#include "audio/DistortEffect.h"
+#include "audio/KaleidoscopeEffect.h"
#include "audio/MultiplexEffect.h"
#include "audio/SmoothEffect.h"
#include "audio/WobbleEffect.h"
#include "audio/DashedLineEffect.h"
#include "audio/VectorCancellingEffect.h"
+#include "audio/ScaleEffect.h"
+#include "audio/RotateEffect.h"
+#include "audio/TranslateEffect.h"
+#include "audio/RippleEffect.h"
+#include "audio/SwirlEffect.h"
+#include "audio/BounceEffect.h"
#include "parser/FileParser.h"
#include "parser/FrameProducer.h"
@@ -29,138 +36,39 @@
OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(BusesProperties().withInput("Input", juce::AudioChannelSet::namedChannelSet(2), true).withOutput("Output", juce::AudioChannelSet::stereo(), true)) {
// locking isn't necessary here because we are in the constructor
- toggleableEffects.push_back(std::make_shared(
- std::make_shared(),
- new osci::EffectParameter("Bit Crush", "Limits the resolution of points drawn to the screen, making the object look pixelated, and making the audio sound more 'digital' and distorted.", "bitCrush", VERSION_HINT, 0.6, 0.0, 1.0)));
- toggleableEffects.push_back(std::make_shared(
- std::make_shared(),
- new osci::EffectParameter("Bulge", "Applies a bulge that makes the centre of the image larger, and squishes the edges of the image. This applies a distortion to the audio.", "bulge", VERSION_HINT, 0.5, 0.0, 1.0)));
- auto multiplexEffect = std::make_shared(
- std::make_shared(),
- std::vector{
- new osci::EffectParameter("Multiplex X", "Controls the horizontal grid size for the multiplex effect.", "multiplexGridX", VERSION_HINT, 1.0, 1.0, 8.0),
- new osci::EffectParameter("Multiplex Y", "Controls the vertical grid size for the multiplex effect.", "multiplexGridY", VERSION_HINT, 1.0, 1.0, 8.0),
- new osci::EffectParameter("Multiplex Z", "Controls the depth grid size for the multiplex effect.", "multiplexGridZ", VERSION_HINT, 1.0, 1.0, 8.0),
- new osci::EffectParameter("Multiplex Smooth", "Controls the smoothness of transitions between grid sizes.", "multiplexSmooth", VERSION_HINT, 0.0, 0.0, 1.0),
- new osci::EffectParameter("Multiplex Phase", "Controls the current phase of the multiplex grid animation.", "gridPhase", VERSION_HINT, 0.0, 0.0, 1.0),
- new osci::EffectParameter("Multiplex Delay", "Controls the delay of the audio samples used in the multiplex effect.", "gridDelay", VERSION_HINT, 0.0, 0.0, 1.0),
- });
- // Set up the Grid Phase parameter with sawtooth LFO at 100Hz
- multiplexEffect->getParameter("gridPhase")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth);
- multiplexEffect->getParameter("gridPhase")->lfoRate->setUnnormalisedValueNotifyingHost(100.0);
- toggleableEffects.push_back(multiplexEffect);
- toggleableEffects.push_back(std::make_shared(
- std::make_shared(),
- new osci::EffectParameter("Vector Cancelling", "Inverts the audio and image every few samples to 'cancel out' the audio, making the audio quiet, and distorting the image.", "vectorCancelling", VERSION_HINT, 0.1111111, 0.0, 1.0)));
- auto scaleEffect = std::make_shared(
- [this](int index, osci::Point input, const std::vector>& values, double sampleRate) {
- return input * osci::Point(values[0], values[1], values[2]);
- },
- std::vector{
- new osci::EffectParameter("Scale X", "Scales the object in the horizontal direction.", "scaleX", VERSION_HINT, 1.0, -3.0, 3.0),
- new osci::EffectParameter("Scale Y", "Scales the object in the vertical direction.", "scaleY", VERSION_HINT, 1.0, -3.0, 3.0),
- new osci::EffectParameter("Scale Z", "Scales the depth of the object.", "scaleZ", VERSION_HINT, 1.0, -3.0, 3.0),
- });
- scaleEffect->markLockable(true);
+ toggleableEffects.push_back(BitCrushEffect().build());
+ toggleableEffects.push_back(BulgeEffect().build());
+ toggleableEffects.push_back(MultiplexEffect().build());
+ toggleableEffects.push_back(KaleidoscopeEffect().build());
+ toggleableEffects.push_back(BounceEffect().build());
+ toggleableEffects.push_back(VectorCancellingEffect().build());
+ toggleableEffects.push_back(RippleEffectApp().build());
+ toggleableEffects.push_back(RotateEffectApp().build());
+ toggleableEffects.push_back(TranslateEffectApp().build());
+ toggleableEffects.push_back(SwirlEffectApp().build());
+ toggleableEffects.push_back(SmoothEffect().build());
+ toggleableEffects.push_back(TwistEffect().build());
+ toggleableEffects.push_back(DelayEffect().build());
+ toggleableEffects.push_back(DashedLineEffect(*this).build());
+ toggleableEffects.push_back(TraceEffect(*this).build());
+ toggleableEffects.push_back(WobbleEffect(*this).build());
+
+ auto scaleEffect = ScaleEffectApp().build();
booleanParameters.push_back(scaleEffect->linked);
toggleableEffects.push_back(scaleEffect);
- auto distortEffect = std::make_shared(
- [this](int index, osci::Point input, const std::vector>& values, double sampleRate) {
- int flip = index % 2 == 0 ? 1 : -1;
- osci::Point jitter = osci::Point(flip * values[0], flip * values[1], flip * values[2]);
- return input + jitter;
- },
- std::vector{
- new osci::EffectParameter("Distort X", "Distorts the image in the horizontal direction by jittering the audio sample being drawn.", "distortX", VERSION_HINT, 0.0, 0.0, 1.0),
- new osci::EffectParameter("Distort Y", "Distorts the image in the vertical direction by jittering the audio sample being drawn.", "distortY", VERSION_HINT, 0.0, 0.0, 1.0),
- new osci::EffectParameter("Distort Z", "Distorts the depth of the image by jittering the audio sample being drawn.", "distortZ", VERSION_HINT, 0.1, 0.0, 1.0),
- });
- distortEffect->markLockable(false);
+
+ auto distortEffect = DistortEffect().build();
booleanParameters.push_back(distortEffect->linked);
toggleableEffects.push_back(distortEffect);
- auto rippleEffect = std::make_shared(
- [this](int index, osci::Point input, const std::vector>& values, double sampleRate) {
- double phase = values[1] * std::numbers::pi;
- double distance = 100 * values[2] * (input.x * input.x + input.y * input.y);
- input.z += values[0] * std::sin(phase + distance);
- return input;
- },
- std::vector{
- new osci::EffectParameter("Ripple Depth", "Controls how large the ripples applied to the image are.", "rippleDepth", VERSION_HINT, 0.2, 0.0, 1.0),
- new osci::EffectParameter("Ripple Phase", "Controls the position of the ripple. Animate this to see a moving ripple effect.", "ripplePhase", VERSION_HINT, 0.0, -1.0, 1.0),
- new osci::EffectParameter("Ripple Amount", "Controls how many ripples are applied to the image.", "rippleAmount", VERSION_HINT, 0.1, 0.0, 1.0),
- });
- rippleEffect->getParameter("ripplePhase")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth);
- toggleableEffects.push_back(rippleEffect);
- auto rotateEffect = std::make_shared(
- [this](int index, osci::Point input, const std::vector>& values, double sampleRate) {
- input.rotate(values[0] * std::numbers::pi, values[1] * std::numbers::pi, values[2] * std::numbers::pi);
- return input;
- },
- std::vector{
- new osci::EffectParameter("Rotate X", "Controls the rotation of the object in the X axis.", "rotateX", VERSION_HINT, 0.0, -1.0, 1.0),
- new osci::EffectParameter("Rotate Y", "Controls the rotation of the object in the Y axis.", "rotateY", VERSION_HINT, 0.0, -1.0, 1.0),
- new osci::EffectParameter("Rotate Z", "Controls the rotation of the object in the Z axis.", "rotateZ", VERSION_HINT, 0.0, -1.0, 1.0),
- });
- rotateEffect->getParameter("rotateY")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth);
- rotateEffect->getParameter("rotateY")->lfoRate->setUnnormalisedValueNotifyingHost(0.2);
- toggleableEffects.push_back(rotateEffect);
- toggleableEffects.push_back(std::make_shared(
- [this](int index, osci::Point input, const std::vector>& values, double sampleRate) {
- return input + osci::Point(values[0], values[1], values[2]);
- },
- std::vector{
- new osci::EffectParameter("Translate X", "Moves the object horizontally.", "translateX", VERSION_HINT, 0.0, -1.0, 1.0),
- new osci::EffectParameter("Translate Y", "Moves the object vertically.", "translateY", VERSION_HINT, 0.0, -1.0, 1.0),
- new osci::EffectParameter("Translate Z", "Moves the object away from the camera.", "translateZ", VERSION_HINT, 0.0, -1.0, 1.0),
- }));
- toggleableEffects.push_back(std::make_shared(
- [this](int index, osci::Point input, const std::vector>& values, double sampleRate) {
- double length = 10 * values[0] * input.magnitude();
- double newX = input.x * std::cos(length) - input.y * std::sin(length);
- double newY = input.x * std::sin(length) + input.y * std::cos(length);
- return osci::Point(newX, newY, input.z);
- },
- std::vector{
- new osci::EffectParameter("Swirl", "Swirls the image in a spiral pattern.", "swirl", VERSION_HINT, 0.3, -1.0, 1.0),
- }));
- toggleableEffects.push_back(std::make_shared(
- std::make_shared(),
- std::vector{
- new osci::EffectParameter("Twist", "Twists the image in a corkscrew pattern.", "twist", VERSION_HINT, 0.5, -1.0, 1.0),
- }));
- toggleableEffects.push_back(std::make_shared(
- std::make_shared(),
- new osci::EffectParameter("Smoothing", "This works as a low-pass frequency filter that removes high frequencies, making the image look smoother, and audio sound less harsh.", "smoothing", VERSION_HINT, 0.75, 0.0, 1.0)));
- std::shared_ptr wobble = std::make_shared(
- std::make_shared(*this),
- std::vector{
- new osci::EffectParameter("Wobble Amount", "Adds a sine wave of the prominent frequency in the audio currently playing. The sine wave's frequency is slightly offset to create a subtle 'wobble' in the image. Increasing the slider increases the strength of the wobble.", "wobble", VERSION_HINT, 0.3, 0.0, 1.0),
- new osci::EffectParameter("Wobble Phase", "Controls the phase of the wobble.", "wobblePhase", VERSION_HINT, 0.0, -1.0, 1.0),
- });
- wobble->getParameter("wobblePhase")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth);
- toggleableEffects.push_back(wobble);
- toggleableEffects.push_back(std::make_shared(
- delayEffect,
- std::vector{
- new osci::EffectParameter("Delay Decay", "Adds repetitions, delays, or echos to the audio. This slider controls the volume of the echo.", "delayDecay", VERSION_HINT, 0.4, 0.0, 1.0),
- new osci::EffectParameter("Delay Length", "Controls the time in seconds between echos.", "delayLength", VERSION_HINT, 0.5, 0.0, 1.0)}));
- auto dashedLineEffect = std::make_shared(
- std::make_shared(*this),
- std::vector{
- new osci::EffectParameter("Dash Count", "Controls the number of dashed lines in the drawing.", "dashCount", VERSION_HINT, 16.0, 1.0, 32.0),
- new osci::EffectParameter("Dash Coverage", "Controls the fraction of each dash unit that is drawn.", "dashCoverage", VERSION_HINT, 0.5, 0.0, 1.0),
- new osci::EffectParameter("Dash Offset", "Offsets the location of the dashed lines.", "dashOffset", VERSION_HINT, 0.0, 0.0, 1.0),
- });
- dashedLineEffect->getParameter("dashOffset")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth);
- dashedLineEffect->getParameter("dashOffset")->lfoRate->setUnnormalisedValueNotifyingHost(1.0);
- toggleableEffects.push_back(dashedLineEffect);
+
+ custom->setIcon(BinaryData::lua_svg);
toggleableEffects.push_back(custom);
- toggleableEffects.push_back(trace);
- trace->getParameter("traceLength")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth);
for (int i = 0; i < toggleableEffects.size(); i++) {
auto effect = toggleableEffects[i];
+ effect->markSelectable(false);
+ booleanParameters.push_back(effect->selected);
+ effect->selected->setValueNotifyingHost(false);
effect->markEnableable(false);
booleanParameters.push_back(effect->enabled);
effect->enabled->setValueNotifyingHost(false);
@@ -175,6 +83,9 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse
for (int i = 0; i < 26; i++) {
addLuaSlider();
+ if (i < luaEffects.size()) {
+ luaEffects[i]->setIcon(BinaryData::lua_svg);
+ }
}
effects.insert(effects.end(), toggleableEffects.begin(), toggleableEffects.end());
@@ -467,6 +378,17 @@ void OscirenderAudioProcessor::setObjectServerPort(int port) {
objectServer.reload();
}
+void OscirenderAudioProcessor::setPreviewEffectId(const juce::String& effectId) {
+ previewEffect.reset();
+ for (auto& eff : toggleableEffects) {
+ if (eff->getId() == effectId) { previewEffect = eff; break; }
+ }
+}
+
+void OscirenderAudioProcessor::clearPreviewEffect() {
+ previewEffect.reset();
+}
+
void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) {
juce::ScopedNoDenormals noDenormals;
// Audio info variables
@@ -629,13 +551,25 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, ju
juce::SpinLock::ScopedLockType lock2(effectsLock);
if (volume > EPSILON) {
for (auto& effect : toggleableEffects) {
- if (effect->enabled->getValue()) {
+ bool isEnabled = effect->enabled != nullptr && effect->enabled->getValue();
+ bool isSelected = effect->selected == nullptr ? true : effect->selected->getBoolValue();
+ if (isEnabled && isSelected) {
if (effect->getId() == custom->getId()) {
effect->setExternalInput(osci::Point{ left, right });
}
channels = effect->apply(sample, channels, currentVolume);
}
}
+ // Apply preview effect if present and not already active in the main chain
+ if (previewEffect) {
+ const bool prevEnabled = (previewEffect->enabled != nullptr) && previewEffect->enabled->getValue();
+ const bool prevSelected = (previewEffect->selected == nullptr) ? true : previewEffect->selected->getBoolValue();
+ if (!(prevEnabled && prevSelected)) {
+ if (previewEffect->getId() == custom->getId())
+ previewEffect->setExternalInput(osci::Point{ left, right });
+ channels = previewEffect->apply(sample, channels, currentVolume);
+ }
+ }
}
for (auto& effect : permanentEffects) {
channels = effect->apply(sample, channels, currentVolume);
diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h
index eb65ab5..548ad64 100644
--- a/Source/PluginProcessor.h
+++ b/Source/PluginProcessor.h
@@ -8,8 +8,6 @@
#pragma once
-#define VERSION_HINT 2
-
#include
#include
@@ -59,6 +57,8 @@ public:
std::vector> toggleableEffects;
std::vector> luaEffects;
+ // Temporary preview effect applied while hovering effects in the grid (guarded by effectsLock)
+ std::shared_ptr previewEffect;
std::atomic luaValues[26] = {0.0};
std::shared_ptr frequencyEffect = std::make_shared(
@@ -72,20 +72,6 @@ public:
"frequency",
VERSION_HINT, 220.0, 0.0, 4200.0));
- std::shared_ptr trace = std::make_shared(
- std::vector{
- new osci::EffectParameter(
- "Trace Start",
- "Defines how far into the frame the drawing is started at. This has the effect of 'tracing' out the image from a single dot when animated. By default, we start drawing from the beginning of the frame, so this value is 0.0.",
- "traceStart",
- VERSION_HINT, 0.0, 0.0, 1.0, 0.001),
- new osci::EffectParameter(
- "Trace Length",
- "Defines how much of the frame is drawn per cycle. This has the effect of 'tracing' out the image from a single dot when animated. By default, we draw the whole frame, corresponding to a value of 1.0.",
- "traceLength",
- VERSION_HINT, 1.0, 0.0, 1.0, 0.001),
- });
-
std::shared_ptr delayEffect = std::make_shared();
std::function errorCallback = [this](int lineNum, juce::String fileName, juce::String error) { notifyErrorListeners(lineNum, fileName, error); };
@@ -94,13 +80,7 @@ public:
customEffect,
new osci::EffectParameter("Lua Effect", "Controls the strength of the custom Lua effect applied. You can write your own custom effect using Lua by pressing the edit button on the right.", "customEffectStrength", VERSION_HINT, 1.0, 0.0, 1.0));
- std::shared_ptr perspectiveEffect = std::make_shared();
- std::shared_ptr perspective = std::make_shared(
- perspectiveEffect,
- std::vector{
- new osci::EffectParameter("Perspective", "Controls the strength of the 3D perspective projection.", "perspectiveStrength", VERSION_HINT, 1.0, 0.0, 1.0),
- new osci::EffectParameter("FOV", "Controls the camera's field of view in degrees. A lower field of view makes the image look more flat, and a higher field of view makes the image look more 3D.", "perspectiveFov", VERSION_HINT, 50.0, 5.0, 130.0),
- });
+ std::shared_ptr perspective = PerspectiveEffect().build();
osci::BooleanParameter* midiEnabled = new osci::BooleanParameter("MIDI Enabled", "midiEnabled", VERSION_HINT, false, "Enable MIDI input for the synth. If disabled, the synth will play a constant tone, as controlled by the frequency slider.");
osci::BooleanParameter* inputEnabled = new osci::BooleanParameter("Audio Input Enabled", "inputEnabled", VERSION_HINT, false, "Enable to use input audio, instead of the generated audio.");
@@ -200,6 +180,10 @@ public:
void removeErrorListener(ErrorListener* listener);
void notifyErrorListeners(int lineNumber, juce::String id, juce::String error);
+ // Preview API: set/clear a temporary effect by ID for hover auditioning
+ void setPreviewEffectId(const juce::String& effectId);
+ void clearPreviewEffect();
+
// Setter for the callback
void setFileRemovedCallback(std::function callback);
diff --git a/Source/SettingsComponent.cpp b/Source/SettingsComponent.cpp
index a5c3bcf..87cf493 100644
--- a/Source/SettingsComponent.cpp
+++ b/Source/SettingsComponent.cpp
@@ -13,7 +13,7 @@ SettingsComponent::SettingsComponent(OscirenderAudioProcessor& p, OscirenderAudi
addChildComponent(frame);
double midiLayoutPreferredSize = std::any_cast(audioProcessor.getProperty("midiLayoutPreferredSize", pluginEditor.CLOSED_PREF_SIZE));
- double mainLayoutPreferredSize = std::any_cast(audioProcessor.getProperty("mainLayoutPreferredSize", -0.4));
+ double mainLayoutPreferredSize = std::any_cast(audioProcessor.getProperty("mainLayoutPreferredSize", -0.5));
midiLayout.setItemLayout(0, -0.1, -1.0, -(1.0 + midiLayoutPreferredSize));
midiLayout.setItemLayout(1, pluginEditor.RESIZER_BAR_SIZE, pluginEditor.RESIZER_BAR_SIZE, pluginEditor.RESIZER_BAR_SIZE);
@@ -46,8 +46,6 @@ void SettingsComponent::resized() {
mainLayout.layOutComponents(columns, 3, dummy.getX(), dummy.getY(), dummy.getWidth(), dummy.getHeight(), false, true);
auto bounds = dummy2.getBounds();
- perspective.setBounds(bounds.removeFromBottom(120));
- bounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE);
main.setBounds(bounds);
juce::Component* effectSettings = nullptr;
@@ -65,6 +63,9 @@ void SettingsComponent::resized() {
dummyBounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE);
}
+ perspective.setBounds(dummyBounds.removeFromBottom(120));
+ dummyBounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE);
+
effects.setBounds(dummyBounds);
if (isVisible() && getWidth() > 0 && getHeight() > 0) {
diff --git a/Source/TestMain.cpp b/Source/TestMain.cpp
index eed3c3c..e5d3a7f 100644
--- a/Source/TestMain.cpp
+++ b/Source/TestMain.cpp
@@ -1,118 +1,4 @@
#include
-#include "obj/Camera.h"
-#include "mathter/Common/Approx.hpp"
-
-class FrustumTest : public juce::UnitTest {
-public:
- FrustumTest() : juce::UnitTest("Frustum Culling") {}
-
- void runTest() override {
- double focalLength = 1;
-
- Camera camera;
- camera.setFocalLength(focalLength);
- Vec3 position = Vec3(0, 0, -focalLength);
- camera.setPosition(position);
- Frustum frustum = camera.getFrustum();
-
- beginTest("Focal Plane Frustum In-Bounds");
-
- // Focal plane is at z = 0
- Vec3 vecs[] = {
- Vec3(0, 0, 0), Vec3(0, 0, 0),
- Vec3(1, 1, 0), Vec3(1, 1, 0),
- Vec3(-1, -1, 0), Vec3(-1, -1, 0),
- Vec3(1, -1, 0), Vec3(1, -1, 0),
- Vec3(-1, 1, 0), Vec3(-1, 1, 0),
- Vec3(0.5, 0.5, 0), Vec3(0.5, 0.5, 0),
- };
-
- testFrustumClippedEqualsExpected(vecs, camera, 6);
-
- beginTest("Focal Plane Frustum Out-Of-Bounds");
-
- // Focal plane is at z = 0
- Vec3 vecs2[] = {
- Vec3(1.1, 1.1, 0), Vec3(1, 1, 0),
- Vec3(-1.1, -1.1, 0), Vec3(-1, -1, 0),
- Vec3(1.1, -1.1, 0), Vec3(1, -1, 0),
- Vec3(-1.1, 1.1, 0), Vec3(-1, 1, 0),
- Vec3(1.1, 0.5, 0), Vec3(1, 0.5, 0),
- Vec3(-1.1, 0.5, 0), Vec3(-1, 0.5, 0),
- Vec3(0.5, -1.1, 0), Vec3(0.5, -1, 0),
- Vec3(0.5, 1.1, 0), Vec3(0.5, 1, 0),
- Vec3(10, 10, 0), Vec3(1, 1, 0),
- Vec3(-10, -10, 0), Vec3(-1, -1, 0),
- Vec3(10, -10, 0), Vec3(1, -1, 0),
- Vec3(-10, 10, 0), Vec3(-1, 1, 0),
- };
-
- testFrustumClippedEqualsExpected(vecs2, camera, 12);
-
- beginTest("Behind Camera Out-Of-Bounds");
-
- double minZWorldCoords = -focalLength + camera.getFrustum().nearDistance;
-
- Vec3 vecs3[] = {
- Vec3(0, 0, -focalLength), Vec3(0, 0, minZWorldCoords),
- Vec3(0, 0, -100), Vec3(0, 0, minZWorldCoords),
- Vec3(0.5, 0.5, -focalLength), Vec3(0.1, 0.1, minZWorldCoords),
- Vec3(10, -10, -focalLength), Vec3(0.1, -0.1, minZWorldCoords),
- Vec3(-0.5, 0.5, -100), Vec3(-0.1, 0.1, minZWorldCoords),
- Vec3(-10, 10, -100), Vec3(-0.1, 0.1, minZWorldCoords),
- };
-
- testFrustumClippedEqualsExpected(vecs3, camera, 6);
-
- beginTest("3D Point Out-Of-Bounds");
-
- Vec3 vecs4[] = {
- Vec3(1, 1, -0.1),
- Vec3(-1, -1, -0.1),
- Vec3(1, -1, -0.1),
- Vec3(-1, 1, -0.1),
- Vec3(0.5, 0.5, minZWorldCoords),
- };
-
- testFrustumClipOccurs(vecs4, camera, 5);
- }
-
- Vec3 project(Vec3& p, double focalLength) {
- return Vec3(
- p.x * focalLength / p.z,
- p.y * focalLength / p.z,
- 0
- );
- }
-
- juce::String vec3ToString(Vec3& p) {
- return "(" + juce::String(p.x) + ", " + juce::String(p.y) + ", " + juce::String(p.z) + ")";
- }
-
- juce::String errorMessage(Vec3& actual, Vec3& expected) {
- return "Expected: " + vec3ToString(expected) + ", Actual: " + vec3ToString(actual);
- }
-
- void testFrustumClippedEqualsExpected(Vec3 vecs[], Camera& camera, int length) {
- for (int i = 0; i < length; i++) {
- Vec3 p = vecs[2 * i];
- p = camera.toCameraSpace(p);
- camera.getFrustum().clipToFrustum(p);
- p = camera.toWorldSpace(p);
- expect(mathter::AlmostEqual(p, vecs[2 * i + 1]), errorMessage(p, vecs[2 * i + 1]));
- }
- }
-
- void testFrustumClipOccurs(Vec3 vecs[], Camera& camera, int length) {
- for (int i = 0; i < length; i++) {
- Vec3 p = vecs[i];
- p = camera.toCameraSpace(p);
- camera.getFrustum().clipToFrustum(p);
- p = camera.toWorldSpace(p);
- expect(!mathter::AlmostEqual(p, vecs[i]), errorMessage(p, vecs[i]));
- }
- }
-};
class ProducerThread : public juce::Thread {
public:
@@ -183,7 +69,6 @@ public:
}
};
-static FrustumTest frustumTest;
static BufferConsumerTest bufferConsumerTest;
int main(int argc, char* argv[]) {
diff --git a/Source/audio/AutoGainControlEffect.h b/Source/audio/AutoGainControlEffect.h
index ab5ca47..a75b10b 100644
--- a/Source/audio/AutoGainControlEffect.h
+++ b/Source/audio/AutoGainControlEffect.h
@@ -40,6 +40,18 @@ public:
return input * gainAdjust;
}
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ std::vector{
+ new osci::EffectParameter("Intensity", "Controls how aggressively the gain adjustment is applied", "agcIntensity", VERSION_HINT, 1.0, 0.0, 1.0),
+ new osci::EffectParameter("Target Level", "Target output level for the automatic gain control", "agcTarget", VERSION_HINT, 0.6, 0.0, 1.0),
+ new osci::EffectParameter("Response", "How quickly the effect responds to level changes (lower is slower)", "agcResponse", VERSION_HINT, 0.0001, 0.0, 1.0)
+ }
+ );
+ return eff;
+ }
+
private:
const double EPSILON = 0.00001;
double smoothedLevel = 0.01; // Start with a small non-zero value to avoid division by zero
diff --git a/Source/audio/BitCrushEffect.h b/Source/audio/BitCrushEffect.h
index af84a66..fe14fc8 100644
--- a/Source/audio/BitCrushEffect.h
+++ b/Source/audio/BitCrushEffect.h
@@ -15,4 +15,12 @@ public:
double dequant = 1.0f / quant;
return osci::Point(dequant * (int)(input.x * quant), dequant * (int)(input.y * quant), dequant * (int)(input.z * quant));
}
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ new osci::EffectParameter("Bit Crush", "Limits the resolution of points drawn to the screen, making the object look pixelated, and making the audio sound more 'digital' and distorted.", "bitCrush", VERSION_HINT, 0.7, 0.0, 1.0));
+ eff->setIcon(BinaryData::bitcrush_svg);
+ return eff;
+ }
};
diff --git a/Source/audio/BounceEffect.h b/Source/audio/BounceEffect.h
new file mode 100644
index 0000000..ce6c126
--- /dev/null
+++ b/Source/audio/BounceEffect.h
@@ -0,0 +1,59 @@
+// BounceEffect.h (Simplified DVD-style 2D bounce)
+// Scales the original shape, then translates it within [-1,1] x [-1,1] using
+// constant-velocity motion that bounces off the edges. Z coordinate is unchanged.
+#pragma once
+
+#include
+
+class BounceEffect : public osci::EffectApplication {
+public:
+ osci::Point apply(int index, osci::Point input, const std::vector>& values, double sampleRate) override {
+ // values[0] = size (0.05..1.0)
+ // values[1] = speed (0..2)
+ // values[2] = angle (0..1 -> 0..2π)
+ double size = juce::jlimit(0.05, 1.0, values[0].load());
+ double speed = juce::jlimit(0.0, 2.0, values[1].load());
+ double angle = values[2].load() * juce::MathConstants::twoPi;
+
+ // Base direction from user
+ double dirX = std::cos(angle);
+ double dirY = std::sin(angle);
+ if (flipX) dirX = -dirX;
+ if (flipY) dirY = -dirY;
+
+ double dt = 1.0 / sampleRate;
+ position.x += dirX * speed * dt;
+ position.y += dirY * speed * dt;
+
+ double maxP = 1.0 - size;
+ double minP = -1.0 + size;
+ if (position.x > maxP) { position.x = maxP; flipX = !flipX; }
+ else if (position.x < minP) { position.x = minP; flipX = !flipX; }
+ if (position.y > maxP) { position.y = maxP; flipY = !flipY; }
+ else if (position.y < minP) { position.y = minP; flipY = !flipY; }
+
+ osci::Point scaled = input * size;
+ osci::Point out = scaled + osci::Point(position.x, position.y, 0.0);
+ out.z = input.z; // preserve original Z
+ return out;
+ }
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ std::vector{
+ new osci::EffectParameter("Bounce Size", "Size (scale) of the bouncing object.", "bounceSize", VERSION_HINT, 0.3, 0.05, 1.0),
+ new osci::EffectParameter("Bounce Speed", "Speed of motion.", "bounceSpeed", VERSION_HINT, 5.0, 0.0, 10.0),
+ new osci::EffectParameter("Bounce Angle", juce::String(juce::CharPointer_UTF8("Direction of travel (0..1 -> 0..360°).")), "bounceAngle", VERSION_HINT, 0.16, 0.0, 1.0),
+ }
+ );
+ eff->setName("Bounce");
+ eff->setIcon(BinaryData::bounce_svg);
+ return eff;
+ }
+
+private:
+ osci::Point position { 0.0, 0.0, 0.0 };
+ bool flipX = false;
+ bool flipY = false;
+};
diff --git a/Source/audio/BulgeEffect.h b/Source/audio/BulgeEffect.h
index 4cca78f..c15c9c1 100644
--- a/Source/audio/BulgeEffect.h
+++ b/Source/audio/BulgeEffect.h
@@ -13,4 +13,12 @@ public:
return osci::Point(rn * cos(theta), rn * sin(theta), input.z);
}
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ new osci::EffectParameter("Bulge", "Applies a bulge that makes the centre of the image larger, and squishes the edges of the image. This applies a distortion to the audio.", "bulge", VERSION_HINT, 0.5, 0.0, 1.0));
+ eff->setIcon(BinaryData::bulge_svg);
+ return eff;
+ }
};
diff --git a/Source/audio/CustomEffect.h b/Source/audio/CustomEffect.h
index 6c66e03..b057e33 100644
--- a/Source/audio/CustomEffect.h
+++ b/Source/audio/CustomEffect.h
@@ -18,6 +18,15 @@ public:
double frequency = 0;
+ std::shared_ptr build() const override {
+ // Note: callers needing CustomEffect with callback/vars should construct directly.
+ auto eff = std::make_shared(
+ std::make_shared([](int, juce::String, juce::String) {}, nullptr),
+ new osci::EffectParameter("Lua Effect", "Controls the strength of the custom Lua effect applied. You can write your own custom effect using Lua by pressing the edit button on the right.", "customEffectStrength", VERSION_HINT, 1.0, 0.0, 1.0));
+ eff->setIcon(BinaryData::lua_svg);
+ return eff;
+ }
+
private:
const juce::String DEFAULT_SCRIPT = "return { x, y, z }";
juce::String code = DEFAULT_SCRIPT;
diff --git a/Source/audio/DashedLineEffect.h b/Source/audio/DashedLineEffect.h
index e48c3f3..aaaa084 100644
--- a/Source/audio/DashedLineEffect.h
+++ b/Source/audio/DashedLineEffect.h
@@ -7,9 +7,18 @@ public:
DashedLineEffect(OscirenderAudioProcessor& p) : audioProcessor(p) {}
osci::Point apply(int index, osci::Point input, const std::vector>& values, double sampleRate) override {
- double dashCount = juce::jmax(1.0, values[0].load()); // Dashes per cycle
- double dashCoverage = juce::jlimit(0.0, 1.0, values[1].load());
- double dashOffset = values[2];
+ // if only 2 parameters are provided, this is being used as a 'trace effect'
+ // where the dash count is 1.
+ double dashCount = 1.0;
+ int i = 0;
+
+ if (values.size() > 2) {
+ dashCount = juce::jmax(1.0, values[i++].load()); // Dashes per cycle
+ }
+
+ double dashOffset = values[i++];
+ double dashCoverage = juce::jlimit(0.0, 1.0, values[i++].load());
+
double dashLengthSamples = (sampleRate / audioProcessor.frequency) / dashCount;
double dashPhase = framePhase * dashCount - dashOffset;
dashPhase = dashPhase - std::floor(dashPhase); // Wrap
@@ -33,10 +42,53 @@ public:
return output;
}
-private:
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(audioProcessor),
+ std::vector{
+ new osci::EffectParameter("Dash Count", "Controls the number of dashed lines in the drawing.", "dashCount", VERSION_HINT, 16.0, 1.0, 32.0),
+ new osci::EffectParameter("Dash Offset", "Offsets the location of the dashed lines.", "dashOffset", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001f, osci::LfoType::Sawtooth, 1.0f),
+ new osci::EffectParameter("Dash Width", "Controls how much each dash unit is drawn.", "dashWidth", VERSION_HINT, 0.5, 0.0, 1.0),
+ }
+ );
+ eff->setName("Dash");
+ eff->setIcon(BinaryData::dash_svg);
+ return eff;
+ }
+
+protected:
OscirenderAudioProcessor &audioProcessor;
+private:
const static int MAX_BUFFER = 192000;
std::vector buffer = std::vector(MAX_BUFFER);
int bufferIndex = 0;
double framePhase = 0.0; // [0, 1]
-};
\ No newline at end of file
+};
+
+class TraceEffect : public DashedLineEffect {
+public:
+ TraceEffect(OscirenderAudioProcessor& p) : DashedLineEffect(p) {}
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(audioProcessor),
+ std::vector{
+ new osci::EffectParameter(
+ "Trace Start",
+ "Defines how far into the frame the drawing is started at. This has the effect of 'tracing' out the image from a single dot when animated. By default, we start drawing from the beginning of the frame, so this value is 0.0.",
+ "traceStart",
+ VERSION_HINT, 0.0, 0.0, 1.0, 0.001
+ ),
+ new osci::EffectParameter(
+ "Trace Length",
+ "Defines how much of the frame is drawn per cycle. This has the effect of 'tracing' out the image from a single dot when animated. By default, we draw the whole frame, corresponding to a value of 1.0.",
+ "traceLength",
+ VERSION_HINT, 1.0, 0.0, 1.0, 0.001, osci::LfoType::Sawtooth
+ )
+ }
+ );
+ eff->setName("Trace");
+ eff->setIcon(BinaryData::trace_svg);
+ return eff;
+ }
+};
diff --git a/Source/audio/DelayEffect.h b/Source/audio/DelayEffect.h
index 74056ff..26c7591 100644
--- a/Source/audio/DelayEffect.h
+++ b/Source/audio/DelayEffect.h
@@ -37,6 +37,19 @@ public:
return vector;
}
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ std::vector{
+ new osci::EffectParameter("Delay Decay", "Adds repetitions, delays, or echos to the audio. This slider controls the volume of the echo.", "delayDecay", VERSION_HINT, 0.4, 0.0, 1.0),
+ new osci::EffectParameter("Delay Length", "Controls the time in seconds between echos.", "delayLength", VERSION_HINT, 0.5, 0.0, 1.0)
+ }
+ );
+ eff->setName("Delay");
+ eff->setIcon(BinaryData::delay_svg);
+ return eff;
+ }
+
private:
const static int MAX_DELAY = 192000 * 10;
std::vector delayBuffer = std::vector(MAX_DELAY);
diff --git a/Source/audio/DistortEffect.h b/Source/audio/DistortEffect.h
index 6b19c20..90bda53 100644
--- a/Source/audio/DistortEffect.h
+++ b/Source/audio/DistortEffect.h
@@ -1,20 +1,27 @@
#pragma once
#include
+
class DistortEffect : public osci::EffectApplication {
public:
- DistortEffect(bool vertical) : vertical(vertical) {}
-
- osci::Point apply(int index, osci::Point input, const std::vector>& values, double sampleRate) override {
- double value = values[0];
- int vertical = (int)this->vertical;
- if (index % 2 == 0) {
- input.translate((1 - vertical) * value, vertical * value, 0);
- } else {
- input.translate((1 - vertical) * -value, vertical * -value, 0);
- }
- return input;
+ osci::Point apply(int index, osci::Point input, const std::vector>& values, double /*sampleRate*/) override {
+ int flip = index % 2 == 0 ? 1 : -1;
+ osci::Point jitter = osci::Point(flip * values[0], flip * values[1], flip * values[2]);
+ return input + jitter;
+ }
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ std::vector{
+ new osci::EffectParameter("Distort X", "Distorts the image in the horizontal direction by jittering the audio sample being drawn.", "distortX", VERSION_HINT, 0.0, 0.0, 1.0),
+ new osci::EffectParameter("Distort Y", "Distorts the image in the vertical direction by jittering the audio sample being drawn.", "distortY", VERSION_HINT, 0.0, 0.0, 1.0),
+ new osci::EffectParameter("Distort Z", "Distorts the depth of the image by jittering the audio sample being drawn.", "distortZ", VERSION_HINT, 0.4, 0.0, 1.0),
+ }
+ );
+ eff->setName("Distort");
+ eff->setIcon(BinaryData::distort_svg);
+ eff->markLockable(false);
+ return eff;
}
-private:
- bool vertical;
};
diff --git a/Source/audio/KaleidoscopeEffect.h b/Source/audio/KaleidoscopeEffect.h
new file mode 100644
index 0000000..4c56d1e
--- /dev/null
+++ b/Source/audio/KaleidoscopeEffect.h
@@ -0,0 +1,88 @@
+// KaleidoscopeEffect.h
+// Repeats and mirrors the input around the origin to create a kaleidoscope pattern.
+// The effect supports a floating point number of segments, allowing smooth morphing
+// between different symmetry counts.
+#pragma once
+
+#include
+
+class KaleidoscopeEffect : public osci::EffectApplication {
+public:
+ osci::Point apply(int /*index*/, osci::Point input, const std::vector>& values, double /*sampleRate*/) override {
+ // values[0] = segments (can be fractional)
+ // values[1] = phase (0-1) selecting which segment is currently being drawn
+ double segments = juce::jmax(values[0].load(), 1.0); // ensure at least 1 segment
+ double phase = values.size() > 1 ? values[1].load() : 0.0;
+
+ // Polar conversion
+ double r = std::sqrt(input.x * input.x + input.y * input.y);
+ if (r < 1e-12) return input;
+ double theta = std::atan2(input.y, input.x);
+
+ int fullSegments = (int)std::floor(segments);
+ double fractionalPart = segments - fullSegments; // in [0,1)
+
+ // 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
+
+ // 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 wedgeAngle = baseWedgeAngle * partialScale;
+
+ // Normalize theta to [0,1) for compression
+ double thetaNorm = (theta + juce::MathConstants::pi) / juce::MathConstants::twoPi; // 0..1
+
+ // Offset for this segment: each preceding full segment occupies baseWedgeAngle
+ double segmentOffset = 0.0;
+ if (currentSegmentIndex < fullSegments) {
+ segmentOffset = currentSegmentIndex * baseWedgeAngle;
+ } else { // partial segment
+ segmentOffset = fullSegments * baseWedgeAngle;
+ }
+ // Map entire original angle range into [segmentOffset, segmentOffset + wedgeAngle) so edges line up exactly.
+ double finalTheta = segmentOffset + thetaNorm * wedgeAngle - juce::MathConstants::pi; // constant 180° rotation
+
+ double newX = r * std::cos(finalTheta);
+ double newY = r * std::sin(finalTheta);
+ return osci::Point(newX, newY, input.z);
+ }
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ std::vector{
+ new osci::EffectParameter(
+ "Kaleidoscope Segments",
+ "Controls how many times the image is rotationally repeated around the centre. Fractional values smoothly morph the repetition.",
+ "kaleidoscopeSegments",
+ VERSION_HINT,
+ 3.0, // default
+ 1.0, // min
+ 10.0, // max
+ 0.0001f, // step
+ osci::LfoType::Sine,
+ 0.25f // LFO frequency (Hz) – slow, visible rotation
+ ),
+ new osci::EffectParameter(
+ "Kaleidoscope Phase",
+ "Selects which kaleidoscope segment is currently being drawn (time-multiplexed). Animate to sweep around the circle.",
+ "kaleidoscopePhase",
+ VERSION_HINT,
+ 0.0, // default
+ 0.0, // min
+ 1.0, // max
+ 0.0001f, // step
+ osci::LfoType::Sawtooth,
+ 55.0f // LFO frequency (Hz) – slow, visible rotation
+ ),
+ }
+ );
+ eff->setName("Kaleidoscope");
+ eff->setIcon(BinaryData::kaleidoscope_svg);
+ return eff;
+ }
+};
diff --git a/Source/audio/MultiplexEffect.h b/Source/audio/MultiplexEffect.h
index 3074981..e4891dc 100644
--- a/Source/audio/MultiplexEffect.h
+++ b/Source/audio/MultiplexEffect.h
@@ -7,9 +7,9 @@ public:
osci::Point apply(int index, osci::Point input, const std::vector>& values, double sampleRate) override {
jassert(values.size() >= 6);
- double gridX = values[0].load();
- double gridY = values[1].load();
- double gridZ = values[2].load();
+ double gridX = values[0].load() + 0.0001;
+ double gridY = values[1].load() + 0.0001;
+ double gridZ = values[2].load() + 0.0001;
double interpolation = values[3].load();
double phase = values[4].load();
double gridDelay = values[5].load();
@@ -51,6 +51,23 @@ public:
return (1.0 - interpolationFactor) * current + interpolationFactor * next;
}
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ std::vector{
+ new osci::EffectParameter("Multiplex X", "Controls the horizontal grid size for the multiplex effect.", "multiplexGridX", VERSION_HINT, 2.0, 1.0, 8.0),
+ new osci::EffectParameter("Multiplex Y", "Controls the vertical grid size for the multiplex effect.", "multiplexGridY", VERSION_HINT, 2.0, 1.0, 8.0),
+ new osci::EffectParameter("Multiplex Z", "Controls the depth grid size for the multiplex effect.", "multiplexGridZ", VERSION_HINT, 1.0, 1.0, 8.0),
+ new osci::EffectParameter("Multiplex Smooth", "Controls the smoothness of transitions between grid sizes.", "multiplexSmooth", VERSION_HINT, 0.0, 0.0, 1.0),
+ new osci::EffectParameter("Multiplex Phase", "Controls the current phase of the multiplex grid animation.", "gridPhase", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001f, osci::LfoType::Sawtooth, 55.0f),
+ new osci::EffectParameter("Multiplex Delay", "Controls the delay of the audio samples used in the multiplex effect.", "gridDelay", VERSION_HINT, 0.0, 0.0, 1.0),
+ }
+ );
+ eff->setName("Multiplex");
+ eff->setIcon(BinaryData::multiplex_svg);
+ return eff;
+ }
+
private:
osci::Point multiplex(osci::Point point, double position, osci::Point grid) {
osci::Point unit = 1.0 / grid;
diff --git a/Source/audio/PerspectiveEffect.h b/Source/audio/PerspectiveEffect.h
index d237c20..3e047d0 100644
--- a/Source/audio/PerspectiveEffect.h
+++ b/Source/audio/PerspectiveEffect.h
@@ -25,6 +25,17 @@ public:
);
}
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ std::vector{
+ new osci::EffectParameter("Perspective", "Controls the strength of the 3D perspective projection.", "perspectiveStrength", VERSION_HINT, 1.0, 0.0, 1.0),
+ new osci::EffectParameter("FOV", "Controls the camera's field of view in degrees. A lower field of view makes the image look more flat, and a higher field of view makes the image look more 3D.", "perspectiveFov", VERSION_HINT, 50.0, 5.0, 130.0),
+ }
+ );
+ return eff;
+ }
+
private:
Camera camera;
diff --git a/Source/audio/RippleEffect.h b/Source/audio/RippleEffect.h
new file mode 100644
index 0000000..36f8bc5
--- /dev/null
+++ b/Source/audio/RippleEffect.h
@@ -0,0 +1,26 @@
+#pragma once
+#include
+
+class RippleEffectApp : public osci::EffectApplication {
+public:
+ osci::Point apply(int /*index*/, osci::Point input, const std::vector>& values, double /*sampleRate*/) override {
+ double phase = values[1] * std::numbers::pi;
+ double distance = 100 * values[2] * (input.x * input.x + input.y * input.y);
+ input.z += values[0] * std::sin(phase + distance);
+ return input;
+ }
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ std::vector{
+ new osci::EffectParameter("Ripple Depth", "Controls how large the ripples applied to the image are.", "rippleDepth", VERSION_HINT, 0.2, 0.0, 1.0),
+ new osci::EffectParameter("Ripple Phase", "Controls the position of the ripple. Animate this to see a moving ripple effect.", "ripplePhase", VERSION_HINT, 0.0, -1.0, 1.0, 0.0001f, osci::LfoType::Sawtooth, 1.0f),
+ new osci::EffectParameter("Ripple Amount", "Controls how many ripples are applied to the image.", "rippleAmount", VERSION_HINT, 0.1, 0.0, 1.0),
+ }
+ );
+ eff->setName("Ripple");
+ eff->setIcon(BinaryData::ripple_svg);
+ return eff;
+ }
+};
diff --git a/Source/audio/RotateEffect.h b/Source/audio/RotateEffect.h
new file mode 100644
index 0000000..dc3e04d
--- /dev/null
+++ b/Source/audio/RotateEffect.h
@@ -0,0 +1,24 @@
+#pragma once
+#include
+
+class RotateEffectApp : public osci::EffectApplication {
+public:
+ osci::Point apply(int /*index*/, osci::Point input, const std::vector>& values, double /*sampleRate*/) override {
+ input.rotate(values[0] * std::numbers::pi, values[1] * std::numbers::pi, values[2] * std::numbers::pi);
+ return input;
+ }
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ std::vector{
+ new osci::EffectParameter("Rotate X", "Controls the rotation of the object in the X axis.", "rotateX", VERSION_HINT, 0.0, -1.0, 1.0),
+ new osci::EffectParameter("Rotate Y", "Controls the rotation of the object in the Y axis.", "rotateY", VERSION_HINT, 0.0, -1.0, 1.0, 0.0001f, osci::LfoType::Sawtooth, 0.2f),
+ new osci::EffectParameter("Rotate Z", "Controls the rotation of the object in the Z axis.", "rotateZ", VERSION_HINT, 0.0, -1.0, 1.0),
+ }
+ );
+ eff->setName("Rotate");
+ eff->setIcon(BinaryData::rotate_svg);
+ return eff;
+ }
+};
diff --git a/Source/audio/ScaleEffect.h b/Source/audio/ScaleEffect.h
new file mode 100644
index 0000000..5368a9a
--- /dev/null
+++ b/Source/audio/ScaleEffect.h
@@ -0,0 +1,24 @@
+#pragma once
+#include
+
+class ScaleEffectApp : public osci::EffectApplication {
+public:
+ osci::Point apply(int /*index*/, osci::Point input, const std::vector>& values, double /*sampleRate*/) override {
+ return input * osci::Point(values[0], values[1], values[2]);
+ }
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ std::vector{
+ new osci::EffectParameter("Scale X", "Scales the object in the horizontal direction.", "scaleX", VERSION_HINT, 1.2, -3.0, 3.0),
+ new osci::EffectParameter("Scale Y", "Scales the object in the vertical direction.", "scaleY", VERSION_HINT, 1.2, -3.0, 3.0),
+ new osci::EffectParameter("Scale Z", "Scales the depth of the object.", "scaleZ", VERSION_HINT, 1.2, -3.0, 3.0),
+ }
+ );
+ eff->setName("Scale");
+ eff->setIcon(BinaryData::scale_svg);
+ eff->markLockable(true);
+ return eff;
+ }
+};
diff --git a/Source/audio/ShapeVoice.cpp b/Source/audio/ShapeVoice.cpp
index a2ffd8b..e980f7d 100644
--- a/Source/audio/ShapeVoice.cpp
+++ b/Source/audio/ShapeVoice.cpp
@@ -1,10 +1,7 @@
#include "ShapeVoice.h"
#include "../PluginProcessor.h"
-ShapeVoice::ShapeVoice(OscirenderAudioProcessor& p, juce::AudioSampleBuffer& externalAudio) : audioProcessor(p), externalAudio(externalAudio) {
- actualTraceStart = audioProcessor.trace->getValue(0);
- actualTraceLength = audioProcessor.trace->getValue(1);
-}
+ShapeVoice::ShapeVoice(OscirenderAudioProcessor& p, juce::AudioSampleBuffer& externalAudio) : audioProcessor(p), externalAudio(externalAudio) {}
bool ShapeVoice::canPlaySound(juce::SynthesiserSound* sound) {
return dynamic_cast (sound) != nullptr;
@@ -93,13 +90,7 @@ void ShapeVoice::renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int star
}
for (auto sample = startSample; sample < startSample + numSamples; ++sample) {
- bool traceEnabled = audioProcessor.trace->enabled->getBoolValue();
-
- // update length increment
- double traceLen = traceEnabled ? actualTraceLength : 1.0;
- double traceMin = traceEnabled ? actualTraceStart : 0.0;
- double proportionalLength = std::max(0.001, traceLen) * frameLength;
- lengthIncrement = juce::jmax(proportionalLength / (audioProcessor.currentSampleRate / actualFrequency), MIN_LENGTH_INCREMENT);
+ lengthIncrement = juce::jmax(frameLength / (audioProcessor.currentSampleRate / actualFrequency), MIN_LENGTH_INCREMENT);
osci::Point channels;
double x = 0.0;
@@ -163,27 +154,11 @@ void ShapeVoice::renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int star
outputBuffer.addSample(0, sample, x * gain);
}
- double traceStartValue = audioProcessor.trace->getActualValue(0);
- double traceLengthValue = audioProcessor.trace->getActualValue(1);
- traceLengthValue = traceEnabled ? traceLengthValue : 1.0;
- traceStartValue = traceEnabled ? traceStartValue : 0.0;
- actualTraceLength = std::max(0.01, traceLengthValue);
- actualTraceStart = traceStartValue;
- if (actualTraceStart < 0) {
- actualTraceStart = 0;
- }
-
if (!renderingSample) {
incrementShapeDrawing();
}
- double drawnFrameLength = frameLength;
- bool willLoopOver = false;
- if (traceEnabled) {
- drawnFrameLength *= actualTraceLength + actualTraceStart;
- }
-
- if (!renderingSample && frameDrawn >= drawnFrameLength) {
+ if (!renderingSample && frameDrawn >= frameLength) {
double currentShapeLength = 0;
if (currentShape < frame.size()) {
currentShapeLength = frame[currentShape]->len;
@@ -191,22 +166,9 @@ void ShapeVoice::renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int star
if (sound.load() != nullptr && currentlyPlaying) {
frameLength = sound.load()->updateFrame(frame);
}
- frameDrawn -= drawnFrameLength;
- if (traceEnabled) {
- shapeDrawn = juce::jlimit(0.0, currentShapeLength, frameDrawn);
- }
+ double prevFrameLength = frameLength;
+ frameDrawn -= prevFrameLength;
currentShape = 0;
-
- // TODO: updateFrame already iterates over all the shapes,
- // so we can improve performance by calculating frameDrawn
- // and shapeDrawn directly. frameDrawn is simply actualTraceStart * frameLength
- // but shapeDrawn is the amount of the current shape that has been drawn so
- // we need to iterate over all the shapes to calculate it.
- if (traceEnabled) {
- while (frameDrawn < actualTraceStart * frameLength) {
- incrementShapeDrawing();
- }
- }
}
}
}
diff --git a/Source/audio/ShapeVoice.h b/Source/audio/ShapeVoice.h
index 3e9f110..b99c4ad 100644
--- a/Source/audio/ShapeVoice.h
+++ b/Source/audio/ShapeVoice.h
@@ -21,14 +21,11 @@ public:
bool renderingSample = false;
private:
- const double MIN_TRACE = 0.005;
const double MIN_LENGTH_INCREMENT = 0.000001;
OscirenderAudioProcessor& audioProcessor;
std::vector> frame;
std::atomic sound = nullptr;
- double actualTraceStart;
- double actualTraceLength;
double frameLength = 0.0;
int currentShape = 0;
diff --git a/Source/audio/SmoothEffect.h b/Source/audio/SmoothEffect.h
index 5698eb3..0c88e9a 100644
--- a/Source/audio/SmoothEffect.h
+++ b/Source/audio/SmoothEffect.h
@@ -3,6 +3,9 @@
class SmoothEffect : public osci::EffectApplication {
public:
+ SmoothEffect() = default;
+ explicit SmoothEffect(juce::String prefix, float defaultValue = 0.75f) : idPrefix(prefix), smoothingDefault(defaultValue) {}
+
osci::Point apply(int index, osci::Point input, const std::vector>& values, double sampleRate) override {
double weight = juce::jmax(values[0].load(), 0.00001);
weight *= 0.95;
@@ -13,6 +16,18 @@ public:
return avg;
}
+
+ std::shared_ptr build() const override {
+ auto id = idPrefix.isEmpty() ? juce::String("smoothing") : (idPrefix + "Smoothing");
+ auto eff = std::make_shared(
+ std::make_shared(id),
+ new osci::EffectParameter("Smoothing", "This works as a low-pass frequency filter that removes high frequencies, making the image look smoother, and audio sound less harsh.", id, VERSION_HINT, smoothingDefault, 0.0, 1.0)
+ );
+ eff->setIcon(BinaryData::smoothing_svg);
+ return eff;
+ }
private:
osci::Point avg;
+ juce::String idPrefix;
+ float smoothingDefault = 0.5f;
};
diff --git a/Source/audio/StereoEffect.h b/Source/audio/StereoEffect.h
index 6d85637..44a86af 100644
--- a/Source/audio/StereoEffect.h
+++ b/Source/audio/StereoEffect.h
@@ -27,6 +27,18 @@ public:
return osci::Point(input.x, buffer[readHead].y, input.z);
}
+ std::shared_ptr build() const override {
+ return std::make_shared(
+ std::make_shared(),
+ new osci::EffectParameter(
+ "Stereo",
+ "Turns mono audio that is uninteresting to visualise into stereo audio that is interesting to visualise.",
+ "stereo",
+ VERSION_HINT, 0.0, 0.0, 1.0
+ )
+ );
+ }
+
private:
void initialiseBuffer(double sampleRate) {
diff --git a/Source/audio/SwirlEffect.h b/Source/audio/SwirlEffect.h
new file mode 100644
index 0000000..d570d16
--- /dev/null
+++ b/Source/audio/SwirlEffect.h
@@ -0,0 +1,23 @@
+#pragma once
+#include
+
+class SwirlEffectApp : public osci::EffectApplication {
+public:
+ osci::Point apply(int /*index*/, osci::Point input, const std::vector>& values, double /*sampleRate*/) override {
+ double length = 10 * values[0] * input.magnitude();
+ double newX = input.x * std::cos(length) - input.y * std::sin(length);
+ double newY = input.x * std::sin(length) + input.y * std::cos(length);
+ return osci::Point(newX, newY, input.z);
+ }
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ std::vector{
+ new osci::EffectParameter("Swirl", "Swirls the image in a spiral pattern.", "swirl", VERSION_HINT, 0.4, -1.0, 1.0),
+ }
+ );
+ eff->setIcon(BinaryData::swirl_svg);
+ return eff;
+ }
+};
diff --git a/Source/audio/TranslateEffect.h b/Source/audio/TranslateEffect.h
new file mode 100644
index 0000000..3a4c5b1
--- /dev/null
+++ b/Source/audio/TranslateEffect.h
@@ -0,0 +1,23 @@
+#pragma once
+#include
+
+class TranslateEffectApp : public osci::EffectApplication {
+public:
+ osci::Point apply(int /*index*/, osci::Point input, const std::vector>& values, double /*sampleRate*/) override {
+ return input + osci::Point(values[0], values[1], values[2]);
+ }
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ std::vector{
+ new osci::EffectParameter("Translate X", "Moves the object horizontally.", "translateX", VERSION_HINT, 0.3, -1.0, 1.0),
+ new osci::EffectParameter("Translate Y", "Moves the object vertically.", "translateY", VERSION_HINT, 0.0, -1.0, 1.0),
+ new osci::EffectParameter("Translate Z", "Moves the object away from the camera.", "translateZ", VERSION_HINT, 0.0, -1.0, 1.0),
+ }
+ );
+ eff->setName("Translate");
+ eff->setIcon(BinaryData::translate_svg);
+ return eff;
+ }
+};
diff --git a/Source/audio/TwistEffect.h b/Source/audio/TwistEffect.h
index b4754c1..59472cc 100644
--- a/Source/audio/TwistEffect.h
+++ b/Source/audio/TwistEffect.h
@@ -10,4 +10,13 @@ public:
input.rotate(0.0, twistTheta, 0.0);
return input;
}
-};
\ No newline at end of file
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ new osci::EffectParameter("Twist", "Twists the image in a corkscrew pattern.", "twist", VERSION_HINT, 0.5, 0.0, 1.0, 0.0001, osci::LfoType::Sine, 0.5)
+ );
+ eff->setIcon(BinaryData::twist_svg);
+ return eff;
+ }
+};
diff --git a/Source/audio/VectorCancellingEffect.h b/Source/audio/VectorCancellingEffect.h
index 5052623..7239834 100644
--- a/Source/audio/VectorCancellingEffect.h
+++ b/Source/audio/VectorCancellingEffect.h
@@ -20,6 +20,14 @@ public:
}
return input;
}
+
+ std::shared_ptr build() const override {
+ auto eff = std::make_shared(
+ std::make_shared(),
+ new osci::EffectParameter("Vector Cancelling", "Inverts the audio and image every few samples to 'cancel out' the audio, making the audio quiet, and distorting the image.", "vectorCancelling", VERSION_HINT, 0.5, 0.0, 1.0));
+ eff->setIcon(BinaryData::vectorcancelling_svg);
+ return eff;
+ }
private:
int lastIndex = 0;
double nextInvert = 0;
diff --git a/Source/audio/WobbleEffect.h b/Source/audio/WobbleEffect.h
index c82e853..3cb2563 100644
--- a/Source/audio/WobbleEffect.h
+++ b/Source/audio/WobbleEffect.h
@@ -14,6 +14,18 @@ public:
return input + delta;
}
+ std::shared_ptr build() const override {
+ auto wobble = std::make_shared(
+ std::make_shared(audioProcessor),
+ std::vector{
+ new osci::EffectParameter("Wobble Amount", "Adds a sine wave of the prominent frequency in the audio currently playing. The sine wave's frequency is slightly offset to create a subtle 'wobble' in the image. Increasing the slider increases the strength of the wobble.", "wobble", VERSION_HINT, 0.3, 0.0, 1.0),
+ new osci::EffectParameter("Wobble Phase", "Controls the phase of the wobble.", "wobblePhase", VERSION_HINT, 0.0, -1.0, 1.0, 0.0001f, osci::LfoType::Sawtooth, 1.0f),
+ });
+ wobble->setName("Wobble");
+ wobble->setIcon(BinaryData::wobble_svg);
+ return wobble;
+ }
+
private:
OscirenderAudioProcessor& audioProcessor;
double smoothedFrequency = 0;
diff --git a/Source/components/DraggableListBox.cpp b/Source/components/DraggableListBox.cpp
index d4f0da0..b54177a 100644
--- a/Source/components/DraggableListBox.cpp
+++ b/Source/components/DraggableListBox.cpp
@@ -1,19 +1,12 @@
#include "DraggableListBox.h"
+#include
DraggableListBoxItemData::~DraggableListBoxItemData() {};
void DraggableListBoxItem::paint(juce::Graphics& g)
{
- if (insertAfter)
- {
- g.setColour(juce::Colour(0xff00ff00));
- g.fillRect(0, getHeight() - 4, getWidth(), 4);
- }
- else if (insertBefore)
- {
- g.setColour(juce::Colour(0xff00ff00));
- g.fillRect(0, 0, getWidth(), 4);
- }
+ // Per-item insertion lines are suppressed in favour of a single overlay drawn by DraggableListBox.
+ juce::ignoreUnused(g);
}
void DraggableListBoxItem::mouseEnter(const juce::MouseEvent&)
@@ -31,7 +24,10 @@ void DraggableListBoxItem::mouseDrag(const juce::MouseEvent&)
{
if (juce::DragAndDropContainer* container = juce::DragAndDropContainer::findParentDragContainerFor(this))
{
- container->startDragging("DraggableListBoxItem", this);
+ auto* obj = new juce::DynamicObject();
+ obj->setProperty("type", juce::var("DraggableListBoxItem"));
+ obj->setProperty("row", juce::var(rowNum));
+ container->startDragging(juce::var(obj), this);
}
}
@@ -60,31 +56,247 @@ void DraggableListBoxItem::hideInsertLines()
void DraggableListBoxItem::itemDragEnter(const SourceDetails& dragSourceDetails)
{
updateInsertLines(dragSourceDetails);
+ updateAutoScroll(dragSourceDetails);
+ // Update the global overlay on the parent list box
+ auto ptGlobal = localPointToGlobal(dragSourceDetails.localPosition);
+ auto ptInLB = listBox.getLocalPoint(nullptr, ptGlobal);
+ listBox.updateDropIndicatorAt(ptInLB);
}
void DraggableListBoxItem::itemDragMove(const SourceDetails& dragSourceDetails)
{
updateInsertLines(dragSourceDetails);
+ updateAutoScroll(dragSourceDetails);
+ auto ptGlobal = localPointToGlobal(dragSourceDetails.localPosition);
+ auto ptInLB = listBox.getLocalPoint(nullptr, ptGlobal);
+ listBox.updateDropIndicatorAt(ptInLB);
}
void DraggableListBoxItem::itemDragExit(const SourceDetails& /*dragSourceDetails*/)
{
hideInsertLines();
+ stopAutoScroll();
+ listBox.clearDropIndicator();
}
void DraggableListBoxItem::itemDropped(const juce::DragAndDropTarget::SourceDetails &dragSourceDetails)
{
hideInsertLines();
+ stopAutoScroll();
+ listBox.clearDropIndicator();
if (DraggableListBoxItem* item = dynamic_cast(dragSourceDetails.sourceComponent.get()))
{
- if (dragSourceDetails.localPosition.y < getHeight() / 2)
- modelData.moveBefore(item->rowNum, rowNum);
- else
- modelData.moveAfter(item->rowNum, rowNum);
+ if (auto* vp = listBox.getViewport())
+ {
+ // Compute the global insertion index using the list box, not the item local midpoint
+ auto ptInThis = dragSourceDetails.localPosition;
+ auto ptGlobal = localPointToGlobal(ptInThis);
+ auto ptInLB = listBox.getLocalPoint(nullptr, ptGlobal);
+ int insertIndex = listBox.getInsertionIndexForPosition(ptInLB.x, ptInLB.y);
+ insertIndex = juce::jlimit(0, modelData.getNumItems(), insertIndex);
+
+ // If dragging an item that appears before the insertion point and we're moving it down,
+ // account for the removal shifting indices.
+ const int fromIndex = item->rowNum;
+ int toIndex = insertIndex;
+ if (toIndex > fromIndex) toIndex -= 1;
+
+ if (toIndex < 0) toIndex = 0;
+ if (toIndex >= modelData.getNumItems())
+ modelData.moveAfter(fromIndex, modelData.getNumItems() - 1);
+ else if (toIndex <= 0)
+ modelData.moveBefore(fromIndex, 0);
+ else
+ modelData.moveBefore(fromIndex, toIndex);
+ }
listBox.updateContent();
}
}
+void DraggableListBoxItem::updateAutoScroll(const SourceDetails& dragSourceDetails)
+{
+ // Determine pointer position within the list's viewport
+ if (auto* vp = listBox.getViewport())
+ {
+ // Convert the drag position to the viewport's local coordinates
+ auto ptInThis = dragSourceDetails.localPosition;
+ auto ptInLB = localPointToGlobal(ptInThis);
+ ptInLB = listBox.getLocalPoint(nullptr, ptInLB); // convert from screen to listbox
+
+ auto viewBounds = vp->getLocalBounds();
+ auto viewTop = viewBounds.getY();
+ auto viewBottom = viewBounds.getBottom();
+
+ // Position of pointer within the viewport component
+ auto ptInVP = vp->getLocalPoint(&listBox, ptInLB);
+ const int yInVP = ptInVP.y;
+
+ const int edgeZone = juce::jmax(12, viewBounds.getHeight() / 6); // scrolling zone near edges
+ double velocity = 0.0;
+
+ if (yInVP < viewTop + edgeZone)
+ {
+ // Near top: scroll up. Speed increases closer to edge.
+ const double t = juce::jlimit(0.0, 1.0, (double)(edgeZone - (yInVP - viewTop)) / (double)edgeZone);
+ velocity = - (1.0 + 14.0 * t * t); // pixels per tick (quadratic ramp), negative = up
+ }
+ else if (yInVP > viewBottom - edgeZone)
+ {
+ // Near bottom: scroll down.
+ const double d = (double)(yInVP - (viewBottom - edgeZone));
+ const double t = juce::jlimit(0.0, 1.0, d / (double)edgeZone);
+ velocity = (1.0 + 14.0 * t * t); // pixels per tick, positive = down
+ }
+
+ scrollPixelsPerTick = velocity;
+
+ if (std::abs(scrollPixelsPerTick) > 0.0)
+ {
+ if (!autoScrollActive)
+ {
+ autoScrollActive = true;
+ startTimerHz(60); // smooth-ish scrolling tied to UI thread
+ }
+ }
+ else
+ {
+ stopAutoScroll();
+ }
+ }
+}
+
+void DraggableListBoxItem::stopAutoScroll()
+{
+ autoScrollActive = false;
+ stopTimer();
+ scrollPixelsPerTick = 0.0;
+}
+
+void DraggableListBoxItem::timerCallback()
+{
+ if (!autoScrollActive || scrollPixelsPerTick == 0.0)
+ return;
+
+ if (auto* vp = listBox.getViewport())
+ {
+ auto current = vp->getViewPosition();
+ const int contentH = vp->getViewedComponent() != nullptr ? vp->getViewedComponent()->getHeight() : listBox.getHeight();
+ const int maxY = juce::jmax(0, contentH - vp->getHeight());
+ int newY = juce::jlimit(0, maxY, current.y + (int)std::lround(scrollPixelsPerTick));
+ if (newY != current.y)
+ {
+ vp->setViewPosition(current.x, newY);
+ }
+
+ // Update the global drop indicator position based on current mouse position (even if the mouse isn't moving)
+ auto screenPos = juce::Desktop::getInstance().getMainMouseSource().getScreenPosition();
+ auto posInLB = listBox.getLocalPoint(nullptr, screenPos.toInt());
+ listBox.updateDropIndicatorAt(posInLB);
+ }
+}
+
+// ===================== DraggableListBox overlay indicator =====================
+
+void DraggableListBox::updateDropIndicator(const SourceDetails& details)
+{
+ // localPosition is already in this component's coordinate space
+ const auto pt = details.localPosition;
+ int index = getInsertionIndexForPosition(pt.x, pt.y);
+ if (index < 0) index = 0; // allow showing at very top (over header spacer)
+ dropInsertIndex = index;
+ showDropIndicator = true;
+ repaint();
+}
+
+void DraggableListBox::clearDropIndicator()
+{
+ showDropIndicator = false;
+ dropInsertIndex = -1;
+ repaint();
+}
+
+void DraggableListBox::updateDropIndicatorAt(const juce::Point& listLocalPos)
+{
+ int index = getInsertionIndexForPosition(listLocalPos.x, listLocalPos.y);
+ if (index < 0) index = 0; // allow showing at very top (over header spacer)
+ dropInsertIndex = index;
+ showDropIndicator = true;
+ repaint();
+}
+
+void DraggableListBox::paintOverChildren(juce::Graphics& g)
+{
+ VListBox::paintOverChildren(g);
+ if (!showDropIndicator) return;
+
+ const int numRows = getModel() != nullptr ? getModel()->getNumRows() : 0;
+ if (dropInsertIndex < 0 || dropInsertIndex > numRows) return;
+
+ auto* vp = getViewport();
+ if (vp == nullptr) return;
+
+ // Determine the y position between rows to draw the indicator line
+ int y = 0;
+ if (dropInsertIndex == 0)
+ {
+ // Top of first row (below header)
+ if (numRows > 0)
+ y = getRowPosition(0, true).getY();
+ else
+ y = 0;
+ }
+ else if (dropInsertIndex >= numRows)
+ {
+ auto lastRowBounds = getRowPosition(numRows - 1, true);
+ y = lastRowBounds.getBottom();
+ }
+ else
+ {
+ auto prevBounds = getRowPosition(dropInsertIndex - 1, true);
+ y = prevBounds.getBottom();
+ }
+
+ // Draw a prominent indicator spanning the visible row width
+ const int x = 0;
+ const int w = getVisibleRowWidth();
+ const int thickness = 3;
+ const juce::Colour colour = juce::Colours::lime.withAlpha(0.9f);
+
+ const float yOffset = -2.5f; // Offset to center the line visually
+
+ g.setColour(colour);
+ g.fillRoundedRectangle(x, y - thickness / 2 + yOffset, w, thickness, 2.0f);
+}
+
+void DraggableListBox::itemDropped(const SourceDetails& details)
+{
+ // Background drop: compute insertion index and use model to move
+ int insertIndex = -1;
+ // localPosition is already relative to this list
+ insertIndex = getInsertionIndexForPosition(details.localPosition.x, details.localPosition.y);
+ if (insertIndex < 0) insertIndex = 0; // clamp to top when over header spacer
+
+ if (auto* m = dynamic_cast(getModel()))
+ {
+ int fromIndex = -1;
+ const juce::var& desc = details.description;
+ if (desc.isObject())
+ {
+ auto* obj = desc.getDynamicObject();
+ if (obj != nullptr)
+ {
+ auto v = obj->getProperty("row");
+ if (v.isInt()) fromIndex = (int)v;
+ }
+ }
+
+ if (fromIndex >= 0 && insertIndex >= 0)
+ m->moveByInsertIndex(fromIndex, insertIndex);
+ }
+
+ clearDropIndicator();
+}
+
juce::Component* DraggableListBoxModel::refreshComponentForRow(int rowNumber, bool isRowSelected, juce::Component *existingComponentToUpdate)
{
std::unique_ptr item(dynamic_cast(existingComponentToUpdate));
diff --git a/Source/components/DraggableListBox.h b/Source/components/DraggableListBox.h
index f21dff9..f7c88e9 100644
--- a/Source/components/DraggableListBox.h
+++ b/Source/components/DraggableListBox.h
@@ -18,14 +18,39 @@ struct DraggableListBoxItemData
virtual void addItemAtEnd() {};
};
-// DraggableListBox is basically just a ListBox, that inherits from DragAndDropContainer.
-// Declare your list box using this type.
-class DraggableListBox : public juce::jc::ListBox, public juce::DragAndDropContainer
+// DraggableListBox extends VListBox to both initiate drags and act as a target, so
+// it can paint a clean, consistent drop indicator between row components.
+class DraggableListBox : public VListBox,
+ public juce::DragAndDropContainer,
+ public juce::DragAndDropTarget
{
+public:
+ using VListBox::VListBox;
+
+ // DragAndDropTarget
+ bool isInterestedInDragSource(const SourceDetails&) override { return true; }
+ void itemDragEnter(const SourceDetails& details) override { updateDropIndicator(details); }
+ void itemDragMove(const SourceDetails& details) override { updateDropIndicator(details); }
+ void itemDragExit(const SourceDetails&) override { clearDropIndicator(); }
+ void itemDropped(const SourceDetails& details) override;
+ bool shouldDrawDragImageWhenOver() override { return true; }
+
+ // Paint a global drop indicator between rows
+ void paintOverChildren(juce::Graphics& g) override;
+
+ // Allow children to drive indicator positioning
+ void updateDropIndicatorAt(const juce::Point& listLocalPos);
+ void clearDropIndicator();
+
+private:
+ void updateDropIndicator(const SourceDetails& details);
+
+ bool showDropIndicator = false;
+ int dropInsertIndex = -1; // index to insert before; may be getNumRows() for end
};
// Everything below this point should be generic.
-class DraggableListBoxItem : public juce::Component, public juce::DragAndDropTarget
+class DraggableListBoxItem : public juce::Component, public juce::DragAndDropTarget, public juce::Timer
{
public:
DraggableListBoxItem(DraggableListBox& lb, DraggableListBoxItemData& data, int rn)
@@ -49,6 +74,9 @@ public:
protected:
void updateInsertLines(const SourceDetails &dragSourceDetails);
void hideInsertLines();
+ void updateAutoScroll(const SourceDetails& dragSourceDetails);
+ void stopAutoScroll();
+ void timerCallback() override;
int rowNum;
DraggableListBoxItemData& modelData;
@@ -57,9 +85,13 @@ protected:
juce::MouseCursor savedCursor;
bool insertAfter = false;
bool insertBefore = false;
+
+ // Auto-scroll state while dragging near viewport edges
+ double scrollPixelsPerTick = 0.0; // positive = scroll down, negative = up
+ bool autoScrollActive = false;
};
-class DraggableListBoxModel : public juce::jc::ListBoxModel
+class DraggableListBoxModel : public VListBoxModel
{
public:
DraggableListBoxModel(DraggableListBox& lb, DraggableListBoxItemData& md)
@@ -70,6 +102,27 @@ public:
juce::Component* refreshComponentForRow(int, bool, juce::Component*) override;
+ // Convenience: move an item using an insertion index (before position). Handles index shifting.
+ void moveByInsertIndex(int fromIndex, int insertIndex)
+ {
+ const int count = modelData.getNumItems();
+ if (count <= 0) return;
+ insertIndex = juce::jlimit(0, count, insertIndex);
+ int toIndex = insertIndex;
+ if (toIndex > fromIndex) toIndex -= 1;
+
+ if (count == 1 || fromIndex == toIndex) return;
+
+ if (toIndex <= 0)
+ modelData.moveBefore(fromIndex, 0);
+ else if (toIndex >= count)
+ modelData.moveAfter(fromIndex, count - 1);
+ else
+ modelData.moveBefore(fromIndex, toIndex);
+
+ listBox.updateContent();
+ }
+
protected:
// Draggable model has a reference to its owner ListBox, so it can tell it to update after DnD.
DraggableListBox &listBox;
diff --git a/Source/components/EffectComponent.cpp b/Source/components/EffectComponent.cpp
index ba5bd37..1c0454f 100644
--- a/Source/components/EffectComponent.cpp
+++ b/Source/components/EffectComponent.cpp
@@ -28,6 +28,7 @@ EffectComponent::EffectComponent(osci::Effect& effect, int index) : effect(effec
slider.setSliderStyle(juce::Slider::LinearHorizontal);
slider.setTextBoxStyle(juce::Slider::TextBoxRight, false, TEXT_BOX_WIDTH, slider.getTextBoxHeight());
+ slider.setScrollWheelEnabled(false);
if (effect.parameters[index]->step == 1.0) {
slider.setNumDecimalPlacesToDisplay(0);
} else {
@@ -39,6 +40,7 @@ EffectComponent::EffectComponent(osci::Effect& effect, int index) : effect(effec
lfoSlider.setTextValueSuffix("Hz");
lfoSlider.setColour(sliderThumbOutlineColourId, juce::Colour(0xff00ff00));
lfoSlider.setNumDecimalPlacesToDisplay(3);
+ lfoSlider.setScrollWheelEnabled(false);
label.setFont(juce::Font(14.0f));
@@ -213,14 +215,8 @@ void EffectComponent::resized() {
}
void EffectComponent::paint(juce::Graphics& g) {
- auto bounds = getLocalBounds();
- auto length = effect.parameters.size();
- auto isEnd = index == length - 1;
- auto isStart = index == 0;
g.setColour(findColour(effectComponentBackgroundColourId, true));
- juce::Path path;
- path.addRoundedRectangle(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), OscirenderLookAndFeel::RECT_RADIUS, OscirenderLookAndFeel::RECT_RADIUS, false, isStart, false, isEnd);
- g.fillPath(path);
+ g.fillRect(getLocalBounds());
}
void EffectComponent::parameterValueChanged(int parameterIndex, float newValue) {
diff --git a/Source/components/EffectTypeGridComponent.cpp b/Source/components/EffectTypeGridComponent.cpp
new file mode 100644
index 0000000..54e5dec
--- /dev/null
+++ b/Source/components/EffectTypeGridComponent.cpp
@@ -0,0 +1,205 @@
+#include "EffectTypeGridComponent.h"
+#include "../LookAndFeel.h"
+#include
+#include
+#include
+
+EffectTypeGridComponent::EffectTypeGridComponent(OscirenderAudioProcessor& processor)
+ : audioProcessor(processor)
+{
+ // Setup scrollable viewport and content
+ addAndMakeVisible(viewport);
+ viewport.setViewedComponent(&content, false);
+ viewport.setScrollBarsShown(true, false); // vertical only
+ // Setup reusable bottom fade
+ initScrollFade(*this);
+ attachToViewport(viewport);
+ setupEffectItems();
+ setSize(400, 200);
+ addAndMakeVisible(cancelButton);
+ cancelButton.onClick = [this]() {
+ if (onCanceled) onCanceled();
+ };
+ refreshDisabledStates();
+}
+
+EffectTypeGridComponent::~EffectTypeGridComponent() = default;
+
+void EffectTypeGridComponent::setupEffectItems()
+{
+ // Clear existing items
+ effectItems.clear();
+ content.removeAllChildren();
+
+ // Get effect types directly from the audio processor's toggleableEffects
+ juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
+ const int n = (int) audioProcessor.toggleableEffects.size();
+ std::vector order(n);
+ std::iota(order.begin(), order.end(), 0);
+ std::sort(order.begin(), order.end(), [this](int a, int b) {
+ auto ea = audioProcessor.toggleableEffects[a];
+ auto eb = audioProcessor.toggleableEffects[b];
+ const int cmp = ea->getName().compareIgnoreCase(eb->getName());
+ if (cmp != 0)
+ return cmp < 0; // ascending alphabetical, case-insensitive
+ // Stable tie-breaker to ensure deterministic layout
+ return ea->getId().compare(eb->getId()) < 0;
+ });
+
+ for (int idx : order)
+ {
+ auto effect = audioProcessor.toggleableEffects[idx];
+ // Extract effect name from the effect
+ juce::String effectName = effect->getName();
+
+ // Create new item component
+ auto* item = new EffectTypeItemComponent(effectName, effect->getIcon(), effect->getId());
+
+ // Set up callback to forward effect selection
+ item->onEffectSelected = [this](const juce::String& effectId) {
+ if (onEffectSelected)
+ onEffectSelected(effectId);
+ };
+ // Hover preview: request temporary preview of this effect while hovered
+ item->onHoverStart = [this](const juce::String& effectId) {
+ if (audioProcessor.getGlobalBoolValue("previewEffectOnHover", true)) {
+ juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
+ audioProcessor.setPreviewEffectId(effectId);
+ }
+ };
+ item->onHoverEnd = [this]() {
+ if (audioProcessor.getGlobalBoolValue("previewEffectOnHover", true)) {
+ juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
+ audioProcessor.clearPreviewEffect();
+ }
+ };
+
+ effectItems.add(item);
+ content.addAndMakeVisible(item);
+ }
+}
+
+void EffectTypeGridComponent::refreshDisabledStates()
+{
+ juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
+ // Build a quick lookup of selected ids
+ std::unordered_set selectedIds;
+ selectedIds.reserve((size_t) audioProcessor.toggleableEffects.size());
+ bool anySelected = false;
+ for (const auto& eff : audioProcessor.toggleableEffects) {
+ const bool isSelected = (eff->selected == nullptr) ? true : eff->selected->getBoolValue();
+ if (isSelected) {
+ anySelected = true;
+ selectedIds.insert(eff->getId().toStdString());
+ }
+ }
+ for (auto* item : effectItems) {
+ const bool disable = selectedIds.find(item->getEffectId().toStdString()) != selectedIds.end();
+ item->setEnabled(! disable);
+ }
+ cancelButton.setVisible(anySelected);
+ // Update fade visibility/layout in case scrollability changed
+ layoutScrollFade(viewport.getBounds(), true, 48);
+}
+
+void EffectTypeGridComponent::paint(juce::Graphics& g)
+{
+ // No background - make component transparent
+}
+
+void EffectTypeGridComponent::resized()
+{
+ auto bounds = getLocalBounds();
+ auto topBar = bounds.removeFromTop(30);
+ cancelButton.setBounds(topBar.removeFromRight(80).reduced(4));
+ viewport.setBounds(bounds);
+ auto contentArea = viewport.getLocalBounds();
+ // Lock content width to viewport width to avoid horizontal scrolling
+ content.setSize(contentArea.getWidth(), content.getHeight());
+
+ // Create FlexBox for responsive grid layout within content
+ flexBox = juce::FlexBox();
+ flexBox.flexWrap = juce::FlexBox::Wrap::wrap;
+ flexBox.justifyContent = juce::FlexBox::JustifyContent::spaceBetween;
+ flexBox.alignContent = juce::FlexBox::AlignContent::flexStart;
+ flexBox.flexDirection = juce::FlexBox::Direction::row;
+
+ // Determine fixed per-item width for this viewport width
+ const int viewW = contentArea.getWidth();
+ const int viewH = contentArea.getHeight();
+ const int itemsPerRow = juce::jmax(1, viewW / MIN_ITEM_WIDTH);
+ const int fixedItemWidth = (itemsPerRow > 0 ? viewW / itemsPerRow : viewW);
+
+ // Add each effect item with a fixed width, and pad the final row with placeholders so it's centered
+ const int total = effectItems.size();
+ const int fullRows = (itemsPerRow > 0 ? total / itemsPerRow : 0);
+ const int remainder = (itemsPerRow > 0 ? total % itemsPerRow : 0);
+
+ auto addItemFlex = [&](juce::Component* c)
+ {
+ flexBox.items.add(juce::FlexItem(*c)
+ .withMinWidth((float) fixedItemWidth)
+ .withMaxWidth((float) fixedItemWidth)
+ .withHeight((float) ITEM_HEIGHT)
+ .withFlex(1.0f) // keep existing flex behaviour; fixed max width holds size
+ .withMargin(juce::FlexItem::Margin(0)));
+ };
+
+ auto addPlaceholder = [&]()
+ {
+ // Placeholder occupies a slot visually but has no component; ensures last row is centered
+ juce::FlexItem placeholder((float) fixedItemWidth, (float) ITEM_HEIGHT);
+ placeholder.flexGrow = 1.0f; // match item flex for consistent spacing
+ placeholder.margin = juce::FlexItem::Margin(0);
+ flexBox.items.add(std::move(placeholder));
+ };
+
+ int index = 0;
+ // Add complete rows
+ for (int r = 0; r < fullRows; ++r)
+ for (int c = 0; c < itemsPerRow; ++c)
+ addItemFlex(effectItems.getUnchecked(index++));
+
+ // Add last row centered with balanced placeholders
+ if (remainder > 0)
+ {
+ const int missing = itemsPerRow - remainder;
+ const int leftPad = missing / 2;
+ const int rightPad = missing - leftPad;
+
+ for (int i = 0; i < leftPad; ++i) addPlaceholder();
+ for (int i = 0; i < remainder; ++i) addItemFlex(effectItems.getUnchecked(index++));
+ for (int i = 0; i < rightPad; ++i) addPlaceholder();
+ }
+
+ // Compute required content height
+ const int requiredHeight = calculateRequiredHeight(viewW);
+
+ // If content is shorter than viewport, make content at least as tall as viewport
+ int yOffset = 0;
+ if (requiredHeight < viewH) {
+ content.setSize(viewW, viewH);
+ yOffset = (viewH - requiredHeight) / 2;
+ } else {
+ content.setSize(viewW, requiredHeight);
+ }
+ // Layout items within content at the computed offset
+ flexBox.performLayout(juce::Rectangle(0.0f, (float) yOffset, (float) viewW, (float) requiredHeight));
+
+ // Layout bottom scroll fade over the viewport area
+ layoutScrollFade(viewport.getBounds(), true, 48);
+}
+
+int EffectTypeGridComponent::calculateRequiredHeight(int availableWidth) const
+{
+ if (effectItems.isEmpty())
+ return ITEM_HEIGHT;
+
+ // Calculate how many items can fit per row
+ int itemsPerRow = juce::jmax(1, availableWidth / MIN_ITEM_WIDTH);
+
+ // Calculate number of rows needed
+ int numRows = (effectItems.size() + itemsPerRow - 1) / itemsPerRow; // Ceiling division
+
+ return numRows * ITEM_HEIGHT;
+}
diff --git a/Source/components/EffectTypeGridComponent.h b/Source/components/EffectTypeGridComponent.h
new file mode 100644
index 0000000..cafdcf6
--- /dev/null
+++ b/Source/components/EffectTypeGridComponent.h
@@ -0,0 +1,35 @@
+#pragma once
+#include
+#include "../PluginProcessor.h"
+#include "EffectTypeItemComponent.h"
+#include "ScrollFadeMixin.h"
+
+class EffectTypeGridComponent : public juce::Component, private ScrollFadeMixin
+{
+public:
+ EffectTypeGridComponent(OscirenderAudioProcessor& processor);
+ ~EffectTypeGridComponent() override;
+
+ void paint(juce::Graphics& g) override;
+ void resized() override;
+
+ int calculateRequiredHeight(int availableWidth) const;
+ std::function onEffectSelected;
+ std::function onCanceled; // optional cancel handler
+ void refreshDisabledStates(); // grey-out items that are already selected
+
+private:
+ OscirenderAudioProcessor& audioProcessor;
+ juce::Viewport viewport; // scroll container
+ juce::Component content; // holds the grid items
+ juce::OwnedArray effectItems;
+ juce::FlexBox flexBox;
+ juce::TextButton cancelButton { "Cancel" };
+
+ static constexpr int ITEM_HEIGHT = 80;
+ static constexpr int MIN_ITEM_WIDTH = 180;
+
+ void setupEffectItems();
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EffectTypeGridComponent)
+};
diff --git a/Source/components/EffectTypeItemComponent.cpp b/Source/components/EffectTypeItemComponent.cpp
new file mode 100644
index 0000000..e15c970
--- /dev/null
+++ b/Source/components/EffectTypeItemComponent.cpp
@@ -0,0 +1,123 @@
+#include "EffectTypeItemComponent.h"
+
+EffectTypeItemComponent::EffectTypeItemComponent(const juce::String& name, const juce::String& icon, const juce::String& id)
+ : effectName(name), effectId(id)
+{
+ juce::String iconSvg = icon;
+ if (icon.isEmpty()) {
+ // Default icon if none is provided
+ iconSvg = juce::String::createStringFromData(BinaryData::rotate_svg, BinaryData::rotate_svgSize);
+ }
+ iconButton = std::make_unique(
+ "effectIcon",
+ iconSvg,
+ juce::Colours::white.withAlpha(0.7f)
+ );
+
+ // Make the icon non-interactive since this is just a visual element
+ iconButton->setInterceptsMouseClicks(false, false);
+ addAndMakeVisible(*iconButton);
+}
+
+EffectTypeItemComponent::~EffectTypeItemComponent() = default;
+
+void EffectTypeItemComponent::paint(juce::Graphics& g)
+{
+ auto bounds = getLocalBounds().toFloat().reduced(10);
+
+ // Get animation progress from inherited HoverAnimationMixin (disabled => no hover)
+ auto animationProgress = isEnabled() ? getAnimationProgress() : 0.0f;
+
+ // Apply upward shift based on animation progress
+ auto yOffset = -animationProgress * HOVER_LIFT_AMOUNT;
+ bounds = bounds.translated(0, yOffset);
+
+ // Draw drop shadow
+ if (animationProgress > 0.01f) {
+ juce::DropShadow shadow;
+ shadow.colour = juce::Colours::lime.withAlpha(animationProgress * 0.2f);
+ shadow.radius = 15 * animationProgress;
+ shadow.offset = juce::Point(0, 4);
+
+ if (shadow.radius > 0) {
+ juce::Path shadowPath;
+ shadowPath.addRoundedRectangle(bounds.toFloat(), CORNER_RADIUS);
+ shadow.drawForPath(g, shadowPath);
+ }
+ }
+
+ // Draw background with rounded corners - interpolate between normal and hover colors
+ juce::Colour normalBgColour = Colours::veryDark;
+ juce::Colour hoverBgColour = normalBgColour.brighter(0.05f);
+ juce::Colour bgColour = normalBgColour.interpolatedWith(hoverBgColour, animationProgress);
+
+ g.setColour(bgColour);
+ g.fillRoundedRectangle(bounds.toFloat(), CORNER_RADIUS);
+
+ // Draw colored outline
+ juce::Colour outlineColour = juce::Colour::fromRGB(160, 160, 160);
+ g.setColour(outlineColour.withAlpha(0.9f));
+ g.drawRoundedRectangle(bounds.toFloat(), CORNER_RADIUS, 1.0f);
+
+ // Create text area - now accounting for icon space on the left
+ auto textArea = bounds.reduced(8, 4);
+ textArea.removeFromLeft(28); // Remove space for icon (24px + 4px gap)
+
+ g.setColour(juce::Colours::white);
+ g.setFont(juce::FontOptions(16.0f, juce::Font::plain));
+ g.drawText(effectName, textArea, juce::Justification::centred, true);
+
+ // If disabled, draw a dark transparent overlay over the rounded rect to simplify visuals
+ if (! isEnabled()) {
+ g.setColour(juce::Colours::black.withAlpha(0.35f));
+ g.fillRoundedRectangle(bounds.toFloat(), CORNER_RADIUS);
+ }
+}
+
+void EffectTypeItemComponent::resized()
+{
+ auto bounds = getLocalBounds().reduced(10);
+
+ // Reserve space for the icon on the left
+ auto iconArea = bounds.removeFromLeft(60); // 24px for icon
+ iconArea = iconArea.withSizeKeepingCentre(40, 40); // Make icon 20x20px
+
+ iconButton->setBounds(iconArea);
+
+ // Get animation progress and calculate Y offset
+ auto animationProgress = isEnabled() ? getAnimationProgress() : 0.0f;
+ auto yOffset = -animationProgress * HOVER_LIFT_AMOUNT;
+
+ iconButton->setTransform(juce::AffineTransform::translation(0, yOffset));
+}
+
+void EffectTypeItemComponent::mouseDown(const juce::MouseEvent& event)
+{
+ if (! isEnabled()) return;
+ // Extend base behavior to keep hover press animation
+ HoverAnimationMixin::mouseDown(event);
+ // Ensure any hover preview is cleared before permanently selecting/enabling the effect
+ if (onHoverEnd) onHoverEnd();
+ if (onEffectSelected) {
+ onEffectSelected(effectId);
+ }
+}
+
+void EffectTypeItemComponent::mouseMove(const juce::MouseEvent& event) {
+ setMouseCursor(isEnabled() ? juce::MouseCursor::PointingHandCursor : juce::MouseCursor::NormalCursor);
+ juce::Desktop::getInstance().getMainMouseSource().forceMouseCursorUpdate();
+}
+
+void EffectTypeItemComponent::mouseEnter(const juce::MouseEvent& event)
+{
+ HoverAnimationMixin::mouseEnter(event);
+ if (isEnabled() && onHoverStart)
+ onHoverStart(effectId);
+}
+
+void EffectTypeItemComponent::mouseExit(const juce::MouseEvent& event)
+{
+ HoverAnimationMixin::mouseExit(event);
+ if (onHoverEnd)
+ onHoverEnd();
+}
diff --git a/Source/components/EffectTypeItemComponent.h b/Source/components/EffectTypeItemComponent.h
new file mode 100644
index 0000000..4bfab69
--- /dev/null
+++ b/Source/components/EffectTypeItemComponent.h
@@ -0,0 +1,38 @@
+#pragma once
+#include
+#include "../LookAndFeel.h"
+#include "HoverAnimationMixin.h"
+#include "SvgButton.h"
+
+class EffectTypeItemComponent : public HoverAnimationMixin
+{
+public:
+ EffectTypeItemComponent(const juce::String& name, const juce::String& icon, const juce::String& id);
+ ~EffectTypeItemComponent() override;
+
+ void paint(juce::Graphics& g) override;
+ void resized() override;
+ void mouseDown(const juce::MouseEvent& event) override;
+ void mouseMove(const juce::MouseEvent& event) override;
+ void mouseEnter(const juce::MouseEvent& event) override;
+ void mouseExit(const juce::MouseEvent& event) override;
+
+ const juce::String& getEffectId() const { return effectId; }
+ const juce::String& getEffectName() const { return effectName; }
+
+ std::function onEffectSelected;
+ std::function onHoverStart;
+ std::function onHoverEnd;
+
+private:
+ juce::String effectName;
+ juce::String effectId;
+
+ // Icon for the effect
+ std::unique_ptr iconButton;
+
+ static constexpr int CORNER_RADIUS = 8;
+ static constexpr float HOVER_LIFT_AMOUNT = 2.0f;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EffectTypeItemComponent)
+};
diff --git a/Source/components/EffectsListComponent.cpp b/Source/components/EffectsListComponent.cpp
index 7c48aaa..c225fb2 100644
--- a/Source/components/EffectsListComponent.cpp
+++ b/Source/components/EffectsListComponent.cpp
@@ -6,45 +6,70 @@
EffectsListComponent::EffectsListComponent(DraggableListBox& lb, AudioEffectListBoxItemData& data, int rn, osci::Effect& effect) : DraggableListBoxItem(lb, data, rn),
effect(effect), audioProcessor(data.audioProcessor), editor(data.editor) {
auto parameters = effect.parameters;
- for (int i = 0; i < parameters.size(); i++) {
- std::shared_ptr effectComponent = std::make_shared(effect, i);
- selected.setToggleState(effect.enabled == nullptr || effect.enabled->getValue(), juce::dontSendNotification);
- // using weak_ptr to avoid circular reference and memory leak
- std::weak_ptr weakEffectComponent = effectComponent;
- effectComponent->slider.setValue(parameters[i]->getValueUnnormalised(), juce::dontSendNotification);
-
- list.setEnabled(selected.getToggleState());
- selected.onClick = [this, weakEffectComponent] {
- if (auto effectComponent = weakEffectComponent.lock()) {
- auto data = (AudioEffectListBoxItemData&)modelData;
- juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
- data.setSelected(rowNum, selected.getToggleState());
- list.setEnabled(selected.getToggleState());
- }
+ for (int i = 0; i < parameters.size(); i++) {
+ std::shared_ptr effectComponent = std::make_shared(effect, i);
+ enabled.setToggleState(effect.enabled == nullptr || effect.enabled->getValue(), juce::dontSendNotification);
+ // using weak_ptr to avoid circular reference and memory leak
+ std::weak_ptr weakEffectComponent = effectComponent;
+ effectComponent->slider.setValue(parameters[i]->getValueUnnormalised(), juce::dontSendNotification);
+
+ list.setEnabled(enabled.getToggleState());
+ enabled.onClick = [this, weakEffectComponent] {
+ if (auto effectComponent = weakEffectComponent.lock()) {
+ auto data = (AudioEffectListBoxItemData&)modelData;
+ juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
+ data.setSelected(rowNum, enabled.getToggleState());
+ list.setEnabled(enabled.getToggleState());
+ }
repaint();
- };
+ };
effectComponent->updateToggleState = [this, i, weakEffectComponent] {
if (auto effectComponent = weakEffectComponent.lock()) {
- selected.setToggleState(effectComponent->effect.enabled == nullptr || effectComponent->effect.enabled->getValue(), juce::dontSendNotification);
- list.setEnabled(selected.getToggleState());
+ enabled.setToggleState(effectComponent->effect.enabled == nullptr || effectComponent->effect.enabled->getValue(), juce::dontSendNotification);
+ list.setEnabled(enabled.getToggleState());
}
repaint();
};
- auto component = createComponent(parameters[i]);
- if (component != nullptr) {
+ auto component = createComponent(parameters[i]);
+ if (component != nullptr) {
effectComponent->setComponent(component);
}
- listModel.addComponent(effectComponent);
- }
+ listModel.addComponent(effectComponent);
+ }
list.setColour(effectComponentBackgroundColourId, juce::Colours::transparentBlack.withAlpha(0.2f));
- list.setModel(&listModel);
- list.setRowHeight(ROW_HEIGHT);
- list.updateContent();
- addAndMakeVisible(list);
- addAndMakeVisible(selected);
+ list.setModel(&listModel);
+ list.setRowHeight(ROW_HEIGHT);
+ list.updateContent();
+ addAndMakeVisible(list);
+ addAndMakeVisible(enabled);
+
+ closeButton.setEdgeIndent(2);
+ closeButton.onClick = [this]() {
+ // Flip flags under lock
+ {
+ juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
+ if (this->effect.enabled) this->effect.enabled->setValueNotifyingHost(false);
+ if (this->effect.selected) this->effect.selected->setValueNotifyingHost(false);
+ // Reset all parameters/flags for this effect back to defaults on removal
+ this->effect.resetToDefault();
+ }
+ // Defer model reset and outer list refresh to avoid re-entrancy on current row
+ juce::MessageManager::callAsync([this]() {
+ auto& data = static_cast(modelData);
+ data.resetData();
+ // Update the outer DraggableListBox, not the inner parameter list
+ this->listBox.updateContent();
+ // If there are no effects visible, open the grid
+ if (data.getNumItems() == 0) {
+ if (data.onAddNewEffectRequested)
+ data.onAddNewEffectRequested();
+ }
+ });
+ };
+ addAndMakeVisible(closeButton);
}
EffectsListComponent::~EffectsListComponent() {
@@ -52,26 +77,34 @@ EffectsListComponent::~EffectsListComponent() {
}
void EffectsListComponent::paint(juce::Graphics& g) {
- auto bounds = getLocalBounds().removeFromLeft(LEFT_BAR_WIDTH);
+ auto bounds = getLocalBounds().removeFromLeft(LEFT_BAR_WIDTH);
g.setColour(findColour(effectComponentHandleColourId));
bounds.removeFromBottom(PADDING);
juce::Path path;
path.addRoundedRectangle(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), OscirenderLookAndFeel::RECT_RADIUS, OscirenderLookAndFeel::RECT_RADIUS, true, false, true, false);
g.fillPath(path);
- g.setColour(juce::Colours::white);
- // draw drag and drop handle using circles
- double size = 4;
- double leftPad = 4;
- double spacing = 7;
- double topPad = 6;
- double y = bounds.getHeight() / 2 - 15;
- g.fillEllipse(leftPad, y + topPad, size, size);
- g.fillEllipse(leftPad, y + topPad + spacing, size, size);
- g.fillEllipse(leftPad, y + topPad + 2 * spacing, size, size);
- g.fillEllipse(leftPad + spacing, y + topPad, size, size);
- g.fillEllipse(leftPad + spacing, y + topPad + spacing, size, size);
- g.fillEllipse(leftPad + spacing, y + topPad + 2 * spacing, size, size);
- DraggableListBoxItem::paint(g);
+ g.setColour(juce::Colours::white);
+ // draw drag and drop handle using circles
+ double size = 4;
+ double leftPad = 4;
+ double spacing = 7;
+ double topPad = 6;
+ double y = bounds.getHeight() / 2 - 15;
+ g.fillEllipse(leftPad, y + topPad, size, size);
+ g.fillEllipse(leftPad, y + topPad + spacing, size, size);
+ g.fillEllipse(leftPad, y + topPad + 2 * spacing, size, size);
+ g.fillEllipse(leftPad + spacing, y + topPad, size, size);
+ g.fillEllipse(leftPad + spacing, y + topPad + spacing, size, size);
+ g.fillEllipse(leftPad + spacing, y + topPad + 2 * spacing, size, size);
+
+ auto rightBar = getLocalBounds().removeFromRight(RIGHT_BAR_WIDTH);
+ rightBar.removeFromBottom(PADDING);
+ g.setColour(findColour(effectComponentHandleColourId));
+ juce::Path rightPath;
+ rightPath.addRoundedRectangle(rightBar.getX(), rightBar.getY(), rightBar.getWidth(), rightBar.getHeight(), OscirenderLookAndFeel::RECT_RADIUS, OscirenderLookAndFeel::RECT_RADIUS, false, true, false, true);
+ g.fillPath(rightPath);
+
+ DraggableListBoxItem::paint(g);
}
void EffectsListComponent::paintOverChildren(juce::Graphics& g) {
@@ -81,51 +114,53 @@ void EffectsListComponent::paintOverChildren(juce::Graphics& g) {
juce::Path path;
path.addRoundedRectangle(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), OscirenderLookAndFeel::RECT_RADIUS, OscirenderLookAndFeel::RECT_RADIUS, false, true, false, true);
- if (!selected.getToggleState()) {
+ if (!enabled.getToggleState()) {
g.fillPath(path);
}
}
void EffectsListComponent::resized() {
- auto area = getLocalBounds();
+ auto area = getLocalBounds();
auto leftBar = area.removeFromLeft(LEFT_BAR_WIDTH);
+ auto buttonBounds = area.removeFromRight(RIGHT_BAR_WIDTH);
+ closeButton.setBounds(buttonBounds);
+ closeButton.setImageTransform(juce::AffineTransform::translation(0, -2));
leftBar.removeFromLeft(20);
- area.removeFromRight(PADDING);
- selected.setBounds(leftBar.withSizeKeepingCentre(30, 20));
- list.setBounds(area);
+ enabled.setBounds(leftBar.withSizeKeepingCentre(30, 20));
+ list.setBounds(area);
}
std::shared_ptr EffectsListComponent::createComponent(osci::EffectParameter* parameter) {
- if (parameter->paramID == "customEffectStrength") {
- std::shared_ptr button = std::make_shared(parameter->name, BinaryData::pencil_svg, juce::Colours::white, juce::Colours::red);
- std::weak_ptr weakButton = button;
- button->setEdgeIndent(5);
- button->setToggleState(editor.editingCustomFunction, juce::dontSendNotification);
- button->setTooltip("Toggles whether the text editor is editing the currently open file, or the custom Lua effect.");
- button->onClick = [this, weakButton] {
- if (auto button = weakButton.lock()) {
+ if (parameter->paramID == "customEffectStrength") {
+ std::shared_ptr button = std::make_shared(parameter->name, BinaryData::pencil_svg, juce::Colours::white, juce::Colours::red);
+ std::weak_ptr weakButton = button;
+ button->setEdgeIndent(5);
+ button->setToggleState(editor.editingCustomFunction, juce::dontSendNotification);
+ button->setTooltip("Toggles whether the text editor is editing the currently open file, or the custom Lua effect.");
+ button->onClick = [this, weakButton] {
+ if (auto button = weakButton.lock()) {
editor.editCustomFunction(button->getToggleState());
}
- };
- return button;
- }
- return nullptr;
+ };
+ return button;
+ }
+ return nullptr;
}
int EffectsListBoxModel::getRowHeight(int row) {
- auto data = (AudioEffectListBoxItemData&)modelData;
- return data.getEffect(row)->parameters.size() * EffectsListComponent::ROW_HEIGHT + EffectsListComponent::PADDING;
+ auto data = (AudioEffectListBoxItemData&)modelData;
+ return data.getEffect(row)->parameters.size() * EffectsListComponent::ROW_HEIGHT + EffectsListComponent::PADDING;
}
bool EffectsListBoxModel::hasVariableHeightRows() const {
- return true;
+ return true;
}
juce::Component* EffectsListBoxModel::refreshComponentForRow(int rowNumber, bool isRowSelected, juce::Component *existingComponentToUpdate) {
+ auto data = (AudioEffectListBoxItemData&)modelData;
+ if (! juce::isPositiveAndBelow(rowNumber, data.getNumItems()))
+ return nullptr;
std::unique_ptr item(dynamic_cast(existingComponentToUpdate));
- if (juce::isPositiveAndBelow(rowNumber, modelData.getNumItems())) {
- auto data = (AudioEffectListBoxItemData&)modelData;
- item = std::make_unique(listBox, (AudioEffectListBoxItemData&)modelData, rowNumber, *data.getEffect(rowNumber));
- }
+ item = std::make_unique(listBox, (AudioEffectListBoxItemData&)modelData, rowNumber, *data.getEffect(rowNumber));
return item.release();
}
diff --git a/Source/components/EffectsListComponent.h b/Source/components/EffectsListComponent.h
index d813224..9fc9b93 100644
--- a/Source/components/EffectsListComponent.h
+++ b/Source/components/EffectsListComponent.h
@@ -5,6 +5,8 @@
#include "EffectComponent.h"
#include "ComponentList.h"
#include "SwitchButton.h"
+#include "EffectTypeGridComponent.h"
+#include "SvgButton.h"
#include
// Application-specific data container
@@ -14,6 +16,7 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData
std::vector> data;
OscirenderAudioProcessor& audioProcessor;
OscirenderAudioProcessorEditor& editor;
+ std::function onAddNewEffectRequested; // callback hooked by parent to open the grid
AudioEffectListBoxItemData(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), editor(editor) {
resetData();
@@ -21,11 +24,31 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData
void randomise() {
juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
-
- for (int i = 0; i < data.size(); i++) {
- auto effect = data[i];
- auto id = effect->getId().toLowerCase();
-
+ // Decide how many effects to select (1..5 or up to available)
+ int total = (int) audioProcessor.toggleableEffects.size();
+ int maxPick = juce::jmin(5, total);
+ int numPick = juce::jmax(1, juce::Random::getSystemRandom().nextInt({1, maxPick + 1}));
+
+ // Build indices [0..total)
+ std::vector indices(total);
+ std::iota(indices.begin(), indices.end(), 0);
+ std::random_device rd;
+ std::mt19937 g(rd());
+ std::shuffle(indices.begin(), indices.end(), g);
+
+ // First, deselect and disable all
+ for (auto& effect : audioProcessor.toggleableEffects) {
+ effect->markSelectable(false);
+ effect->markEnableable(false);
+ }
+
+ // Pick numPick to select & enable, and randomise params
+ for (int k = 0; k < numPick && k < indices.size(); ++k) {
+ auto& effect = audioProcessor.toggleableEffects[indices[k]];
+ effect->markSelectable(true);
+ effect->markEnableable(true);
+
+ auto id = effect->getId().toLowerCase();
if (id.contains("scale") || id.contains("translate") || id.contains("trace")) {
continue;
}
@@ -35,26 +58,23 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData
if (parameter->lfo != nullptr) {
parameter->lfo->setUnnormalisedValueNotifyingHost((int) osci::LfoType::Static);
parameter->lfoRate->setUnnormalisedValueNotifyingHost(1);
-
if (juce::Random::getSystemRandom().nextFloat() > 0.8) {
parameter->lfo->setUnnormalisedValueNotifyingHost((int)(juce::Random::getSystemRandom().nextFloat() * (int) osci::LfoType::Noise));
parameter->lfoRate->setValueNotifyingHost(juce::Random::getSystemRandom().nextFloat() * 0.1);
}
}
}
- effect->enabled->setValueNotifyingHost(juce::Random::getSystemRandom().nextFloat() > 0.7);
- }
+ }
- // shuffle precedence
- std::random_device rd;
- std::mt19937 g(rd());
- std::shuffle(data.begin(), data.end(), g);
-
- for (int i = 0; i < data.size(); i++) {
- data[i]->setPrecedence(i);
- }
-
- audioProcessor.updateEffectPrecedence();
+ // Refresh local data with only selected effects
+ resetData();
+
+ // shuffle precedence of the selected subset
+ std::shuffle(data.begin(), data.end(), g);
+ for (int i = 0; i < data.size(); i++) {
+ data[i]->setPrecedence(i);
+ }
+ audioProcessor.updateEffectPrecedence();
}
void resetData() {
@@ -63,17 +83,22 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData
for (int i = 0; i < audioProcessor.toggleableEffects.size(); i++) {
auto effect = audioProcessor.toggleableEffects[i];
effect->setValue(effect->getValue());
- data.push_back(effect);
+ // Ensure 'selected' exists and defaults to true for older projects
+ effect->markSelectable(effect->selected == nullptr ? true : effect->selected->getBoolValue());
+ if (effect->selected == nullptr || effect->selected->getBoolValue()) {
+ data.push_back(effect);
+ }
}
}
int getNumItems() override {
- return data.size();
+ // Only the effects themselves are rows; the "+ Add new effect" button is a separate control below the list
+ return (int) data.size();
}
// CURRENTLY NOT USED
void deleteItem(int indexOfItemToDelete) override {
- // data.erase(data.begin() + indexOfItemToDelete);
+ // data.erase(data.begin() + indexOfItemToDelete);
}
// CURRENTLY NOT USED
@@ -81,10 +106,10 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData
// data.push_back(juce::String("Yahoo"));
}
- void moveBefore(int indexOfItemToMove, int indexOfItemToPlaceBefore) override {
+ void moveBefore(int indexOfItemToMove, int indexOfItemToPlaceBefore) override {
juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
- auto effect = data[indexOfItemToMove];
+ auto effect = data[indexOfItemToMove];
if (indexOfItemToMove < indexOfItemToPlaceBefore) {
move(data, indexOfItemToMove, indexOfItemToPlaceBefore - 1);
@@ -92,12 +117,12 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData
move(data, indexOfItemToMove, indexOfItemToPlaceBefore);
}
- for (int i = 0; i < data.size(); i++) {
- data[i]->setPrecedence(i);
- }
+ for (int i = 0; i < data.size(); i++) {
+ data[i]->setPrecedence(i);
+ }
audioProcessor.updateEffectPrecedence();
- }
+ }
void moveAfter(int indexOfItemToMove, int indexOfItemToPlaceAfter) override {
juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock);
@@ -150,6 +175,7 @@ public:
void resized() override;
static const int LEFT_BAR_WIDTH = 50;
+ static const int RIGHT_BAR_WIDTH = 15; // space for close button
static const int ROW_HEIGHT = 30;
static const int PADDING = 4;
@@ -157,7 +183,8 @@ protected:
osci::Effect& effect;
ComponentListModel listModel;
juce::ListBox list;
- jux::SwitchButton selected = { effect.enabled };
+ jux::SwitchButton enabled = { effect.enabled };
+ SvgButton closeButton = SvgButton("closeEffect", juce::String::createStringFromData(BinaryData::close_svg, BinaryData::close_svgSize), juce::Colours::white, juce::Colours::white);
private:
OscirenderAudioProcessor& audioProcessor;
OscirenderAudioProcessorEditor& editor;
diff --git a/Source/components/HoverAnimationMixin.cpp b/Source/components/HoverAnimationMixin.cpp
new file mode 100644
index 0000000..12d02e7
--- /dev/null
+++ b/Source/components/HoverAnimationMixin.cpp
@@ -0,0 +1,70 @@
+#include "HoverAnimationMixin.h"
+
+HoverAnimationMixin::HoverAnimationMixin()
+ : animatorUpdater(this),
+ hoverAnimator(juce::ValueAnimatorBuilder{}
+ .withEasing(getEasingFunction())
+ .withDurationMs(getHoverAnimationDurationMs())
+ .withValueChangedCallback([this](auto value) {
+ animationProgress = static_cast(value);
+ repaint();
+ resized();
+ })
+ .build()),
+ unhoverAnimator(juce::ValueAnimatorBuilder{}
+ .withEasing(getEasingFunction())
+ .withDurationMs(getHoverAnimationDurationMs())
+ .withValueChangedCallback([this](auto value) {
+ animationProgress = 1.0f - static_cast(value);
+ repaint();
+ resized();
+ })
+ .build())
+{
+ setupAnimators();
+}
+
+void HoverAnimationMixin::setupAnimators()
+{
+ animatorUpdater.addAnimator(hoverAnimator);
+ animatorUpdater.addAnimator(unhoverAnimator);
+}
+
+void HoverAnimationMixin::animateHover(bool isHovering)
+{
+ if (isHovering)
+ {
+ unhoverAnimator.complete();
+ hoverAnimator.start();
+ }
+ else
+ {
+ hoverAnimator.complete();
+ unhoverAnimator.start();
+ }
+}
+
+void HoverAnimationMixin::mouseEnter(const juce::MouseEvent&)
+{
+ isHovered = true;
+ animateHover(true);
+}
+
+void HoverAnimationMixin::mouseExit(const juce::MouseEvent&)
+{
+ isHovered = false;
+ // Fixed logic to prevent getting stuck in hovered state
+ animateHover(false);
+}
+
+void HoverAnimationMixin::mouseDown(const juce::MouseEvent&)
+{
+ animateHover(false);
+}
+
+void HoverAnimationMixin::mouseUp(const juce::MouseEvent& event)
+{
+ // Only animate hover if the mouse is still within the component bounds
+ if (getLocalBounds().contains(event.getEventRelativeTo(this).getPosition()))
+ animateHover(true);
+}
diff --git a/Source/components/HoverAnimationMixin.h b/Source/components/HoverAnimationMixin.h
new file mode 100644
index 0000000..65c0b07
--- /dev/null
+++ b/Source/components/HoverAnimationMixin.h
@@ -0,0 +1,42 @@
+#pragma once
+#include
+
+// Base Component providing animated hover behavior via JUCE mouse overrides.
+class HoverAnimationMixin : public juce::Component
+{
+public:
+ HoverAnimationMixin();
+ ~HoverAnimationMixin() override = default;
+
+ // Animation control (available for programmatic triggers if needed)
+ void animateHover(bool isHovering);
+
+ // Getters
+ float getAnimationProgress() const { return animationProgress; }
+ bool getIsHovered() const { return isHovered; }
+
+protected:
+ // Customization hooks
+ virtual int getHoverAnimationDurationMs() const { return 200; }
+ virtual std::function getEasingFunction() const { return juce::Easings::createEaseOut(); }
+
+public:
+ // juce::Component overrides for mouse events - keep public so children can call base explicitly
+ void mouseEnter(const juce::MouseEvent& event) override;
+ void mouseExit(const juce::MouseEvent& event) override;
+ void mouseDown(const juce::MouseEvent& event) override;
+ void mouseUp(const juce::MouseEvent& event) override;
+
+private:
+ float animationProgress = 0.0f;
+ bool isHovered = false;
+
+ // Animation components
+ juce::VBlankAnimatorUpdater animatorUpdater;
+ juce::Animator hoverAnimator;
+ juce::Animator unhoverAnimator;
+
+ void setupAnimators();
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(HoverAnimationMixin)
+};
diff --git a/Source/components/MainMenuBarModel.cpp b/Source/components/MainMenuBarModel.cpp
index 2dff617..e07787b 100644
--- a/Source/components/MainMenuBarModel.cpp
+++ b/Source/components/MainMenuBarModel.cpp
@@ -6,12 +6,17 @@ MainMenuBarModel::~MainMenuBarModel() {}
void MainMenuBarModel::addTopLevelMenu(const juce::String& name) {
topLevelMenuNames.add(name);
- menuItems.push_back(std::vector>>());
+ menuItems.push_back({});
menuItemsChanged();
}
void MainMenuBarModel::addMenuItem(int topLevelMenuIndex, const juce::String& name, std::function action) {
- menuItems[topLevelMenuIndex].push_back(std::make_pair(name, action));
+ menuItems[topLevelMenuIndex].push_back({ name, std::move(action), {}, false });
+ menuItemsChanged();
+}
+
+void MainMenuBarModel::addToggleMenuItem(int topLevelMenuIndex, const juce::String& name, std::function action, std::function isTicked) {
+ menuItems[topLevelMenuIndex].push_back({ name, std::move(action), std::move(isTicked), true });
menuItemsChanged();
}
@@ -26,8 +31,13 @@ juce::PopupMenu MainMenuBarModel::getMenuForIndex(int topLevelMenuIndex, const j
customMenuLogic(menu, topLevelMenuIndex);
}
- for (int i = 0; i < menuItems[topLevelMenuIndex].size(); i++) {
- menu.addItem(i + 1, menuItems[topLevelMenuIndex][i].first);
+ for (int i = 0; i < (int) menuItems[topLevelMenuIndex].size(); i++) {
+ auto& mi = menuItems[topLevelMenuIndex][i];
+ juce::PopupMenu::Item item(mi.name);
+ item.itemID = i + 1;
+ if (mi.hasTick && mi.isTicked)
+ item.setTicked(mi.isTicked());
+ menu.addItem(item);
}
return menu;
@@ -37,7 +47,9 @@ void MainMenuBarModel::menuItemSelected(int menuItemID, int topLevelMenuIndex) {
if (customMenuSelectedLogic && customMenuSelectedLogic(menuItemID, topLevelMenuIndex)) {
return;
}
- menuItems[topLevelMenuIndex][menuItemID - 1].second();
+ auto& mi = menuItems[topLevelMenuIndex][menuItemID - 1];
+ if (mi.action)
+ mi.action();
}
void MainMenuBarModel::menuBarActivated(bool isActive) {}
diff --git a/Source/components/MainMenuBarModel.h b/Source/components/MainMenuBarModel.h
index 5e80fd8..ea9f993 100644
--- a/Source/components/MainMenuBarModel.h
+++ b/Source/components/MainMenuBarModel.h
@@ -8,6 +8,8 @@ public:
void addTopLevelMenu(const juce::String& name);
void addMenuItem(int topLevelMenuIndex, const juce::String& name, std::function action);
+ // Adds a toggle (ticked) menu item whose tick state is provided dynamically via isTicked()
+ void addToggleMenuItem(int topLevelMenuIndex, const juce::String& name, std::function action, std::function isTicked);
void resetMenuItems();
std::function customMenuLogic;
@@ -19,6 +21,14 @@ private:
void menuItemSelected(int menuItemID, int topLevelMenuIndex) override;
void menuBarActivated(bool isActive);
+ struct MenuItem
+ {
+ juce::String name;
+ std::function action;
+ std::function isTicked; // optional tick state
+ bool hasTick = false;
+ };
+
juce::StringArray topLevelMenuNames;
- std::vector>>> menuItems;
+ std::vector> menuItems;
};
diff --git a/Source/components/OsciMainMenuBarModel.cpp b/Source/components/OsciMainMenuBarModel.cpp
index c81bf7f..16e2974 100644
--- a/Source/components/OsciMainMenuBarModel.cpp
+++ b/Source/components/OsciMainMenuBarModel.cpp
@@ -10,12 +10,13 @@ OsciMainMenuBarModel::OsciMainMenuBarModel(OscirenderAudioProcessor& p, Oscirend
void OsciMainMenuBarModel::resetMenuItems() {
MainMenuBarModel::resetMenuItems();
- addTopLevelMenu("File");
- addTopLevelMenu("About");
- addTopLevelMenu("Video");
+ addTopLevelMenu("File"); // index 0
+ addTopLevelMenu("About"); // index 1
+ addTopLevelMenu("Video"); // index 2
if (editor.processor.wrapperType == juce::AudioProcessor::WrapperType::wrapperType_Standalone) {
- addTopLevelMenu("Audio");
+ addTopLevelMenu("Audio"); // index 3 (only if standalone)
}
+ addTopLevelMenu("Interface"); // index 3 (if not standalone) or 4 (if standalone)
addMenuItem(0, "Open Project", [this] { editor.openProject(); });
addMenuItem(0, "Save Project", [this] { editor.saveProject(); });
@@ -60,11 +61,7 @@ void OsciMainMenuBarModel::resetMenuItems() {
});
addMenuItem(1, "Randomize Blender Port", [this] {
audioProcessor.setObjectServerPort(juce::Random::getSystemRandom().nextInt(juce::Range