#include "PluginEditor.h" #include void KnobLookAndFeel::drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height, float sliderPos, float rotaryStartAngle, float rotaryEndAngle, juce::Slider& slider) { auto bounds = juce::Rectangle(x, y, width, height).toFloat(); float labelH = 20.0f; auto dialBounds = bounds.withTrimmedTop(labelH); auto radius = juce::jmin(dialBounds.getWidth(), dialBounds.getHeight()) / 2.0f - 4.0f; auto centreX = dialBounds.getCentreX(); auto centreY = dialBounds.getCentreY(); auto rx = centreX - radius; auto ry = centreY - radius; auto rw = radius * 2.0f; auto angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle); // Drop shadow g.setColour(juce::Colour(0x40000000)); g.fillEllipse(rx + 2.0f, ry + 3.0f, rw, rw); // Outer bevel (dark bottom-right, light top-left) auto bevelPath = juce::Path(); bevelPath.addEllipse(rx, ry, rw, rw); // Knob body gradient — light top-left to dark bottom-right for 3D bulge juce::ColourGradient bodyGrad(juce::Colour(0xff404040), rx, ry, juce::Colour(0xff1a1a1a), rx + rw, ry + rw, true); g.setGradientFill(bodyGrad); g.fillEllipse(rx, ry, rw, rw); // Soft inner shadow ring (bottom-right dark edge, subtle) g.setColour(juce::Colour(0x18000000)); auto shadowArc = juce::Path(); float innerR = radius - 2.0f; shadowArc.addCentredArc(centreX, centreY, innerR, innerR, 0.0f, 0.5f, 2.7f, true); g.strokePath(shadowArc, juce::PathStrokeType(1.5f)); // Outer rim g.setColour(juce::Colour(0xff555555)); g.drawEllipse(rx, ry, rw, rw, 1.5f); // Pointer line with glow — green (left) → yellow (mid) → red (right) auto indicatorColour = juce::Colour::fromHSV(0.33f * (1.0f - sliderPos), 0.85f, 0.9f, 1.0f); auto glowColour = juce::Colour::fromHSV(0.33f * (1.0f - sliderPos), 0.85f, 0.9f, 0.4f); g.setColour(glowColour); juce::Path pointerGlow; pointerGlow.addRoundedRectangle(-2.5f, -radius + 9, 5.0f, radius * 0.5f, 2.0f); g.fillPath(pointerGlow, juce::AffineTransform::rotation(angle).translated(centreX, centreY)); g.setColour(indicatorColour); juce::Path pointer; pointer.addRoundedRectangle(-1.5f, -radius + 9, 3.0f, radius * 0.5f, 1.5f); g.fillPath(pointer, juce::AffineTransform::rotation(angle).translated(centreX, centreY)); // Label above knob auto name = slider.getName(); if (name.isNotEmpty()) { g.setColour(juce::Colour(0xff999999)); g.setFont(juce::Font(13.0f).boldened()); g.drawText(name, bounds.getX(), bounds.getY(), bounds.getWidth(), labelH, juce::Justification::centredBottom); } } void ComboBoxLookAndFeel::drawComboBox(juce::Graphics& g, int width, int height, bool isButtonDown, int /*buttonX*/, int /*buttonY*/, int /*buttonW*/, int /*buttonH*/, juce::ComboBox& /*box*/) { auto bounds = juce::Rectangle(0, 0, width, height).toFloat(); // Drop shadow g.setColour(juce::Colour(0x40000000)); g.fillRoundedRectangle(bounds.getX() + 1.5f, bounds.getY() + 2.0f, bounds.getWidth(), bounds.getHeight(), 4.0f); // Body gradient — light top to dark bottom for 3D raised look juce::ColourGradient bodyGrad(juce::Colour(0xff404040), bounds.getCentreX(), bounds.getY(), juce::Colour(0xff1a1a1a), bounds.getCentreX(), bounds.getBottom(), true); g.setGradientFill(bodyGrad); g.fillRoundedRectangle(bounds, 4.0f); // Top highlight edge g.setColour(juce::Colour(0x30ffffff)); g.drawRoundedRectangle(bounds.getX() + 0.5f, bounds.getY() + 0.5f, bounds.getWidth() - 1.0f, bounds.getHeight() - 1.0f, 4.0f, 1.0f); // Bottom shadow edge g.setColour(juce::Colour(0x30000000)); g.drawLine(bounds.getX() + 4.0f, bounds.getBottom() - 0.5f, bounds.getRight() - 4.0f, bounds.getBottom() - 0.5f, 1.0f); // Outline g.setColour(juce::Colour(0xff555555)); g.drawRoundedRectangle(bounds, 4.0f, 1.0f); // Arrow (gold triangle on right) float arrowSize = 6.0f; float arrowX = bounds.getRight() - 16.0f; float arrowY = bounds.getCentreY(); juce::Path arrow; arrow.addTriangle(arrowX, arrowY - arrowSize / 2, arrowX + arrowSize, arrowY, arrowX, arrowY + arrowSize / 2); g.setColour(juce::Colour(0xffccaa44)); g.fillPath(arrow); } void ComboBoxLookAndFeel::drawPopupMenuItem(juce::Graphics& g, const juce::Rectangle& area, bool isSeparator, bool /*isActive*/, bool isHighlighted, bool /*isTicked*/, bool /*hasSubMenu*/, const juce::String& text, const juce::String& /*shortcutKeyText*/, const juce::Drawable* /*icon*/, const juce::Colour* /*textColour*/) { if (isSeparator) { g.setColour(juce::Colour(0xff444444)); g.drawLine(area.getX() + 8.0f, area.getCentreY(), area.getRight() - 8.0f, area.getCentreY(), 1.0f); return; } if (isHighlighted) { g.setColour(juce::Colour(0xffccaa44)); g.fillRoundedRectangle(area.reduced(2).toFloat(), 3.0f); } g.setColour(isHighlighted ? juce::Colour(0xff1a1a1a) : juce::Colour(0xffcccccc)); g.setFont(juce::Font(13.0f)); g.drawText(text, area.reduced(8, 0), juce::Justification::centredLeft, true); } // --- VU Meter --- void VuMeter::paint(juce::Graphics& g) { auto bounds = getLocalBounds().toFloat(); juce::ColourGradient bgGrad(juce::Colour(0xff222222), bounds.getCentreX(), bounds.getY(), juce::Colour(0xff111111), bounds.getCentreX(), bounds.getBottom(), false); g.setGradientFill(bgGrad); g.fillRoundedRectangle(bounds, 3.0f); float level = processor.getRmsLevel(); float clamped = juce::jlimit(0.0f, 1.0f, level); float barH = bounds.getHeight() * clamped; auto barBounds = juce::Rectangle(bounds.getX(), bounds.getBottom() - barH, bounds.getWidth(), barH); auto innerBar = barBounds.reduced(1.0f); { juce::Graphics::ScopedSaveState saved(g); g.setOpacity(0.7f); juce::ColourGradient grad(juce::Colour(0xff00cc44), 0.0f, bounds.getBottom(), juce::Colour(0xffcc2200), 0.0f, bounds.getY(), false); g.setGradientFill(grad); g.fillRoundedRectangle(innerBar, 2.0f); } g.setColour(juce::Colour(0xff444444)); g.drawRoundedRectangle(bounds, 3.0f, 1.0f); g.setColour(juce::Colour(0xffccaa44)); g.setFont(juce::Font(13.0f).boldened()); g.drawText("VU", bounds.withBottom(bounds.getY() + 22.0f).translated(0, 6), juce::Justification::centred); } // --- Waveform Display --- void WaveformDisplay::paint(juce::Graphics& g) { auto bounds = getLocalBounds().toFloat(); juce::ColourGradient bgGrad(juce::Colour(0xff222222), bounds.getCentreX(), bounds.getY(), juce::Colour(0xff111111), bounds.getCentreX(), bounds.getBottom(), false); g.setGradientFill(bgGrad); g.fillRoundedRectangle(bounds, 3.0f); g.setColour(juce::Colour(0xff555555)); g.drawRoundedRectangle(bounds, 3.0f, 1.0f); g.setColour(juce::Colour(0xff333333)); g.drawLine(bounds.getX(), bounds.getCentreY(), bounds.getRight(), bounds.getCentreY(), 1.0f); int writePos = processor.scopeWritePos.load(std::memory_order_acquire); int bufSize = ChromaFlockProcessor::scopeBufferSize; float w = bounds.getWidth(); float h = bounds.getHeight(); float midY = bounds.getY() + h * 0.5f; juce::Path wavePath; bool started = false; for (int i = 0; i < bufSize; ++i) { int idx = (writePos + i) % bufSize; float x = bounds.getX() + (static_cast(i) / static_cast(bufSize)) * w; float sample = processor.scopeBuffer[idx]; float y = midY - sample * h * 0.45f; if (!started) { wavePath.startNewSubPath(x, y); started = true; } else { wavePath.lineTo(x, y); } } juce::Path filledPath(wavePath); filledPath.lineTo(bounds.getRight(), midY); filledPath.lineTo(bounds.getX(), midY); filledPath.closeSubPath(); { juce::Graphics::ScopedSaveState saved(g); g.setOpacity(0.7f); juce::ColourGradient waveGrad(juce::Colour(0xff00ff88), 0.0f, bounds.getY(), juce::Colour(0xff005522), 0.0f, bounds.getBottom(), false); g.setGradientFill(waveGrad); g.fillPath(filledPath); g.setColour(juce::Colour(0xff00ff88).withAlpha(0.8f)); g.strokePath(wavePath, juce::PathStrokeType(1.5f)); } g.setColour(juce::Colour(0xffccaa44)); g.setFont(juce::Font(13.0f).boldened()); g.drawText("WAVE", bounds.withBottom(bounds.getY() + 22.0f).translated(0, 6), juce::Justification::centred); } // --- Spectrum Analyzer --- void SpectrumAnalyzer::paint(juce::Graphics& g) { auto bounds = getLocalBounds().toFloat(); juce::ColourGradient bgGrad(juce::Colour(0xff222222), bounds.getCentreX(), bounds.getY(), juce::Colour(0xff111111), bounds.getCentreX(), bounds.getBottom(), false); g.setGradientFill(bgGrad); g.fillRoundedRectangle(bounds, 3.0f); g.setColour(juce::Colour(0xff555555)); g.drawRoundedRectangle(bounds, 3.0f, 1.0f); std::array fftData{}; int writePos = processor.fftWritePos.load(std::memory_order_acquire); int fftSize = ChromaFlockProcessor::fftSize; for (int i = 0; i < fftSize; ++i) { int idx = (writePos + i) % fftSize; fftData[i] = processor.fftInput[idx]; } for (int i = 0; i < fftSize; ++i) { float window = 0.5f - 0.5f * std::cos(2.0f * 3.14159265f * static_cast(i) / static_cast(fftSize)); fftData[i] *= window; } fft->performFrequencyOnlyForwardTransform(fftData.data()); int numBars = 48; float w = bounds.getWidth(); float h = bounds.getHeight() - 22.0f; float barBottom = bounds.getBottom() - 2.0f; float barW = w / static_cast(numBars); int maxBin = fftSize / 4; for (int bar = 0; bar < numBars; ++bar) { float t = static_cast(bar) / static_cast(numBars); float tNext = static_cast(bar + 1) / static_cast(numBars); int binStart = static_cast(std::pow(t, 2.0f) * static_cast(maxBin)); int binEnd = static_cast(std::pow(tNext, 2.0f) * static_cast(maxBin)); if (binEnd <= binStart) binEnd = binStart + 1; if (binEnd > maxBin) binEnd = maxBin; float mag = 0.0f; int count = 0; for (int b = binStart; b < binEnd; ++b) { mag += fftData[b]; ++count; } mag = count > 0 ? mag / static_cast(count) : 0.0f; mag = mag / static_cast(fftSize); mag = std::sqrt(mag) * 6.0f; mag = juce::jlimit(0.0f, 1.0f, mag); float barH = mag * h; float x = bounds.getX() + static_cast(bar) * barW; float bw = barW - 2.0f; float bx = x + 1.0f; float by = barBottom - barH; { juce::Graphics::ScopedSaveState saved(g); g.setOpacity(0.7f); juce::ColourGradient barGrad(juce::Colour(0xff00ff66), 0.0f, by, juce::Colour(0xff005522), 0.0f, barBottom, false); g.setGradientFill(barGrad); g.fillRect(bx, by, bw, barH); } } g.setColour(juce::Colour(0xffccaa44)); g.setFont(juce::Font(13.0f).boldened()); g.drawText("SPECTRUM", bounds.withBottom(bounds.getY() + 22.0f).translated(0, 6), juce::Justification::centred); } MainContentComponent::MainContentComponent(ChromaFlockProcessor& p) : processorRef(p), vuMeter(p), waveformDisplay(p), spectrumAnalyzer(p), pianoRoll(p) { auto setupParam = [&](juce::Slider& knob, std::unique_ptr& attach, const juce::String& paramId, const juce::String& name) { setupKnob(knob, name); attach = std::make_unique(processorRef.apvts, paramId, knob); }; auto setupCB = [&](juce::ComboBox& box, std::unique_ptr& attach, const juce::String& paramId, const juce::StringArray& items) { setupCombo(box); box.addItemList(items, 1); attach = std::make_unique(processorRef.apvts, paramId, box); }; setupLabel(osc1Label, "OSC 1"); setupCB(osc1WaveBox, osc1WaveAttach, "osc1Wave", {"Sine", "Saw", "Square", "Triangle", "Noise"}); setupParam(osc1OctKnob, osc1OctAttach, "osc1Oct", "OCT"); setupParam(osc1SemiKnob, osc1SemiAttach, "osc1Semi", "SEMI"); setupParam(osc1FineKnob, osc1FineAttach, "osc1Fine", "FINE"); setupParam(osc1LevelKnob, osc1LevelAttach, "osc1Level", "LEVEL"); setupLabel(osc2Label, "OSC 2"); setupCB(osc2WaveBox, osc2WaveAttach, "osc2Wave", {"Sine", "Saw", "Square", "Triangle", "Noise"}); setupParam(osc2OctKnob, osc2OctAttach, "osc2Oct", "OCT"); setupParam(osc2SemiKnob, osc2SemiAttach, "osc2Semi", "SEMI"); setupParam(osc2FineKnob, osc2FineAttach, "osc2Fine", "FINE"); setupParam(osc2LevelKnob, osc2LevelAttach, "osc2Level", "LEVEL"); setupParam(phaseOffsetKnob, phaseOffsetAttach, "phaseOffset", "PHASE"); setupLabel(filterLabel, "FILTER"); setupCB(filterTypeBox, filterTypeAttach, "filterType", {"LP 12dB", "LP 24dB", "Band Pass", "High Pass", "Notch"}); setupParam(filterCutoffKnob, filterCutoffAttach, "filterCutoff", "CUTOFF"); setupParam(filterResKnob, filterResAttach, "filterRes", "RES"); setupParam(filterEnvAmtKnob, filterEnvAmtAttach, "filterEnvAmt", "ENV AMT"); setupParam(keyTrackKnob, keyTrackAttach, "keyTrack", "KEY TRK"); setupLabel(envLabel, "AMP ENV"); setupParam(envAttackKnob, envAttackAttach, "envAttack", "ATTACK"); setupParam(envDecayKnob, envDecayAttach, "envDecay", "DECAY"); setupParam(envSustainKnob, envSustainAttach, "envSustain", "SUSTAIN"); setupParam(envReleaseKnob, envReleaseAttach, "envRelease", "RELEASE"); setupLabel(fEnvLabel, "FILTER ENV"); setupParam(fEnvAttackKnob, fEnvAttackAttach, "fEnvAttack", "ATTACK"); setupParam(fEnvDecayKnob, fEnvDecayAttach, "fEnvDecay", "DECAY"); setupParam(fEnvSustainKnob, fEnvSustainAttach, "fEnvSustain", "SUSTAIN"); setupParam(fEnvReleaseKnob, fEnvReleaseAttach, "fEnvRelease", "RELEASE"); setupLabel(globalLabel, "MASTER"); setupParam(panKnob, panAttach, "pan", "PAN"); setupParam(driveKnob, driveAttach, "drive", "DRIVE"); setupParam(masterKnob, masterAttach, "masterLevel", "LEVEL"); setupParam(pbRangeKnob, pbRangeAttach, "pitchBendRange", "PB RANGE"); // FX setupCB(distTypeBox, distTypeAttach, "distType", {"Soft Clip", "Hard Clip", "Foldback", "Overdrive"}); setupParam(distAmountKnob, distAmountAttach, "distAmount", "AMOUNT"); setupParam(distMixKnob, distMixAttach, "distMix", "MIX"); setupParam(compThresholdKnob, compThresholdAttach, "compThreshold", "THRESH"); setupParam(compRatioKnob, compRatioAttach, "compRatio", "RATIO"); setupParam(compAttackKnob, compAttackAttach, "compAttack", "ATTACK"); setupParam(compReleaseKnob, compReleaseAttach, "compRelease", "RELEASE"); setupParam(compMakeupKnob, compMakeupAttach, "compMakeup", "MAKEUP"); setupParam(compMixKnob, compMixAttach, "compMix", "MIX"); setupParam(autoPanRateKnob, autoPanRateAttach, "autoPanRate", "RATE"); setupParam(autoPanDepthKnob, autoPanDepthAttach, "autoPanDepth", "DEPTH"); // LFO setupParam(lfo1RateKnob, lfo1RateAttach, "lfo1Rate", "RATE"); setupParam(lfo1DepthKnob, lfo1DepthAttach, "lfo1Depth", "DEPTH"); setupCB(lfo1ShapeBox, lfo1ShapeAttach, "lfo1Shape", {"Sine", "Triangle", "Saw", "Square"}); setupCB(lfo1DestBox, lfo1DestAttach, "lfo1Dest", {"Filter", "OSC1", "OSC2", "Both OSC"}); setupParam(lfo2RateKnob, lfo2RateAttach, "lfo2Rate", "RATE"); setupParam(lfo2DepthKnob, lfo2DepthAttach, "lfo2Depth", "DEPTH"); setupCB(lfo2ShapeBox, lfo2ShapeAttach, "lfo2Shape", {"Sine", "Triangle", "Saw", "Square"}); setupCB(lfo2DestBox, lfo2DestAttach, "lfo2Dest", {"Filter", "OSC1", "OSC2", "Both OSC"}); // FX2 setupParam(delayTimeKnob, delayTimeAttach, "delayTime", "TIME"); setupParam(delayFbKnob, delayFbAttach, "delayFeedback", "FBACK"); setupParam(delayMixKnob, delayMixAttach, "delayMix", "MIX"); setupCB(delayPingPongBox, delayPingPongAttach, "delayPingPong", {"Off", "On"}); setupParam(delaySpreadKnob, delaySpreadAttach, "delaySpread", "SPREAD"); setupParam(reverbSizeKnob, reverbSizeAttach, "reverbSize", "SIZE"); setupParam(reverbDampKnob, reverbDampAttach, "reverbDamping", "DAMP"); setupParam(reverbMixKnob, reverbMixAttach, "reverbMix", "MIX"); // Preset button presetButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a2a)); presetButton.setColour(juce::TextButton::textColourOffId, juce::Colour(0xffccaa44)); presetButton.setLookAndFeel(&comboLaf); presetButton.onClick = [this]() { showPresetMenu(); }; addAndMakeVisible(presetButton); scaleLabel.setText("SCALE", juce::dontSendNotification); scaleLabel.setFont(juce::Font(9.0f)); scaleLabel.setColour(juce::Label::textColourId, juce::Colour(0xff888888)); scaleLabel.setJustificationType(juce::Justification::centredRight); addAndMakeVisible(scaleLabel); addAndMakeVisible(vuMeter); addAndMakeVisible(waveformDisplay); addAndMakeVisible(spectrumAnalyzer); addAndMakeVisible(pianoRoll); setupCombo(uiScaleBox); uiScaleBox.addItem("100%", 1); uiScaleBox.addItem("125%", 2); uiScaleBox.addItem("150%", 3); uiScaleBox.addItem("200%", 4); uiScaleBox.setSelectedId(1, juce::dontSendNotification); uiScaleBox.onChange = [this]() { static const float scales[] = {1.0f, 1.25f, 1.5f, 2.0f}; int idx = uiScaleBox.getSelectedId() - 1; if (idx >= 0 && idx < 4 && onScaleChanged) onScaleChanged(scales[idx]); }; } void MainContentComponent::setupLabel(juce::Label& label, const juce::String& text) { label.setText(text, juce::dontSendNotification); label.setFont(juce::Font(13.0f).boldened()); label.setColour(juce::Label::textColourId, juce::Colour(0xffccaa44)); label.setJustificationType(juce::Justification::centred); addAndMakeVisible(label); } void MainContentComponent::setupKnob(juce::Slider& knob, const juce::String& name) { knob.setSliderStyle(juce::Slider::RotaryVerticalDrag); knob.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 55, 16); knob.setLookAndFeel(&knobLaf); knob.setName(name); knob.setColour(juce::Slider::textBoxTextColourId, juce::Colour(0xffcccccc)); knob.setColour(juce::Slider::textBoxBackgroundColourId, juce::Colour(0xff1a1a1a)); knob.setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentBlack); addAndMakeVisible(knob); } void MainContentComponent::setupCombo(juce::ComboBox& combo) { combo.setEditableText(false); combo.setLookAndFeel(&comboLaf); combo.setColour(juce::ComboBox::backgroundColourId, juce::Colour(0xff2a2a2a)); combo.setColour(juce::ComboBox::textColourId, juce::Colour(0xffcccccc)); combo.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff555555)); combo.setColour(juce::ComboBox::arrowColourId, juce::Colour(0xffccaa44)); combo.setColour(juce::PopupMenu::backgroundColourId, juce::Colour(0xff2a2a2a)); combo.setColour(juce::PopupMenu::textColourId, juce::Colour(0xffcccccc)); combo.setColour(juce::PopupMenu::highlightedBackgroundColourId, juce::Colour(0xffccaa44)); combo.setColour(juce::PopupMenu::highlightedTextColourId, juce::Colour(0xff1a1a1a)); addAndMakeVisible(combo); } void MainContentComponent::showPresetMenu() { juce::PopupMenu menu; auto categories = processorRef.presetManager.getCategories(); int menuId = 1; for (auto& cat : categories) { juce::PopupMenu subMenu; auto presets = processorRef.presetManager.getPresetsInCategory(cat); for (auto& preset : presets) { subMenu.addItem(menuId++, preset.name); } menu.addSubMenu(cat, subMenu); } menu.showMenuAsync(juce::PopupMenu::Options(), [this](int result) { if (result > 0) { auto& presets = processorRef.presetManager.getPresets(); int idx = 0; for (int i = 0; i < result - 1 && i < static_cast(presets.size()); ++i) ++idx; if (idx < static_cast(presets.size())) processorRef.presetManager.applyPreset(presets[idx].name, processorRef.apvts); } }); } void MainContentComponent::paint(juce::Graphics& g) { g.fillAll(juce::Colour(0xff1a1a1a)); // Draw background image tiled if (!bgImageLoaded) { bgImage = juce::ImageFileFormat::loadFrom( BinaryData::chromaflockbg5_png, BinaryData::chromaflockbg5_pngSize); bgImageLoaded = true; } if (bgImage.isValid()) { int imgW = bgImage.getWidth(); int imgH = bgImage.getHeight(); int compW = getWidth(); int compH = getHeight(); g.setOpacity(0.35f); for (int x = 0; x < compW; x += imgW) for (int y = 0; y < compH; y += imgH) g.drawImageAt(bgImage, x, y); g.setOpacity(1.0f); } // Load logo if (!logoLoaded) { logoImage = juce::ImageFileFormat::loadFrom( BinaryData::cfnew_png, BinaryData::cfnew_pngSize); logoLoaded = true; } if (logoImage.isValid()) { int logoH = 124; int logoW = static_cast(logoH * 805.0 / 119.0); int logoX = (getWidth() - logoW) / 2; auto logoArea = juce::Rectangle(logoX, 0, logoW, logoH); g.drawImage(logoImage, logoArea.toFloat(), juce::RectanglePlacement::centred); } g.setColour(juce::Colour(0xffccaa44)); g.drawHorizontalLine(124, 10.0f, 1650.0f); auto drawSection = [&](int x, int y, int w, int h) { auto rect = juce::Rectangle(static_cast(x), static_cast(y), static_cast(w), static_cast(h)); // 3D fill gradient: slight light top, dark bottom juce::ColourGradient fillGrad(juce::Colour(0xff222222), rect.getCentreX(), rect.getY(), juce::Colour(0xff111111), rect.getCentreX(), rect.getBottom(), false); g.setGradientFill(fillGrad); g.fillRoundedRectangle(rect, 4.0f); // Border g.setColour(juce::Colour(0xff333333)); g.drawRoundedRectangle(rect, 4.0f, 1.0f); }; drawSection(10, 128, 498, 200); drawSection(520, 128, 620, 200); drawSection(1152, 128, 498, 200); drawSection(75, 338, 498, 150); drawSection(585, 338, 498, 150); drawSection(1095, 338, 490, 150); drawSection(10, 900, 1640, 128); drawSection(10, 1038, 1640, 136); // FX sub-sections auto drawSubSection = [&](int x, int y, int w, int h, const juce::String& title, int titlePadY = 2) { auto rect = juce::Rectangle(static_cast(x), static_cast(y), static_cast(w), static_cast(h)); juce::ColourGradient fillGrad(juce::Colour(0xff222222), rect.getCentreX(), rect.getY(), juce::Colour(0xff111111), rect.getCentreX(), rect.getBottom(), false); g.setGradientFill(fillGrad); g.fillRoundedRectangle(rect, 3.0f); g.setColour(juce::Colour(0xff333333)); g.drawRoundedRectangle(rect, 3.0f, 1.0f); g.setColour(juce::Colour(0xffccaa44)); g.setFont(juce::Font(11.0f).boldened()); g.drawText(title, x + 6, y + titlePadY, w - 12, 14, juce::Justification::centred); }; drawSubSection(15, 524, 395, 140, "DISTORTION", 6); drawSubSection(420, 524, 660, 140, "COMPRESSOR", 6); drawSubSection(1090, 524, 240, 140, "AUTO-PAN", 6); // LFO sub-sections drawSubSection(15, 690, 530, 150, "LFO 1", 4); drawSubSection(560, 690, 530, 150, "LFO 2", 4); // FX2 sub-sections drawSubSection(1100, 690, 265, 200, "DELAY", 4); drawSubSection(1375, 690, 265, 200, "REVERB", 4); } void MainContentComponent::resized() { auto sectionY = 132; auto knobSize = 112; auto knobSpacing = 10; auto labelH = 18; auto comboH = 48; osc1Label.setBounds(10, sectionY, 498, labelH); osc1WaveBox.setBounds(20, sectionY + labelH + 4, 200, comboH); int knobY1 = sectionY + labelH + comboH + 8; osc1OctKnob.setBounds(20, knobY1, knobSize, knobSize); osc1SemiKnob.setBounds(20 + knobSize + knobSpacing, knobY1, knobSize, knobSize); osc1FineKnob.setBounds(20 + (knobSize + knobSpacing) * 2, knobY1, knobSize, knobSize); osc1LevelKnob.setBounds(20 + (knobSize + knobSpacing) * 3, knobY1, knobSize, knobSize); osc2Label.setBounds(520, sectionY, 620, labelH); osc2WaveBox.setBounds(530, sectionY + labelH + 4, 200, comboH); int knobY2 = sectionY + labelH + comboH + 8; osc2OctKnob.setBounds(530, knobY2, knobSize, knobSize); osc2SemiKnob.setBounds(530 + knobSize + knobSpacing, knobY2, knobSize, knobSize); osc2FineKnob.setBounds(530 + (knobSize + knobSpacing) * 2, knobY2, knobSize, knobSize); osc2LevelKnob.setBounds(530 + (knobSize + knobSpacing) * 3, knobY2, knobSize, knobSize); phaseOffsetKnob.setBounds(530 + (knobSize + knobSpacing) * 4, knobY2, knobSize, knobSize); filterLabel.setBounds(1152, sectionY, 498, labelH); filterTypeBox.setBounds(1162, sectionY + labelH + 4, 200, comboH); int knobYF = sectionY + labelH + comboH + 8; filterCutoffKnob.setBounds(1162, knobYF, knobSize, knobSize); filterResKnob.setBounds(1162 + knobSize + knobSpacing, knobYF, knobSize, knobSize); filterEnvAmtKnob.setBounds(1162 + (knobSize + knobSpacing) * 2, knobYF, knobSize, knobSize); keyTrackKnob.setBounds(1162 + (knobSize + knobSpacing) * 3, knobYF, knobSize, knobSize); envLabel.setBounds(75, 352, 498, labelH); int knobYA = 352 + labelH + 6; envAttackKnob.setBounds(85, knobYA, knobSize, knobSize); envDecayKnob.setBounds(85 + knobSize + knobSpacing, knobYA, knobSize, knobSize); envSustainKnob.setBounds(85 + (knobSize + knobSpacing) * 2, knobYA, knobSize, knobSize); envReleaseKnob.setBounds(85 + (knobSize + knobSpacing) * 3, knobYA, knobSize, knobSize); fEnvLabel.setBounds(585, 352, 498, labelH); int knobYFE = 352 + labelH + 6; fEnvAttackKnob.setBounds(595, knobYFE, knobSize, knobSize); fEnvDecayKnob.setBounds(595 + knobSize + knobSpacing, knobYFE, knobSize, knobSize); fEnvSustainKnob.setBounds(595 + (knobSize + knobSpacing) * 2, knobYFE, knobSize, knobSize); fEnvReleaseKnob.setBounds(595 + (knobSize + knobSpacing) * 3, knobYFE, knobSize, knobSize); globalLabel.setBounds(1095, 352, 490, labelH); int knobYM = 352 + labelH + 6; panKnob.setBounds(1105, knobYM, knobSize, knobSize); driveKnob.setBounds(1105 + knobSize + knobSpacing, knobYM, knobSize, knobSize); masterKnob.setBounds(1105 + (knobSize + knobSpacing) * 2, knobYM, knobSize, knobSize); pbRangeKnob.setBounds(1105 + (knobSize + knobSpacing) * 3, knobYM, knobSize, knobSize); // --- FX SECTION --- int fxY = 496; int fxKnobSize = 100; int fxLabelH = 16; int fxKnobY = fxY + 54; // sub-section y(476) + titlePad(6) + titleH(14) + bottomPad(6) + 4 fxLabel.setBounds(10, fxY + 8, 1640, fxLabelH); // Distortion sub-section (X=15, Y=476, W=395, H=140) distTypeBox.setBounds(22, fxKnobY + 2, 140, 36); distAmountKnob.setBounds(170, fxKnobY, fxKnobSize, fxKnobSize); distMixKnob.setBounds(280, fxKnobY, fxKnobSize, fxKnobSize); // Compressor sub-section (X=420, Y=476, W=660, H=140) compThresholdKnob.setBounds(428, fxKnobY, fxKnobSize, fxKnobSize); compRatioKnob.setBounds(538, fxKnobY, fxKnobSize, fxKnobSize); compAttackKnob.setBounds(648, fxKnobY, fxKnobSize, fxKnobSize); compReleaseKnob.setBounds(758, fxKnobY, fxKnobSize, fxKnobSize); compMakeupKnob.setBounds(868, fxKnobY, fxKnobSize, fxKnobSize); compMixKnob.setBounds(978, fxKnobY, fxKnobSize, fxKnobSize); // Auto-Pan sub-section (X=1090, Y=476, W=240, H=140) autoPanRateKnob.setBounds(1098, fxKnobY, fxKnobSize, fxKnobSize); autoPanDepthKnob.setBounds(1098 + fxKnobSize + 8, fxKnobY, fxKnobSize, fxKnobSize); // --- LFO SECTION --- int lfoY = 670; int lfoKnobSize = 80; int lfoKnobY = 690 + 4 + 14 + 6; // subSectionY + titlePad + titleH + bottomPad lfoLabel.setBounds(10, lfoY, 1080, fxLabelH); fx2Label.setBounds(1100, lfoY, 540, fxLabelH); // LFO 1 sub-section (X=15, Y=642, W=530, H=110) lfo1RateKnob.setBounds(22, lfoKnobY, lfoKnobSize, lfoKnobSize); lfo1DepthKnob.setBounds(132, lfoKnobY, lfoKnobSize, lfoKnobSize); lfo1ShapeBox.setBounds(242, lfoKnobY + 2, 130, 36); lfo1DestBox.setBounds(382, lfoKnobY + 2, 155, 36); // LFO 2 sub-section (X=560, Y=642, W=530, H=110) lfo2RateKnob.setBounds(568, lfoKnobY, lfoKnobSize, lfoKnobSize); lfo2DepthKnob.setBounds(678, lfoKnobY, lfoKnobSize, lfoKnobSize); lfo2ShapeBox.setBounds(788, lfoKnobY + 2, 130, 36); lfo2DestBox.setBounds(928, lfoKnobY + 2, 155, 36); // --- FX2 SECTION --- int fx2KnobY = lfoKnobY; // Delay sub-section (X=1100, Y=690, W=265, H=130) // Row 1: TIME, FBACK, MIX delayTimeKnob.setBounds(1110, fx2KnobY, lfoKnobSize, lfoKnobSize); delayFbKnob.setBounds(1198, fx2KnobY, lfoKnobSize, lfoKnobSize); delayMixKnob.setBounds(1286, fx2KnobY, lfoKnobSize, lfoKnobSize); // Row 2: PINGPONG combo, SPREAD knob int delayRow2Y = fx2KnobY + lfoKnobSize + 4; delayPingPongBox.setBounds(1110, delayRow2Y, 100, 36); delaySpreadKnob.setBounds(1220, delayRow2Y, lfoKnobSize, lfoKnobSize); // Reverb sub-section (X=1375, Y=690, W=265, H=130) reverbSizeKnob.setBounds(1383, fx2KnobY, lfoKnobSize, lfoKnobSize); reverbDampKnob.setBounds(1468, fx2KnobY, lfoKnobSize, lfoKnobSize); reverbMixKnob.setBounds(1553, fx2KnobY, lfoKnobSize, lfoKnobSize); // --- PRESET + SCALE --- presetButton.setBounds(20, 8, 120, 28); scaleLabel.setBounds(1520, 10, 42, 20); uiScaleBox.setBounds(1566, 8, 80, 24); // Visualizer area int vizY = 900; int vizH = 128; vuMeter.setBounds(14, vizY, 40, vizH); waveformDisplay.setBounds(60, vizY, 780, vizH); spectrumAnalyzer.setBounds(850, vizY, 794, vizH); // Piano roll pianoRoll.setBounds(10, 1038, 1640, 136); } // ===== PianoRollComponent ===== int PianoRollComponent::midiNoteForKey(int index) const { // Map key index to MIDI note, skipping black keys in the index // White keys: C D E F G A B (indices 0,2,4,5,7,9,11 in chromatic scale) static const int whiteToChromatic[] = {0, 2, 4, 5, 7, 9, 11}; int octave = index / 7; int noteInOctave = index % 7; return firstMidiNote + octave * 12 + whiteToChromatic[noteInOctave]; } bool PianoRollComponent::isBlackKey(int midiNote) const { int noteInOctave = midiNote % 12; return noteInOctave == 1 || noteInOctave == 3 || noteInOctave == 6 || noteInOctave == 8 || noteInOctave == 10; } int PianoRollComponent::keyAtPosition(juce::Point pos) const { int pbRight = pitchBendWidth; int keysX = pbRight + 8; int keysWidth = getWidth() - keysX - 6; int numWhiteKeys = (numKeys * 7) / 12 + 1; int localWhiteKeyWidth = keysWidth / numWhiteKeys; // Check black keys first (they're on top) for (int i = 0; i < numKeys; ++i) { int note = firstMidiNote + i; if (!isBlackKey(note)) continue; int chroma = note % 12; int octave = (note - firstMidiNote) / 12; static const int blackToLeftWhite[] = { 0,0,1,1,2,3,3,4,4,5,5,6 }; int leftWhite = blackToLeftWhite[chroma]; int whiteIndexBefore = octave * 7 + leftWhite; float bx = static_cast(keysX + (whiteIndexBefore + 1) * localWhiteKeyWidth) - static_cast(blackKeyWidth) * 0.5f; auto blackRect = juce::Rectangle(static_cast(bx), 0, blackKeyWidth, blackKeyHeight); if (blackRect.contains(pos)) return note; } // Check white keys for (int i = 0; i < numKeys; ++i) { int note = firstMidiNote + i; if (isBlackKey(note)) continue; int chroma = note % 12; int octave = (note - firstMidiNote) / 12; int whiteIndex = octave * 7; if (chroma == 2) whiteIndex += 1; else if (chroma == 4) whiteIndex += 2; else if (chroma == 5) whiteIndex += 3; else if (chroma == 7) whiteIndex += 4; else if (chroma == 9) whiteIndex += 5; else if (chroma == 11) whiteIndex += 6; auto whiteRect = juce::Rectangle(keysX + whiteIndex * localWhiteKeyWidth, 0, localWhiteKeyWidth, whiteKeyHeight); if (whiteRect.contains(pos)) return note; } return -1; } void PianoRollComponent::triggerNote(int note, bool on) { if (on) processor.noteOn(note, 0.8f); else processor.noteOff(note); } void PianoRollComponent::paint(juce::Graphics& g) { int pbRight = pitchBendWidth; int keysX = pbRight + 8; int keysWidth = getWidth() - keysX - 6; int numWhiteKeys = (numKeys * 7) / 12 + 1; whiteKeyWidth = keysWidth / numWhiteKeys; // Draw pitch bend strip background g.setColour(juce::Colour(0xff222222)); g.fillRect(10, 0, pbRight - 10, whiteKeyHeight); // Right edge border to separate from keys g.setColour(juce::Colour(0xff444444)); g.drawVerticalLine(pbRight - 8, 0.0f, static_cast(whiteKeyHeight)); // Active bend area indicator (shaded region from center to current position) int pbCenter = whiteKeyHeight / 2; float pbNorm = static_cast(currentPitchBend - 64) / 64.0f; // -1..+1 int pbY = pbCenter - static_cast(pbNorm * static_cast(pbCenter - 8)); g.setColour(juce::Colour(0x40ccaa44)); if (pbY < pbCenter) g.fillRect(14, pbY, 18, pbCenter - pbY); else g.fillRect(14, pbCenter, 18, pbY - pbCenter); // Pitch bend center line g.setColour(juce::Colour(0xffccaa44)); g.drawHorizontalLine(pbCenter, 12.0f, static_cast(pbRight - 14)); // Pitch bend indicator dot g.setColour(juce::Colour(0xffccaa44)); g.fillEllipse(14, pbY - 5, 18, 10); // PB labels g.setColour(juce::Colour(0xff888888)); g.setFont(9.0f); g.drawText("+", 14, 2, 18, 12, juce::Justification::centred); g.drawText("-", 14, whiteKeyHeight - 14, 18, 12, juce::Justification::centred); // Draw white keys first for (int i = 0; i < numKeys; ++i) { int note = firstMidiNote + i; if (isBlackKey(note)) continue; int chroma = note % 12; int octave = (note - firstMidiNote) / 12; int whiteIndex = octave * 7; if (chroma == 2) whiteIndex += 1; else if (chroma == 4) whiteIndex += 2; else if (chroma == 5) whiteIndex += 3; else if (chroma == 7) whiteIndex += 4; else if (chroma == 9) whiteIndex += 5; else if (chroma == 11) whiteIndex += 6; int kx = keysX + whiteIndex * whiteKeyWidth; auto rect = juce::Rectangle(kx, 0, whiteKeyWidth - 1, whiteKeyHeight); bool pressed = processor.isNoteActive(note); if (pressed) g.setColour(juce::Colour(0xffccaa44)); else g.setColour(juce::Colour(0xffdddddd)); g.fillRect(rect); g.setColour(juce::Colour(0xff555555)); g.drawRect(rect, 1); // Draw note name on C keys if (chroma == 0) { g.setColour(juce::Colour(0xff666666)); g.setFont(9.0f); g.drawText("C" + juce::String(octave + 3), kx + 2, whiteKeyHeight - 14, whiteKeyWidth - 4, 12, juce::Justification::centred); } } // Draw black keys on top for (int i = 0; i < numKeys; ++i) { int note = firstMidiNote + i; if (!isBlackKey(note)) continue; int chroma = note % 12; int octave = (note - firstMidiNote) / 12; // chroma → white key index to the LEFT of this black key static const int blackToLeftWhite[] = { 0,0,1,1,2,3,3,4,4,5,5,6 }; int leftWhite = blackToLeftWhite[chroma]; int whiteIndexBefore = octave * 7 + leftWhite; float bx = static_cast(keysX + (whiteIndexBefore + 1) * whiteKeyWidth) - static_cast(blackKeyWidth) * 0.5f; auto rect = juce::Rectangle(static_cast(bx), 0, blackKeyWidth, blackKeyHeight); bool pressed = processor.isNoteActive(note); if (pressed) g.setColour(juce::Colour(0xffccaa44)); else g.setColour(juce::Colour(0xff333333)); g.fillRect(rect); g.setColour(juce::Colour(0xff555555)); g.drawRect(rect, 1); } } void PianoRollComponent::mouseDown(const juce::MouseEvent& e) { auto pos = e.getPosition(); // Pitch bend strip if (pos.x < pitchBendWidth) { float pbNorm = 1.0f - static_cast(pos.y) / static_cast(whiteKeyHeight); pbNorm = std::clamp(pbNorm, -1.0f, 1.0f); currentPitchBend = 64 + static_cast(pbNorm * 64.0f); processor.pitchBendValue.store(pbNorm); return; } int note = keyAtPosition(pos); if (note >= 0) { if (lastTriggeredNote >= 0 && lastTriggeredNote != note) triggerNote(lastTriggeredNote, false); lastTriggeredNote = note; triggerNote(note, true); repaint(); } } void PianoRollComponent::mouseUp(const juce::MouseEvent& e) { // Reset pitch bend to center on release if (e.getPosition().x < pitchBendWidth || lastTriggeredNote < 0) { if (e.getPosition().x < pitchBendWidth) { currentPitchBend = 64; processor.pitchBendValue.store(0.0f); repaint(); } if (lastTriggeredNote < 0) return; } if (lastTriggeredNote >= 0) { triggerNote(lastTriggeredNote, false); lastTriggeredNote = -1; repaint(); } } void PianoRollComponent::mouseDrag(const juce::MouseEvent& e) { auto pos = e.getPosition(); // Pitch bend drag if (pos.x < pitchBendWidth) { float pbNorm = 1.0f - static_cast(pos.y) / static_cast(whiteKeyHeight); pbNorm = std::clamp(pbNorm, -1.0f, 1.0f); currentPitchBend = 64 + static_cast(pbNorm * 64.0f); processor.pitchBendValue.store(pbNorm); repaint(); return; } int note = keyAtPosition(pos); if (note >= 0 && note != lastTriggeredNote) { if (lastTriggeredNote >= 0) triggerNote(lastTriggeredNote, false); lastTriggeredNote = note; triggerNote(note, true); repaint(); } } ChromaFlockEditor::ChromaFlockEditor(ChromaFlockProcessor& p) : AudioProcessorEditor(&p), content(p) { addAndMakeVisible(content); content.onScaleChanged = [this](float s) { setUIScale(s); }; setSize(baseWidth, baseHeight); setResizable(false, false); } ChromaFlockEditor::~ChromaFlockEditor() {} void ChromaFlockEditor::paint(juce::Graphics& g) { g.fillAll(juce::Colour(0xff1a1a1a)); } void ChromaFlockEditor::resized() { content.setBounds(0, 0, baseWidth, baseHeight); content.setTransform(juce::AffineTransform::scale(currentScale, currentScale)); } void ChromaFlockEditor::setUIScale(float newScale) { currentScale = newScale; setSize(static_cast(baseWidth * newScale), static_cast(baseHeight * newScale)); }