8-voice polyphonic granular synth (VST3/AU/LV2) with: - 128 grain pool per voice, Hann windowing, linear interpolation - Root note selector, sample rate correction, sustain pedal (CC64) - Scatter controls, direction modes (Fwd/Rev/PingPong), freeze - ADSR envelope, global filter (LP/HP/BP), reverb - Waveform display with grain visualization - Drag & drop sample loading, full state save/restore - CI/CD for Windows/macOS/Linux Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
91 sor
3.4 KiB
C++
91 sor
3.4 KiB
C++
#include "ScatterPanel.h"
|
|
|
|
ScatterPanel::ScatterPanel()
|
|
{
|
|
titleLabel.setText ("SCATTER", juce::dontSendNotification);
|
|
titleLabel.setJustificationType (juce::Justification::centredLeft);
|
|
titleLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textPrimary);
|
|
addAndMakeVisible (titleLabel);
|
|
|
|
setupKnob (posScatterKnob, posScatLabel, "Pos");
|
|
setupKnob (sizeScatterKnob, sizeScatLabel, "Size");
|
|
setupKnob (pitchScatterKnob, pitchScatLabel, "Pitch");
|
|
setupKnob (panScatterKnob, panScatLabel, "Pan");
|
|
|
|
directionBox.addItem ("Forward", 1);
|
|
directionBox.addItem ("Reverse", 2);
|
|
directionBox.addItem ("PingPong", 3);
|
|
directionBox.setSelectedId (1);
|
|
addAndMakeVisible (directionBox);
|
|
|
|
dirLabel.setText ("Direction", juce::dontSendNotification);
|
|
dirLabel.setJustificationType (juce::Justification::centred);
|
|
dirLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
|
|
addAndMakeVisible (dirLabel);
|
|
|
|
freezeButton.setButtonText ("Freeze");
|
|
addAndMakeVisible (freezeButton);
|
|
|
|
freezeLabel.setText ("Freeze", juce::dontSendNotification);
|
|
freezeLabel.setJustificationType (juce::Justification::centred);
|
|
freezeLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
|
|
addAndMakeVisible (freezeLabel);
|
|
}
|
|
|
|
void ScatterPanel::setupKnob (juce::Slider& knob, juce::Label& label, const juce::String& name)
|
|
{
|
|
knob.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
|
|
knob.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 50, 14);
|
|
knob.setRange (0.0, 1.0);
|
|
knob.setValue (0.0);
|
|
knob.getProperties().set (InstaGrainLookAndFeel::knobTypeProperty, "dark");
|
|
addAndMakeVisible (knob);
|
|
|
|
label.setText (name, juce::dontSendNotification);
|
|
label.setJustificationType (juce::Justification::centred);
|
|
label.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
|
|
addAndMakeVisible (label);
|
|
}
|
|
|
|
void ScatterPanel::resized()
|
|
{
|
|
auto bounds = getLocalBounds().reduced (4);
|
|
titleLabel.setBounds (bounds.removeFromTop (20));
|
|
|
|
// Top row: 4 knobs
|
|
int knobW = bounds.getWidth() / 4;
|
|
int knobH = (bounds.getHeight() - 30) * 2 / 3;
|
|
auto topRow = bounds.removeFromTop (knobH);
|
|
|
|
juce::Slider* knobs[] = { &posScatterKnob, &sizeScatterKnob, &pitchScatterKnob, &panScatterKnob };
|
|
juce::Label* labels[] = { &posScatLabel, &sizeScatLabel, &pitchScatLabel, &panScatLabel };
|
|
|
|
for (int i = 0; i < 4; ++i)
|
|
{
|
|
auto col = topRow.removeFromLeft (knobW);
|
|
knobs[i]->setBounds (col.reduced (2));
|
|
labels[i]->setBounds (col.getX(), col.getBottom() - 2, col.getWidth(), 16);
|
|
}
|
|
|
|
// Bottom row: Direction combo + Freeze toggle
|
|
auto botRow = bounds.reduced (2);
|
|
int halfW = botRow.getWidth() / 2;
|
|
|
|
auto dirArea = botRow.removeFromLeft (halfW);
|
|
dirLabel.setBounds (dirArea.removeFromTop (14));
|
|
directionBox.setBounds (dirArea.reduced (4, 2));
|
|
|
|
auto freezeArea = botRow;
|
|
freezeLabel.setBounds (freezeArea.removeFromTop (14));
|
|
freezeButton.setBounds (freezeArea.reduced (4, 2));
|
|
}
|
|
|
|
void ScatterPanel::paint (juce::Graphics& g)
|
|
{
|
|
auto bounds = getLocalBounds().toFloat();
|
|
g.setColour (InstaGrainLookAndFeel::bgMedium.withAlpha (0.5f));
|
|
g.fillRoundedRectangle (bounds, 4.0f);
|
|
g.setColour (InstaGrainLookAndFeel::bgLight.withAlpha (0.3f));
|
|
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
|
|
}
|