#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(0xb3404040), rx, ry, juce::Colour(0xb31a1a1a), 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 — spotlight at 88° from horizontal (near-vertical, subtle tilt) float spotlightAngle = 88.0f * juce::MathConstants::pi / 180.0f; float sdx = std::cos(spotlightAngle); float sdy = std::sin(spotlightAngle); float scx = bounds.getCentreX(); float scy = bounds.getCentreY(); float slen = std::hypot(bounds.getWidth(), bounds.getHeight()) * 0.5f; juce::ColourGradient bodyGrad(juce::Colour(0xb3404040), scx - sdx * slen, scy - sdy * slen, juce::Colour(0xb31a1a1a), scx + sdx * slen, scy + sdy * slen, 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; } // Persistent highlight for the currently selected (ticked) entry or section if (isTicked && !isHighlighted) { g.setColour(juce::Colour(0xffccaa44).withAlpha(0.3f)); g.fillRoundedRectangle(area.reduced(2).toFloat(), 3.0f); } if (isHighlighted) { g.setColour(juce::Colour(0xffccaa44)); g.fillRoundedRectangle(area.reduced(2).toFloat(), 3.0f); } g.setColour(isHighlighted ? juce::Colour(0xff1a1a1a) : (isTicked ? juce::Colour(0xffccaa44) : 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().reduced(0.5f); juce::ColourGradient bgGrad(juce::Colour(0xBB222222), bounds.getCentreX(), bounds.getY(), juce::Colour(0xBB111111), bounds.getCentreX(), bounds.getBottom(), false); g.setGradientFill(bgGrad); g.fillRoundedRectangle(bounds, 9.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); juce::Path clipPath; clipPath.addRoundedRectangle(bounds, 9.0f); g.reduceClipRegion(clipPath); juce::ColourGradient grad(juce::Colour(0xff00cc44).withAlpha(0.7f), 0.0f, bounds.getBottom(), juce::Colour(0xffcc2200).withAlpha(0.7f), 0.0f, bounds.getY(), false); g.setGradientFill(grad); g.fillRoundedRectangle(innerBar, 2.0f); } g.setColour(juce::Colour(0xff333333)); g.drawRoundedRectangle(bounds, 9.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().reduced(0.5f); juce::ColourGradient bgGrad(juce::Colour(0xBB222222), bounds.getCentreX(), bounds.getY(), juce::Colour(0xBB111111), bounds.getCentreX(), bounds.getBottom(), false); g.setGradientFill(bgGrad); g.fillRoundedRectangle(bounds, 9.0f); g.setColour(juce::Colour(0xff333333)); g.drawRoundedRectangle(bounds, 9.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); juce::Path clipPath; clipPath.addRoundedRectangle(bounds, 9.0f); g.reduceClipRegion(clipPath); juce::ColourGradient waveGrad(juce::Colour(0xff00ff88).withAlpha(0.5f), 0.0f, bounds.getY(), juce::Colour(0xff005522).withAlpha(0.4f), 0.0f, bounds.getBottom(), false); g.setGradientFill(waveGrad); g.fillPath(filledPath); g.setColour(juce::Colour(0xff00ff88).withAlpha(0.7f)); g.strokePath(wavePath, juce::PathStrokeType(0.7f)); } 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().reduced(0.7f); juce::ColourGradient bgGrad(juce::Colour(0xBB222222), bounds.getCentreX(), bounds.getY(), juce::Colour(0xBB111111), bounds.getCentreX(), bounds.getBottom(), false); g.setGradientFill(bgGrad); g.fillRoundedRectangle(bounds, 9.0f); g.setColour(juce::Colour(0xff333333)); g.drawRoundedRectangle(bounds, 9.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; juce::Graphics::ScopedSaveState savedClip(g); juce::Path clipPath; clipPath.addRoundedRectangle(bounds, 9.0f); g.reduceClipRegion(clipPath); 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; g.setColour(juce::Colour(0xff00ff88).withAlpha(0.4f)); 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); } // --- Patch LCD (green dot-matrix) --- namespace { struct Glyph { char c; unsigned char rows[7]; }; // 5x7 font; each byte is a row, bits 4..0 are columns left->right. const Glyph font5x7[] = { {' ', {0x00,0x00,0x00,0x00,0x00,0x00,0x00}}, {'-', {0x00,0x00,0x00,0x1F,0x00,0x00,0x00}}, {'A', {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11}}, {'B', {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E}}, {'C', {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}}, {'D', {0x1E,0x11,0x11,0x11,0x11,0x11,0x1E}}, {'E', {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}}, {'F', {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10}}, {'G', {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}}, {'H', {0x11,0x11,0x11,0x1F,0x11,0x11,0x11}}, {'I', {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}}, {'J', {0x07,0x02,0x02,0x02,0x02,0x12,0x0C}}, {'K', {0x11,0x12,0x14,0x18,0x14,0x12,0x11}}, {'L', {0x10,0x10,0x10,0x10,0x10,0x10,0x1F}}, {'M', {0x11,0x1B,0x15,0x15,0x11,0x11,0x11}}, {'N', {0x11,0x11,0x19,0x15,0x13,0x11,0x11}}, {'O', {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}}, {'P', {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10}}, {'Q', {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}}, {'R', {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11}}, {'S', {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E}}, {'T', {0x1F,0x04,0x04,0x04,0x04,0x04,0x04}}, {'U', {0x11,0x11,0x11,0x11,0x11,0x11,0x0E}}, {'V', {0x11,0x11,0x11,0x11,0x11,0x0A,0x04}}, {'W', {0x11,0x11,0x11,0x15,0x15,0x1B,0x11}}, {'X', {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11}}, {'Y', {0x11,0x11,0x0A,0x04,0x04,0x04,0x04}}, {'Z', {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F}}, {'0', {0x0E,0x13,0x15,0x15,0x19,0x11,0x0E}}, {'1', {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E}}, {'2', {0x0E,0x11,0x01,0x06,0x08,0x10,0x1F}}, {'3', {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E}}, {'4', {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}}, {'5', {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E}}, {'6', {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}}, {'7', {0x1F,0x01,0x02,0x04,0x08,0x08,0x08}}, {'8', {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}}, {'9', {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C}}, }; const unsigned char* getFontGlyph(char c) { for (const auto& g : font5x7) if (g.c == c) return g.rows; return font5x7[0].rows; // space fallback } } void PatchLCD::paint(juce::Graphics& g) { auto b = getLocalBounds(); // Dark LCD panel g.setColour(juce::Colour(0xcc061206)); g.fillRoundedRectangle(b.toFloat(), 3.0f); g.setColour(juce::Colour(0xff114411)); g.drawRoundedRectangle(b.toFloat(), 3.0f, 1.0f); const int pitch = 3; const int glyphW = 5, glyphH = 7, glyphGap = 1; // Faint "off" LED grid g.setColour(juce::Colour(0xff0a200a)); for (int y = pitch / 2; y < b.getHeight(); y += pitch) for (int x = pitch / 2; x < b.getWidth(); x += pitch) g.fillRect(static_cast(x) - 0.5f, static_cast(y) - 0.5f, 1.0f, 1.0f); int y0 = (b.getHeight() - glyphH * pitch) / 2; int cx = 5; int cy = y0; if (cy < 0) cy = 0; juce::String u = text.toUpperCase(); for (auto chr : u) { const unsigned char* g7 = getFontGlyph(chr); for (int row = 0; row < glyphH; ++row) { unsigned char bits = g7[row]; for (int col = 0; col < glyphW; ++col) { if (bits & (1 << (glyphW - 1 - col))) { float px = static_cast(cx + col * pitch + pitch / 2); float py = static_cast(cy + row * pitch + pitch / 2); g.setColour(juce::Colour(0xff1a330a)); g.fillEllipse(px - pitch * 0.5f, py - pitch * 0.5f, pitch, pitch); g.setColour(juce::Colour(0xff33ff66)); g.fillEllipse(px - pitch * 0.4f, py - pitch * 0.4f, pitch * 0.8f, pitch * 0.8f); } } } cx += (glyphW + glyphGap) * pitch; if (cx > b.getWidth()) break; } } 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", "Pulse 25%", "Pulse 12%", "Rev Saw", "Full Rect", "Half Rect", "Stair 4", "Stair 8", "Soft Sine", "Sinc", "Exp Pulse", "Double Sine"}); 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", "Pulse 25%", "Pulse 12%", "Rev Saw", "Full Rect", "Half Rect", "Stair 4", "Stair 8", "Soft Sine", "Sinc", "Exp Pulse", "Double Sine"}); 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(distSymmetryKnob, distSymmetryAttach, "distSymmetry", "SYMMETRY"); setupParam(distToneKnob, distToneAttach, "distTone", "TONE"); 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(autoPanRateKnob, autoPanRateAttach, "autoPanRate", "RATE"); setupParam(autoPanDepthKnob, autoPanDepthAttach, "autoPanDepth", "DEPTH"); setupParam(autoPanPhaseKnob, autoPanPhaseAttach, "autoPanPhase", "PHASE"); setupParam(limiterThresholdKnob, limiterThresholdAttach, "limiterThreshold", "THRESH"); setupParam(limiterCeilingKnob, limiterCeilingAttach, "limiterCeiling", "CEILING"); setupParam(limiterReleaseKnob, limiterReleaseAttach, "limiterRelease", "RELEASE"); // LFO setupKnob(lfo1RateKnob, "RATE"); setupParam(lfo1DepthKnob, lfo1DepthAttach, "lfo1Depth", "DEPTH"); setupCB(lfo1ShapeBox, lfo1ShapeAttach, "lfo1Shape", {"Sine", "Triangle", "Saw", "Square"}); setupCB(lfo1DestBox, lfo1DestAttach, "lfo1Dest", {"Filter", "OSC1", "OSC2", "Both OSC", "OSC2 Phase"}); setupCB(lfo1SyncBox, lfo1SyncAttach, "lfo1Sync", {"Sync Off", "Sync On"}); lfo1SyncBox.onChange = [this]() { setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncBox); }; setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncBox); setupKnob(lfo2RateKnob, "RATE"); setupParam(lfo2DepthKnob, lfo2DepthAttach, "lfo2Depth", "DEPTH"); setupCB(lfo2ShapeBox, lfo2ShapeAttach, "lfo2Shape", {"Sine", "Triangle", "Saw", "Square"}); setupCB(lfo2DestBox, lfo2DestAttach, "lfo2Dest", {"Filter", "OSC1", "OSC2", "Both OSC", "OSC2 Phase"}); setupCB(lfo2SyncBox, lfo2SyncAttach, "lfo2Sync", {"Sync Off", "Sync On"}); lfo2SyncBox.onChange = [this]() { setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncBox); }; setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncBox); // FX2 setupKnob(delayTimeKnob, "TIME"); setupCB(delaySyncBox, delaySyncAttach, "delaySync", {"Sync Off", "Sync On"}); delaySyncBox.onChange = [this]() { setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox); }; setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox); setupParam(delayFbKnob, delayFbAttach, "delayFeedback", "FBACK"); setupParam(delayMixKnob, delayMixAttach, "delayMix", "MIX"); setupParam(reverbSizeKnob, reverbSizeAttach, "reverbSize", "SIZE"); setupParam(reverbDampKnob, reverbDampAttach, "reverbDamping", "DAMP"); setupParam(reverbMixKnob, reverbMixAttach, "reverbMix", "MIX"); // Preset section: title button + dot-matrix LCD + prev/next presetButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xcc2a2a2a)); presetButton.setColour(juce::TextButton::textColourOffId, juce::Colour(0xffccaa44)); presetButton.setLookAndFeel(&comboLaf); presetButton.onClick = [this]() { showPresetMenu(); }; addAndMakeVisible(presetButton); addAndMakeVisible(patchLCD); patchLCD.onClick = [this]() { showFileMenu(); }; auto setupArrow = [&](juce::TextButton& btn) { btn.setColour(juce::TextButton::buttonColourId, juce::Colour(0xcc2a2a2a)); btn.setColour(juce::TextButton::textColourOffId, juce::Colour(0xffccaa44)); btn.setLookAndFeel(&comboLaf); addAndMakeVisible(btn); }; setupArrow(prevPresetButton); setupArrow(nextPresetButton); prevPresetButton.onClick = [this]() { auto& presets = processorRef.presetManager.getPresets(); if (presets.empty()) return; currentPresetIndex = (currentPresetIndex - 1 + static_cast(presets.size())) % static_cast(presets.size()); applyCurrentPreset(); }; nextPresetButton.onClick = [this]() { auto& presets = processorRef.presetManager.getPresets(); if (presets.empty()) return; currentPresetIndex = (currentPresetIndex + 1) % static_cast(presets.size()); applyCurrentPreset(); }; randomizeButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xcc2a2a2a)); randomizeButton.setColour(juce::TextButton::textColourOffId, juce::Colour(0xffccaa44)); randomizeButton.setLookAndFeel(&comboLaf); randomizeButton.onClick = [this]() { randomizeParameters(); }; addAndMakeVisible(randomizeButton); auto& presets = processorRef.presetManager.getPresets(); if (!presets.empty()) { currentPresetIndex = 0; patchLCD.setText(presets[currentPresetIndex].name); } 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); auto setupTransposeLabel = [&](juce::Label& label, const juce::String& text) { label.setText(text, juce::dontSendNotification); label.setFont(juce::Font(9.0f)); label.setColour(juce::Label::textColourId, juce::Colour(0xff888888)); label.setJustificationType(juce::Justification::centredLeft); addAndMakeVisible(label); }; setupTransposeLabel(octaveTransposeLabel, "OCT"); setupTransposeLabel(semitoneTransposeLabel, "SEMI"); setupCombo(octaveTransposeBox); octaveTransposeBox.addItemList(juce::StringArray{"-3", "-2", "-1", "0", "+1", "+2", "+3"}, 1); octaveTransposeAttach = std::make_unique( processorRef.apvts, "octaveTranspose", octaveTransposeBox); setupCombo(semitoneTransposeBox); semitoneTransposeBox.addItemList( juce::StringArray{getSemitoneChoices()}, 1); semitoneTransposeAttach = std::make_unique( processorRef.apvts, "semitoneTranspose", semitoneTransposeBox); 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.setRotaryParameters(juce::MathConstants::pi * 1.2f, juce::MathConstants::pi * 2.8f, true); 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::setupSyncKnob(juce::Slider& knob, std::unique_ptr& rateAttach, std::unique_ptr& beatAttach, const juce::String& rateId, const juce::String& beatId, juce::ComboBox& syncBox) { bool on = syncBox.getSelectedId() == 2; rateAttach.reset(); beatAttach.reset(); if (on) beatAttach = std::make_unique(processorRef.apvts, beatId, knob); else rateAttach = std::make_unique(processorRef.apvts, rateId, knob); } void MainContentComponent::showPresetMenu() { juce::PopupMenu menu; std::vector menuOrder; auto categories = processorRef.presetManager.getCategories(); juce::String currentName; juce::String currentCategory; auto& allPresets = processorRef.presetManager.getPresets(); if (currentPresetIndex >= 0 && currentPresetIndex < static_cast(allPresets.size())) { currentName = allPresets[currentPresetIndex].name; currentCategory = allPresets[currentPresetIndex].category; } for (auto& cat : categories) { juce::PopupMenu subMenu; auto presets = processorRef.presetManager.getPresetsInCategory(cat); for (auto& preset : presets) { menuOrder.push_back(preset.name); bool isCurrent = (preset.name == currentName); subMenu.addItem(static_cast(menuOrder.size()), preset.name, true, isCurrent); } // Tick the section that contains the active patch (cleared when randomize -> index -1) menu.addSubMenu(cat, subMenu, true, juce::Image(), cat == currentCategory); } menu.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&presetButton), [this, menuOrder](int result) { if (result > 0 && result <= static_cast(menuOrder.size())) { auto name = menuOrder[static_cast(result) - 1]; auto& presets = processorRef.presetManager.getPresets(); for (int i = 0; i < static_cast(presets.size()); ++i) { if (presets[i].name == name) { currentPresetIndex = i; break; } } applyCurrentPreset(); } }); } void MainContentComponent::showFileMenu() { juce::PopupMenu menu; menu.addItem(1, "Load Preset..."); menu.addItem(2, "Save Preset..."); menu.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&patchLCD), [this](int result) { if (result == 1) loadPresetFile(); else if (result == 2) savePresetFile(); }); } static const char* kCflTag = "ChromaFlockPatch"; #include "Version.h" static const juce::String kCflVersion = CHROMAFLOCK_VERSION; juce::String MainContentComponent::currentPresetName() const { auto& presets = processorRef.presetManager.getPresets(); if (currentPresetIndex >= 0 && currentPresetIndex < static_cast(presets.size())) return presets[currentPresetIndex].name; return patchLCD.getText(); } void MainContentComponent::savePresetFile() { auto* chooser = new juce::FileChooser("Save ChromaFlock Preset", juce::File::getSpecialLocation(juce::File::userHomeDirectory) .getChildFile("chromaflock-" + currentPresetName().replaceCharacter(' ', '-') + ".cfl"), "*.cfl"); chooser->launchAsync(juce::FileBrowserComponent::saveMode | juce::FileBrowserComponent::canSelectFiles, [this, chooser](const juce::FileChooser& c) { std::unique_ptr deleter(chooser); auto file = c.getResult(); if (file == juce::File()) return; if (file.getFileExtension().toLowerCase() != ".cfl") file = file.withFileExtension(".cfl"); auto state = processorRef.apvts.copyState(); std::unique_ptr presetXml(state.createXml()); std::unique_ptr root(new juce::XmlElement(kCflTag)); root->setAttribute("version", kCflVersion); root->setAttribute("generator", "ChromaFlock"); root->addChildElement(presetXml.release()); if (!root->writeTo(file)) juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon, "Save Failed", "Could not write preset file:\n" + file.getFullPathName(), this); else patchLCD.setText(file.getFileNameWithoutExtension()); }); } void MainContentComponent::loadPresetFile() { auto* chooser = new juce::FileChooser("Load ChromaFlock Preset", juce::File::getSpecialLocation(juce::File::userHomeDirectory), "*.cfl"); chooser->launchAsync(juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles, [this, chooser](const juce::FileChooser& c) { std::unique_ptr deleter(chooser); auto file = c.getResult(); if (file == juce::File() || !file.existsAsFile()) return; std::unique_ptr root(juce::XmlDocument::parse(file)); if (root == nullptr || !root->hasTagName(kCflTag)) { juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon, "Invalid File", "This is not a valid ChromaFlock (.cfl) preset file.", this); return; } auto fileVersion = root->getStringAttribute("version", ""); if (fileVersion != kCflVersion) { juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon, "Version Mismatch", "This preset was saved with ChromaFlock " + (fileVersion.isEmpty() ? "" : fileVersion) + ", but you are running ChromaFlock " + kCflVersion + ".\nThe preset may not sound as intended.", this); } auto* presetXml = root->getChildByName(processorRef.apvts.state.getType()); if (presetXml == nullptr) { juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon, "Invalid File", "The preset file does not contain ChromaFlock parameter data.", this); return; } processorRef.fadeOutActiveVoices(0.25f); processorRef.apvts.replaceState(juce::ValueTree::fromXml(*presetXml)); currentPresetIndex = -1; patchLCD.setText(file.getFileNameWithoutExtension()); setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncBox); setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncBox); setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox); }); } void MainContentComponent::applyCurrentPreset() { auto& presets = processorRef.presetManager.getPresets(); if (presets.empty()) return; currentPresetIndex = juce::jlimit(0, static_cast(presets.size()) - 1, currentPresetIndex); processorRef.fadeOutActiveVoices(0.25f); processorRef.presetManager.applyPreset(presets[currentPresetIndex].name, processorRef.apvts); patchLCD.setText(presets[currentPresetIndex].name); // The ComboBoxAttachment updates the sync boxes without firing onChange, // so re-sync the rate/beat knob attachments to match. setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncBox); setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncBox); setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox); } void MainContentComponent::randomizeParameters() { juce::Random rand; processorRef.fadeOutActiveVoices(0.25f); currentPresetIndex = -1; for (auto* p : processorRef.getParameters()) { auto* rap = dynamic_cast(p); if (rap == nullptr) continue; const auto& id = rap->paramID; if (id == "octaveTranspose" || id == "semitoneTranspose") continue; float v = rand.nextFloat(); if (id == "masterLevel") v = 0.4f + 0.6f * v; rap->setValueNotifyingHost(v); } patchLCD.setText("RANDOM"); // Re-sync the rate/beat knob attachments to match the randomized sync states. setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncBox); setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncBox); setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox); } void MainContentComponent::paint(juce::Graphics& g) { g.fillAll(juce::Colour(0xff1a1a1a)); // Draw background image tiled if (!bgImageLoaded) { bgImage = juce::ImageFileFormat::loadFrom( BinaryData::bg_png, BinaryData::bg_pngSize); bgImageLoaded = true; } if (bgImage.isValid()) { g.setOpacity(0.35f); g.drawImage(bgImage, juce::Rectangle(0.0f, 0.0f, static_cast(getWidth()), static_cast(getHeight())), juce::RectanglePlacement::stretchToFit); g.setOpacity(1.0f); } // Load logo if (!logoLoaded) { auto img = juce::ImageFileFormat::loadFrom( BinaryData::newlogo_png, BinaryData::newlogo_pngSize); if (img.isValid()) { juce::Image::BitmapData bd(img, juce::Image::BitmapData::readOnly); int minX = bd.width, maxX = 0, minY = bd.height, maxY = 0; bool found = false; for (int y = 0; y < bd.height; ++y) { for (int x = 0; x < bd.width; ++x) { int a = bd.data[static_cast(y) * bd.lineStride + x * bd.pixelStride + 3]; if (a > 10) { found = true; if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; } } } logoImage = found ? img.getClippedImage(juce::Rectangle(minX, minY, maxX - minX + 1, maxY - minY + 1)) : img; } logoLoaded = true; } if (logoImage.isValid()) { int logoW = 440; int logoH = logoImage.getWidth() > 0 ? static_cast(logoW * static_cast(logoImage.getHeight()) / logoImage.getWidth()) : logoW; int logoX = (getWidth() - logoW) / 2; int logoY = (124 - logoH) / 2; auto logoArea = juce::Rectangle(logoX, logoY, logoW, logoH); juce::Graphics::ScopedSaveState saved(g); g.setOpacity(0.8f); g.drawImage(logoImage, logoArea.toFloat(), juce::RectanglePlacement::centred); } 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(0xBB222222), rect.getCentreX(), rect.getY(), juce::Colour(0xBB111111), rect.getCentreX(), rect.getBottom(), false); g.setGradientFill(fillGrad); g.fillRoundedRectangle(rect, 9.0f); // Border g.setColour(juce::Colour(0xff333333)); g.drawRoundedRectangle(rect, 9.0f, 1.0f); }; drawSection(10, 128, 498, 200); drawSection(518, 128, 624, 200); drawSection(1152, 128, 498, 200); drawSection(75, 338, 498, 160); drawSection(583, 338, 502, 160); drawSection(1095, 338, 490, 160); drawSection(10, 956, 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(0xBB222222), rect.getCentreX(), rect.getY(), juce::Colour(0xBB111111), rect.getCentreX(), rect.getBottom(), false); g.setGradientFill(fillGrad); g.fillRoundedRectangle(rect, 9.0f); g.setColour(juce::Colour(0xff333333)); g.drawRoundedRectangle(rect, 9.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(10, 508, 512, 140, "DISTORTION", 6); drawSubSection(528, 508, 470, 140, "COMPRESSOR", 6); drawSubSection(1004, 508, 270, 140, "AUTO-PAN", 6); drawSubSection(1280, 508, 370, 140, "LIMITER", 6); // LFO sub-sections drawSubSection(10, 658, 540, 150, "LFO 1", 4); drawSubSection(560, 658, 530, 150, "LFO 2", 4); // FX2 sub-sections drawSubSection(1100, 658, 265, 150, "DELAY", 4); drawSubSection(1375, 658, 275, 150, "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, 36); 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(518, sectionY, 624, labelH); osc2WaveBox.setBounds(528, sectionY + labelH + 4, 200, 36); int knobY2 = sectionY + labelH + comboH + 8; osc2OctKnob.setBounds(528, knobY2, knobSize, knobSize); osc2SemiKnob.setBounds(528 + knobSize + knobSpacing, knobY2, knobSize, knobSize); osc2FineKnob.setBounds(528 + (knobSize + knobSpacing) * 2, knobY2, knobSize, knobSize); osc2LevelKnob.setBounds(528 + (knobSize + knobSpacing) * 3, knobY2, knobSize, knobSize); phaseOffsetKnob.setBounds(528 + (knobSize + knobSpacing) * 4, knobY2, knobSize, knobSize); filterLabel.setBounds(1152, sectionY, 498, labelH); filterTypeBox.setBounds(1162, sectionY + labelH + 4, 200, 28); 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(10, 342, 563, labelH); { int rowW = 4 * knobSize + 3 * knobSpacing; int sx = 10 + (563 - rowW) / 2; int knobYA = 352 + labelH + 6; envAttackKnob.setBounds(sx, knobYA, knobSize, knobSize); envDecayKnob.setBounds(sx + (knobSize + knobSpacing), knobYA, knobSize, knobSize); envSustainKnob.setBounds(sx + (knobSize + knobSpacing) * 2, knobYA, knobSize, knobSize); envReleaseKnob.setBounds(sx + (knobSize + knobSpacing) * 3, knobYA, knobSize, knobSize); } fEnvLabel.setBounds(583, 342, 502, labelH); { int rowW = 4 * knobSize + 3 * knobSpacing; int sx = 583 + (502 - rowW) / 2; int knobYFE = 352 + labelH + 6; fEnvAttackKnob.setBounds(sx, knobYFE, knobSize, knobSize); fEnvDecayKnob.setBounds(sx + (knobSize + knobSpacing), knobYFE, knobSize, knobSize); fEnvSustainKnob.setBounds(sx + (knobSize + knobSpacing) * 2, knobYFE, knobSize, knobSize); fEnvReleaseKnob.setBounds(sx + (knobSize + knobSpacing) * 3, knobYFE, knobSize, knobSize); } globalLabel.setBounds(1095, 342, 555, labelH); { int rowW = 4 * knobSize + 3 * knobSpacing; int sx = 1095 + (555 - rowW) / 2; int knobYM = 352 + labelH + 6; panKnob.setBounds(sx, knobYM, knobSize, knobSize); driveKnob.setBounds(sx + (knobSize + knobSpacing), knobYM, knobSize, knobSize); masterKnob.setBounds(sx + (knobSize + knobSpacing) * 2, knobYM, knobSize, knobSize); pbRangeKnob.setBounds(sx + (knobSize + knobSpacing) * 3, knobYM, knobSize, knobSize); } // --- FX SECTION --- int fxY = 480; int fxKnobSize = 80; 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=10, W=512) — dropdown fixed at x=17, 3 knobs fill remaining width distTypeBox.setBounds(17, fxKnobY + (fxKnobSize - 28) / 2, 140, 28); { int availLeft = 17 + 140; int availW = (10 + 512) - availLeft; int rowW = 3 * fxKnobSize + 2 * 6; int dx = availLeft + (availW - rowW) / 2; distAmountKnob.setBounds(dx, fxKnobY, fxKnobSize, fxKnobSize); distSymmetryKnob.setBounds(dx + (fxKnobSize + 6), fxKnobY, fxKnobSize, fxKnobSize); distToneKnob.setBounds(dx + (fxKnobSize + 6) * 2, fxKnobY, fxKnobSize, fxKnobSize); } // Compressor sub-section (X=528, W=470) { int rowW = 5 * fxKnobSize + 4 * 6; int sx = 528 + (470 - rowW) / 2; compThresholdKnob.setBounds(sx, fxKnobY, fxKnobSize, fxKnobSize); compRatioKnob.setBounds(sx + (fxKnobSize + 6), fxKnobY, fxKnobSize, fxKnobSize); compAttackKnob.setBounds(sx + (fxKnobSize + 6) * 2, fxKnobY, fxKnobSize, fxKnobSize); compReleaseKnob.setBounds(sx + (fxKnobSize + 6) * 3, fxKnobY, fxKnobSize, fxKnobSize); compMakeupKnob.setBounds(sx + (fxKnobSize + 6) * 4, fxKnobY, fxKnobSize, fxKnobSize); } // Auto-Pan sub-section (X=1004, W=270) { int rowW = 3 * fxKnobSize + 2 * 6; int ax = 1004 + (270 - rowW) / 2; autoPanRateKnob.setBounds(ax, fxKnobY, fxKnobSize, fxKnobSize); autoPanDepthKnob.setBounds(ax + (fxKnobSize + 6), fxKnobY, fxKnobSize, fxKnobSize); autoPanPhaseKnob.setBounds(ax + (fxKnobSize + 6) * 2, fxKnobY, fxKnobSize, fxKnobSize); } // Limiter sub-section (X=1280, W=370) { int rowW = 3 * fxKnobSize + 2 * 6; int lx = 1280 + (370 - rowW) / 2; limiterThresholdKnob.setBounds(lx, fxKnobY, fxKnobSize, fxKnobSize); limiterCeilingKnob.setBounds(lx + (fxKnobSize + 6), fxKnobY, fxKnobSize, fxKnobSize); limiterReleaseKnob.setBounds(lx + (fxKnobSize + 6) * 2, fxKnobY, fxKnobSize, fxKnobSize); } // --- LFO SECTION --- int lfoY = 638; int lfoKnobSize = 80; int lfoKnobY = 658 + 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); lfo1SyncBox.setBounds(22, lfoKnobY + lfoKnobSize + 6, 200, 28); // 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); lfo2SyncBox.setBounds(568, lfoKnobY + lfoKnobSize + 6, 200, 28); // --- 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); delaySyncBox.setBounds(1110, fx2KnobY + lfoKnobSize + 6, 240, 28); // 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 (vertically centered within top section, y: 0..128) --- int headerH = 128; int btnH = 28, btnGap = 8; int presetGroupH = btnH * 2 + btnGap; int presetStartY = (headerH - presetGroupH) / 2; presetButton.setBounds(20, presetStartY, 90, btnH); randomizeButton.setBounds(20, presetStartY + btnH + btnGap, 90, btnH); int rowH = 28; int rowY = (headerH - rowH) / 2; patchLCD.setBounds(118, rowY, 360, rowH); prevPresetButton.setBounds(486, rowY, 28, rowH); nextPresetButton.setBounds(518, rowY, 28, rowH); int comboH2 = 24, labelH2 = 20; int comboY2 = (headerH - comboH2) / 2; int labelY2 = (headerH - labelH2) / 2; octaveTransposeLabel.setBounds(1112, labelY2, 30, labelH2); octaveTransposeBox.setBounds(1142, comboY2, 64, comboH2); semitoneTransposeLabel.setBounds(1214, labelY2, 32, labelH2); semitoneTransposeBox.setBounds(1246, comboY2, 74, comboH2); scaleLabel.setBounds(1520, labelY2, 42, labelH2); uiScaleBox.setBounds(1566, comboY2, 80, comboH2); // Visualizer area int vizY = 818; int vizH = 128; vuMeter.setBounds(10, vizY, 40, vizH); waveformDisplay.setBounds(60, vizY, 790, vizH); spectrumAnalyzer.setBounds(860, vizY, 790, vizH); // Piano roll pianoRoll.setBounds(10, 956, 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).withAlpha(0.7f)); 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).withAlpha(0.7f)); } else { juce::ColourGradient grad(juce::Colour(0xffe4e4e4).withAlpha(0.7f), static_cast(kx), 0.0f, juce::Colour(0xffc8c8c8).withAlpha(0.7f), static_cast(kx), static_cast(whiteKeyHeight), false); g.setGradientFill(grad); } 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 { juce::ColourGradient grad(juce::Colour(0xff444444), static_cast(bx), 0.0f, juce::Colour(0xff222222), static_cast(bx), static_cast(blackKeyHeight), false); g.setGradientFill(grad); } 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: ignore plain clicks — require a vertical drag (see mouseDrag) if (pos.x < pitchBendWidth) { pitchBendDragging = false; 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 (only if it was actually dragged) if (pitchBendDragging) { pitchBendDragging = false; pitchBendGliding = true; pitchBendGlideStart = static_cast(currentPitchBend - 64) / 64.0f; pitchBendGlideStartMs = juce::Time::currentTimeMillis(); repaint(); } if (lastTriggeredNote >= 0) { triggerNote(lastTriggeredNote, false); lastTriggeredNote = -1; repaint(); } } void PianoRollComponent::timerCallback() { if (pitchBendGliding) { float t = static_cast(juce::Time::currentTimeMillis() - pitchBendGlideStartMs) / static_cast(pitchBendGlideMs); if (t >= 1.0f) { pitchBendGliding = false; currentPitchBend = 64; processor.pitchBendValue.store(0.0f); } else { float pbNorm = pitchBendGlideStart * (1.0f - t); currentPitchBend = 64 + static_cast(pbNorm * 64.0f); processor.pitchBendValue.store(pbNorm); } repaint(); } else { repaint(); } } void PianoRollComponent::mouseDrag(const juce::MouseEvent& e) { auto pos = e.getPosition(); // Pitch bend drag — only while touching the strip if (pos.x < pitchBendWidth) { pitchBendDragging = true; pitchBendGliding = false; float pbNorm = 1.0f - 2.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)); }