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:
hariel1985
2026-03-26 17:26:06 +01:00
commit 55b5f89ac5
34 fájl változott, egészen pontosan 2728 új sor hozzáadva és 0 régi sor törölve

252
Source/PluginEditor.cpp Normal file
Fájl megtekintése

@@ -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;
}
}
}