mirror of
https://codeberg.org/armin/chromaflock.git
synced 2026-09-01 12:20:47 +02:00
1439 lines
62 KiB
C++
1439 lines
62 KiB
C++
#include "PluginEditor.h"
|
|
#include <BinaryData.h>
|
|
|
|
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<int>(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<int>(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<float>::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<int>& 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<float>(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<float>(i) / static_cast<float>(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<float, ChromaFlockProcessor::fftSize * 2> 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<float>(i) / static_cast<float>(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<float>(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<float>(bar) / static_cast<float>(numBars);
|
|
float tNext = static_cast<float>(bar + 1) / static_cast<float>(numBars);
|
|
int binStart = static_cast<int>(std::pow(t, 2.0f) * static_cast<float>(maxBin));
|
|
int binEnd = static_cast<int>(std::pow(tNext, 2.0f) * static_cast<float>(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<float>(count) : 0.0f;
|
|
mag = mag / static_cast<float>(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<float>(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<float>(x) - 0.5f, static_cast<float>(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<float>(cx + col * pitch + pitch / 2);
|
|
float py = static_cast<float>(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<SliderAttachment>& attach,
|
|
const juce::String& paramId, const juce::String& name) {
|
|
setupKnob(knob, name);
|
|
attach = std::make_unique<SliderAttachment>(processorRef.apvts, paramId, knob);
|
|
};
|
|
|
|
auto setupCB = [&](juce::ComboBox& box, std::unique_ptr<ComboBoxAttachment>& attach,
|
|
const juce::String& paramId, const juce::StringArray& items) {
|
|
setupCombo(box);
|
|
box.addItemList(items, 1);
|
|
attach = std::make_unique<ComboBoxAttachment>(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<int>(presets.size())) % static_cast<int>(presets.size());
|
|
applyCurrentPreset();
|
|
};
|
|
nextPresetButton.onClick = [this]() {
|
|
auto& presets = processorRef.presetManager.getPresets();
|
|
if (presets.empty()) return;
|
|
currentPresetIndex = (currentPresetIndex + 1) % static_cast<int>(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<ComboBoxAttachment>(
|
|
processorRef.apvts, "octaveTranspose", octaveTransposeBox);
|
|
|
|
setupCombo(semitoneTransposeBox);
|
|
semitoneTransposeBox.addItemList(
|
|
juce::StringArray{getSemitoneChoices()}, 1);
|
|
semitoneTransposeAttach = std::make_unique<ComboBoxAttachment>(
|
|
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<float>::pi * 1.2f,
|
|
juce::MathConstants<float>::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<SliderAttachment>& rateAttach,
|
|
std::unique_ptr<SliderAttachment>& 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<SliderAttachment>(processorRef.apvts, beatId, knob);
|
|
else
|
|
rateAttach = std::make_unique<SliderAttachment>(processorRef.apvts, rateId, knob);
|
|
}
|
|
|
|
void MainContentComponent::showPresetMenu() {
|
|
juce::PopupMenu menu;
|
|
std::vector<juce::String> menuOrder;
|
|
|
|
auto categories = processorRef.presetManager.getCategories();
|
|
|
|
juce::String currentName;
|
|
juce::String currentCategory;
|
|
auto& allPresets = processorRef.presetManager.getPresets();
|
|
if (currentPresetIndex >= 0 && currentPresetIndex < static_cast<int>(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<int>(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<int>(menuOrder.size())) {
|
|
auto name = menuOrder[static_cast<size_t>(result) - 1];
|
|
auto& presets = processorRef.presetManager.getPresets();
|
|
for (int i = 0; i < static_cast<int>(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<int>(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<juce::FileChooser> 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<juce::XmlElement> presetXml(state.createXml());
|
|
|
|
std::unique_ptr<juce::XmlElement> 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<juce::FileChooser> deleter(chooser);
|
|
auto file = c.getResult();
|
|
if (file == juce::File() || !file.existsAsFile())
|
|
return;
|
|
|
|
std::unique_ptr<juce::XmlElement> 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() ? "<unknown>" : 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<int>(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<juce::RangedAudioParameter*>(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<float>(0.0f, 0.0f, static_cast<float>(getWidth()),
|
|
static_cast<float>(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<size_t>(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<int>(minX, minY, maxX - minX + 1, maxY - minY + 1))
|
|
: img;
|
|
}
|
|
logoLoaded = true;
|
|
}
|
|
if (logoImage.isValid()) {
|
|
int logoW = 440;
|
|
int logoH = logoImage.getWidth() > 0
|
|
? static_cast<int>(logoW * static_cast<double>(logoImage.getHeight()) / logoImage.getWidth())
|
|
: logoW;
|
|
int logoX = (getWidth() - logoW) / 2;
|
|
int logoY = (124 - logoH) / 2;
|
|
auto logoArea = juce::Rectangle<int>(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<float>(static_cast<float>(x), static_cast<float>(y),
|
|
static_cast<float>(w), static_cast<float>(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(10, 338, 563, 160);
|
|
drawSection(583, 338, 502, 160);
|
|
drawSection(1095, 338, 555, 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<float>(static_cast<float>(x), static_cast<float>(y),
|
|
static_cast<float>(w), static_cast<float>(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, 28);
|
|
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, 28);
|
|
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 + (lfoKnobSize - 28) / 2, 130, 28);
|
|
lfo1DestBox.setBounds(382, lfoKnobY + (lfoKnobSize - 28) / 2, 155, 28);
|
|
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 + (lfoKnobSize - 28) / 2, 130, 28);
|
|
lfo2DestBox.setBounds(928, lfoKnobY + (lfoKnobSize - 28) / 2, 155, 28);
|
|
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)
|
|
{
|
|
int rowW = 3 * lfoKnobSize + 2 * 8;
|
|
int rx = 1650 - rowW;
|
|
reverbSizeKnob.setBounds(rx, fx2KnobY, lfoKnobSize, lfoKnobSize);
|
|
reverbDampKnob.setBounds(rx + (lfoKnobSize + 8), fx2KnobY, lfoKnobSize, lfoKnobSize);
|
|
reverbMixKnob.setBounds(rx + (lfoKnobSize + 8) * 2, 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;
|
|
int lcdX = 150; // gap between the preset/random buttons and the patch LCD
|
|
patchLCD.setBounds(lcdX, rowY, 360, rowH);
|
|
prevPresetButton.setBounds(lcdX + 360 + 8, rowY, 28, rowH);
|
|
nextPresetButton.setBounds(lcdX + 360 + 8 + 32, rowY, 28, rowH);
|
|
|
|
int comboH2 = 28, 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<int> 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<float>(keysX + (whiteIndexBefore + 1) * localWhiteKeyWidth) - static_cast<float>(blackKeyWidth) * 0.5f;
|
|
auto blackRect = juce::Rectangle<int>(static_cast<int>(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<int>(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<float>(whiteKeyHeight));
|
|
|
|
// Active bend area indicator (shaded region from center to current position)
|
|
int pbCenter = whiteKeyHeight / 2;
|
|
float pbNorm = static_cast<float>(currentPitchBend - 64) / 64.0f; // -1..+1
|
|
int pbY = pbCenter - static_cast<int>(pbNorm * static_cast<float>(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<float>(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<int>(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<float>(kx), 0.0f,
|
|
juce::Colour(0xffc8c8c8).withAlpha(0.7f), static_cast<float>(kx), static_cast<float>(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<float>(keysX + (whiteIndexBefore + 1) * whiteKeyWidth) - static_cast<float>(blackKeyWidth) * 0.5f;
|
|
auto rect = juce::Rectangle<int>(static_cast<int>(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<float>(bx), 0.0f,
|
|
juce::Colour(0xff222222), static_cast<float>(bx), static_cast<float>(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: begin a drag that starts from center (0 bend)
|
|
if (pos.x < pitchBendWidth) {
|
|
pitchBendDragging = true;
|
|
pitchBendGliding = false;
|
|
pitchBendStartY = pos.y;
|
|
currentPitchBend = 64;
|
|
processor.pitchBendValue.store(0.0f);
|
|
repaint();
|
|
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<float>(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<float>(juce::Time::currentTimeMillis() - pitchBendGlideStartMs)
|
|
/ static_cast<float>(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<int>(pbNorm * 64.0f);
|
|
processor.pitchBendValue.store(pbNorm);
|
|
}
|
|
repaint();
|
|
} else {
|
|
repaint();
|
|
}
|
|
}
|
|
|
|
void PianoRollComponent::mouseDrag(const juce::MouseEvent& e) {
|
|
auto pos = e.getPosition();
|
|
|
|
// Pitch bend drag — relative to where the drag started (starts at 0)
|
|
if (pos.x < pitchBendWidth) {
|
|
pitchBendDragging = true;
|
|
pitchBendGliding = false;
|
|
float pbNorm = 2.0f * static_cast<float>(pitchBendStartY - pos.y) / static_cast<float>(whiteKeyHeight);
|
|
pbNorm = std::clamp(pbNorm, -1.0f, 1.0f);
|
|
currentPitchBend = 64 + static_cast<int>(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<int>(baseWidth * newScale), static_cast<int>(baseHeight * newScale));
|
|
}
|