#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 ( "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; } } }