Initial release — InstaGrain granular synthesizer v1.0
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>
This commit is contained in:
252
Source/PluginEditor.cpp
Normal file
252
Source/PluginEditor.cpp
Normal file
@@ -0,0 +1,252 @@
|
||||
#include "PluginEditor.h"
|
||||
|
||||
InstaGrainEditor::InstaGrainEditor (InstaGrainProcessor& p)
|
||||
: AudioProcessorEditor (&p), processor (p)
|
||||
{
|
||||
setLookAndFeel (&lookAndFeel);
|
||||
setSize (900, 650);
|
||||
setResizable (true, true);
|
||||
setResizeLimits (700, 500, 1400, 1000);
|
||||
|
||||
// Title
|
||||
titleLabel.setText ("INSTAGRAIN", juce::dontSendNotification);
|
||||
titleLabel.setJustificationType (juce::Justification::centredLeft);
|
||||
titleLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::accent);
|
||||
addAndMakeVisible (titleLabel);
|
||||
|
||||
// Version
|
||||
versionLabel.setText (kInstaGrainVersion, juce::dontSendNotification);
|
||||
versionLabel.setJustificationType (juce::Justification::centredLeft);
|
||||
versionLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (versionLabel);
|
||||
|
||||
// Load button
|
||||
loadButton.onClick = [this]
|
||||
{
|
||||
fileChooser = std::make_unique<juce::FileChooser> (
|
||||
"Load Sample", juce::File(), "*.wav;*.aif;*.aiff;*.mp3;*.flac;*.ogg");
|
||||
fileChooser->launchAsync (juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles,
|
||||
[this] (const juce::FileChooser& fc)
|
||||
{
|
||||
auto file = fc.getResult();
|
||||
if (file.existsAsFile())
|
||||
loadSampleFile (file);
|
||||
});
|
||||
};
|
||||
addAndMakeVisible (loadButton);
|
||||
|
||||
// Bypass
|
||||
bypassButton.setToggleState (processor.bypass.load(), juce::dontSendNotification);
|
||||
addAndMakeVisible (bypassButton);
|
||||
bypassLabel.setText ("BYPASS", juce::dontSendNotification);
|
||||
bypassLabel.setJustificationType (juce::Justification::centredLeft);
|
||||
bypassLabel.setColour (juce::Label::textColourId, InstaGrainLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (bypassLabel);
|
||||
|
||||
// Panels
|
||||
addAndMakeVisible (waveformDisplay);
|
||||
addAndMakeVisible (grainPanel);
|
||||
addAndMakeVisible (scatterPanel);
|
||||
addAndMakeVisible (envelopePanel);
|
||||
addAndMakeVisible (effectsPanel);
|
||||
addAndMakeVisible (masterPanel);
|
||||
|
||||
// Waveform position callback
|
||||
waveformDisplay.onPositionChanged = [this] (float pos)
|
||||
{
|
||||
processor.getEngine().position.store (pos);
|
||||
grainPanel.positionKnob.setValue (pos, juce::dontSendNotification);
|
||||
};
|
||||
|
||||
// Initialize waveform if sample already loaded
|
||||
if (processor.getEngine().hasSample())
|
||||
waveformDisplay.setBuffer (&processor.getEngine().getSampleBuffer());
|
||||
|
||||
// Sync knobs from engine state
|
||||
syncKnobsToEngine();
|
||||
|
||||
// Start timer (30 Hz)
|
||||
startTimerHz (30);
|
||||
}
|
||||
|
||||
InstaGrainEditor::~InstaGrainEditor()
|
||||
{
|
||||
setLookAndFeel (nullptr);
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
void InstaGrainEditor::loadSampleFile (const juce::File& file)
|
||||
{
|
||||
processor.getEngine().loadSample (file);
|
||||
waveformDisplay.setBuffer (&processor.getEngine().getSampleBuffer());
|
||||
}
|
||||
|
||||
void InstaGrainEditor::syncKnobsToEngine()
|
||||
{
|
||||
auto& eng = processor.getEngine();
|
||||
grainPanel.positionKnob.setValue (eng.position.load(), juce::dontSendNotification);
|
||||
grainPanel.sizeKnob.setValue (eng.grainSizeMs.load(), juce::dontSendNotification);
|
||||
grainPanel.densityKnob.setValue (eng.density.load(), juce::dontSendNotification);
|
||||
grainPanel.pitchKnob.setValue (eng.pitchSemitones.load(), juce::dontSendNotification);
|
||||
grainPanel.panKnob.setValue (eng.pan.load(), juce::dontSendNotification);
|
||||
grainPanel.rootNoteBox.setSelectedId (eng.rootNote.load() + 1, juce::dontSendNotification);
|
||||
|
||||
scatterPanel.posScatterKnob.setValue (eng.posScatter.load(), juce::dontSendNotification);
|
||||
scatterPanel.sizeScatterKnob.setValue (eng.sizeScatter.load(), juce::dontSendNotification);
|
||||
scatterPanel.pitchScatterKnob.setValue (eng.pitchScatter.load(), juce::dontSendNotification);
|
||||
scatterPanel.panScatterKnob.setValue (eng.panScatter.load(), juce::dontSendNotification);
|
||||
scatterPanel.directionBox.setSelectedId (eng.direction.load() + 1, juce::dontSendNotification);
|
||||
scatterPanel.freezeButton.setToggleState (eng.freeze.load(), juce::dontSendNotification);
|
||||
|
||||
envelopePanel.attackKnob.setValue (eng.attackTime.load(), juce::dontSendNotification);
|
||||
envelopePanel.decayKnob.setValue (eng.decayTime.load(), juce::dontSendNotification);
|
||||
envelopePanel.sustainKnob.setValue (eng.sustainLevel.load(), juce::dontSendNotification);
|
||||
envelopePanel.releaseKnob.setValue (eng.releaseTime.load(), juce::dontSendNotification);
|
||||
|
||||
effectsPanel.filterTypeBox.setSelectedId (eng.filterType.load() + 1, juce::dontSendNotification);
|
||||
effectsPanel.cutoffKnob.setValue (eng.filterCutoff.load(), juce::dontSendNotification);
|
||||
effectsPanel.resoKnob.setValue (eng.filterReso.load(), juce::dontSendNotification);
|
||||
effectsPanel.reverbSizeKnob.setValue (eng.reverbSize.load(), juce::dontSendNotification);
|
||||
effectsPanel.reverbDecayKnob.setValue (eng.reverbDecay.load(), juce::dontSendNotification);
|
||||
|
||||
masterPanel.volumeKnob.setValue (eng.masterVolume.load(), juce::dontSendNotification);
|
||||
bypassButton.setToggleState (processor.bypass.load(), juce::dontSendNotification);
|
||||
}
|
||||
|
||||
void InstaGrainEditor::syncEngineFromKnobs()
|
||||
{
|
||||
auto& eng = processor.getEngine();
|
||||
eng.position.store ((float) grainPanel.positionKnob.getValue());
|
||||
eng.grainSizeMs.store ((float) grainPanel.sizeKnob.getValue());
|
||||
eng.density.store ((float) grainPanel.densityKnob.getValue());
|
||||
eng.pitchSemitones.store ((float) grainPanel.pitchKnob.getValue());
|
||||
eng.pan.store ((float) grainPanel.panKnob.getValue());
|
||||
eng.rootNote.store (grainPanel.rootNoteBox.getSelectedId() - 1);
|
||||
|
||||
eng.posScatter.store ((float) scatterPanel.posScatterKnob.getValue());
|
||||
eng.sizeScatter.store ((float) scatterPanel.sizeScatterKnob.getValue());
|
||||
eng.pitchScatter.store ((float) scatterPanel.pitchScatterKnob.getValue());
|
||||
eng.panScatter.store ((float) scatterPanel.panScatterKnob.getValue());
|
||||
eng.direction.store (scatterPanel.directionBox.getSelectedId() - 1);
|
||||
eng.freeze.store (scatterPanel.freezeButton.getToggleState());
|
||||
|
||||
eng.attackTime.store ((float) envelopePanel.attackKnob.getValue());
|
||||
eng.decayTime.store ((float) envelopePanel.decayKnob.getValue());
|
||||
eng.sustainLevel.store ((float) envelopePanel.sustainKnob.getValue());
|
||||
eng.releaseTime.store ((float) envelopePanel.releaseKnob.getValue());
|
||||
|
||||
eng.filterType.store (effectsPanel.filterTypeBox.getSelectedId() - 1);
|
||||
eng.filterCutoff.store ((float) effectsPanel.cutoffKnob.getValue());
|
||||
eng.filterReso.store ((float) effectsPanel.resoKnob.getValue());
|
||||
eng.reverbSize.store ((float) effectsPanel.reverbSizeKnob.getValue());
|
||||
eng.reverbDecay.store ((float) effectsPanel.reverbDecayKnob.getValue());
|
||||
|
||||
eng.masterVolume.store ((float) masterPanel.volumeKnob.getValue());
|
||||
processor.bypass.store (bypassButton.getToggleState());
|
||||
}
|
||||
|
||||
void InstaGrainEditor::timerCallback()
|
||||
{
|
||||
// GUI → Engine
|
||||
syncEngineFromKnobs();
|
||||
|
||||
// Update waveform visualization
|
||||
auto& eng = processor.getEngine();
|
||||
waveformDisplay.setGrainPosition (eng.position.load());
|
||||
waveformDisplay.setScatterRange (eng.posScatter.load());
|
||||
waveformDisplay.setActiveGrains (eng.getActiveGrainInfo());
|
||||
|
||||
// Update VU meter
|
||||
masterPanel.vuMeter.setLevel (eng.vuLevelL.load(), eng.vuLevelR.load());
|
||||
}
|
||||
|
||||
void InstaGrainEditor::paint (juce::Graphics& g)
|
||||
{
|
||||
// Background
|
||||
g.fillAll (InstaGrainLookAndFeel::bgDark);
|
||||
lookAndFeel.drawBackgroundTexture (g, getLocalBounds());
|
||||
|
||||
// Top bar background
|
||||
float scale = (float) getHeight() / 650.0f;
|
||||
int topBarH = (int) (36.0f * scale);
|
||||
g.setColour (InstaGrainLookAndFeel::bgMedium.withAlpha (0.7f));
|
||||
g.fillRect (0, 0, getWidth(), topBarH);
|
||||
g.setColour (InstaGrainLookAndFeel::bgLight.withAlpha (0.3f));
|
||||
g.drawHorizontalLine (topBarH, 0, (float) getWidth());
|
||||
}
|
||||
|
||||
void InstaGrainEditor::resized()
|
||||
{
|
||||
auto bounds = getLocalBounds();
|
||||
float scale = (float) getHeight() / 650.0f;
|
||||
|
||||
int topBarH = (int) (36.0f * scale);
|
||||
int padding = (int) (6.0f * scale);
|
||||
|
||||
// Top bar
|
||||
auto topBar = bounds.removeFromTop (topBarH).reduced (padding, 0);
|
||||
titleLabel.setFont (lookAndFeel.getBoldFont (20.0f * scale));
|
||||
titleLabel.setBounds (topBar.removeFromLeft ((int) (130 * scale)));
|
||||
versionLabel.setFont (lookAndFeel.getRegularFont (13.0f * scale));
|
||||
versionLabel.setBounds (topBar.removeFromLeft ((int) (50 * scale)));
|
||||
|
||||
// Right side: bypass + load
|
||||
auto bypassArea = topBar.removeFromRight ((int) (30 * scale));
|
||||
bypassButton.setBounds (bypassArea);
|
||||
bypassLabel.setFont (lookAndFeel.getRegularFont (11.0f * scale));
|
||||
bypassLabel.setBounds (topBar.removeFromRight ((int) (50 * scale)));
|
||||
topBar.removeFromRight (padding);
|
||||
loadButton.setBounds (topBar.removeFromRight ((int) (120 * scale)).reduced (0, 2));
|
||||
|
||||
auto content = bounds.reduced (padding);
|
||||
|
||||
// Waveform display (~40%)
|
||||
int waveH = (int) (content.getHeight() * 0.40f);
|
||||
waveformDisplay.setBounds (content.removeFromTop (waveH));
|
||||
content.removeFromTop (padding);
|
||||
|
||||
// Middle row: Grain + Scatter (~25%)
|
||||
int midH = (int) (content.getHeight() * 0.45f);
|
||||
auto midRow = content.removeFromTop (midH);
|
||||
int halfW = midRow.getWidth() / 2;
|
||||
grainPanel.setBounds (midRow.removeFromLeft (halfW - padding / 2));
|
||||
midRow.removeFromLeft (padding);
|
||||
scatterPanel.setBounds (midRow);
|
||||
content.removeFromTop (padding);
|
||||
|
||||
// Bottom row: Envelope + Effects + Master
|
||||
auto botRow = content;
|
||||
int thirdW = botRow.getWidth() / 3;
|
||||
envelopePanel.setBounds (botRow.removeFromLeft (thirdW - padding / 2).reduced (0, 0));
|
||||
botRow.removeFromLeft (padding);
|
||||
effectsPanel.setBounds (botRow.removeFromLeft (thirdW - padding / 2).reduced (0, 0));
|
||||
botRow.removeFromLeft (padding);
|
||||
masterPanel.setBounds (botRow);
|
||||
}
|
||||
|
||||
bool InstaGrainEditor::isInterestedInFileDrag (const juce::StringArray& files)
|
||||
{
|
||||
for (auto& f : files)
|
||||
{
|
||||
auto ext = juce::File (f).getFileExtension().toLowerCase();
|
||||
if (ext == ".wav" || ext == ".aif" || ext == ".aiff" || ext == ".mp3"
|
||||
|| ext == ".flac" || ext == ".ogg")
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void InstaGrainEditor::filesDropped (const juce::StringArray& files, int /*x*/, int /*y*/)
|
||||
{
|
||||
for (auto& f : files)
|
||||
{
|
||||
juce::File file (f);
|
||||
auto ext = file.getFileExtension().toLowerCase();
|
||||
if (ext == ".wav" || ext == ".aif" || ext == ".aiff" || ext == ".mp3"
|
||||
|| ext == ".flac" || ext == ".ogg")
|
||||
{
|
||||
loadSampleFile (file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user