UI polish: amber theme, smooth spectrum, MIDI LED, VU tuning

- Rename section labels: COMPRESSOR → COMPRESSION, AUTO-PAN → PANNING
- Replace spectrum analyzer bars with smooth gapless filled wave
- Use gold (#c59c07) for wave/spectrum strokes and piano roll active keys
- Amber CRT-style patch LCD with dark background and gold dot-matrix text
- Add MIDI signal LED with glow between semi and scale controls
- Reduce VU meter sensitivity (5x → 1.5x) and match amber gradient theme
- Add subtle glow behind section and sub-section boxes
- Fix Compressor.h class name and PluginProcessor include to match
This commit is contained in:
Armin 2026-07-16 02:31:34 +02:00
commit 628e1e3cba
6 changed files with 120 additions and 41 deletions

View file

@ -2,7 +2,7 @@
#include <cmath>
#include <algorithm>
class Compressor {
class Compression {
public:
void prepare(double sr) {
sampleRate = sr;

View file

@ -168,7 +168,7 @@ void VuMeter::paint(juce::Graphics& g) {
clipPath.addRoundedRectangle(bounds, 9.0f);
g.reduceClipRegion(clipPath);
juce::ColourGradient grad(juce::Colour(0xff00cc44).withAlpha(0.7f), 0.0f, bounds.getBottom(),
juce::ColourGradient grad(juce::Colour(0xffc59c07).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);
@ -230,13 +230,13 @@ void WaveformDisplay::paint(juce::Graphics& g) {
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);
juce::ColourGradient waveGrad(juce::Colour(0xffc59c07).withAlpha(0.5f), 0.0f, bounds.getY(),
juce::Colour(0xff3d2e02).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(0xffc59c07).withAlpha(0.9f));
g.strokePath(wavePath, juce::PathStrokeType(1.5f));
}
g.setColour(juce::Colour(0xffccaa44));
@ -272,23 +272,26 @@ void SpectrumAnalyzer::paint(juce::Graphics& g) {
fft->performFrequencyOnlyForwardTransform(fftData.data());
int numBars = 48;
int numPoints = 128;
float w = bounds.getWidth();
float h = bounds.getHeight() - 22.0f;
float barBottom = bounds.getBottom() - 2.0f;
float barW = w / static_cast<float>(numBars);
float bottom = bounds.getBottom() - 2.0f;
int maxBin = fftSize / 4;
float sampleRate = static_cast<float>(processor.getSampleRate());
float binHz = sampleRate / static_cast<float>(fftSize);
float lowCut = 80.0f;
float lowPass = 250.0f;
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);
std::vector<float> mags(numPoints + 1);
for (int i = 0; i <= numPoints; ++i) {
float t = static_cast<float>(i) / static_cast<float>(numPoints);
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));
int binEnd = static_cast<int>(std::pow(t + 1.0f / static_cast<float>(numPoints), 2.0f) * static_cast<float>(maxBin));
if (binEnd <= binStart) binEnd = binStart + 1;
if (binEnd > maxBin) binEnd = maxBin;
@ -301,24 +304,43 @@ void SpectrumAnalyzer::paint(juce::Graphics& g) {
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);
float centerHz = static_cast<float>((binStart + binEnd) / 2) * binHz;
float rolloff = juce::jlimit(0.0f, 1.0f, (centerHz - lowCut) / (lowPass - lowCut));
mag *= rolloff;
mags[i] = juce::jlimit(0.0f, 1.0f, mag);
}
juce::Path wavePath;
wavePath.startNewSubPath(bounds.getX(), bottom);
juce::Path strokePath;
strokePath.startNewSubPath(bounds.getX(), bottom - mags[0] * h);
for (int i = 0; i <= numPoints; ++i) {
float t = static_cast<float>(i) / static_cast<float>(numPoints);
float x = bounds.getX() + t * w;
float y = bottom - mags[i] * h;
wavePath.lineTo(x, y);
if (i > 0) strokePath.lineTo(x, y);
}
wavePath.lineTo(bounds.getRight(), bottom);
wavePath.closeSubPath();
juce::ColourGradient fillGrad(juce::Colour(0xffc59c07).withAlpha(0.35f), 0.0f, bottom,
juce::Colour(0xffc59c07).withAlpha(0.02f), 0.0f, bottom - h, false);
g.setGradientFill(fillGrad);
g.fillPath(wavePath);
g.setColour(juce::Colour(0xffc59c07).withAlpha(0.9f));
g.strokePath(strokePath, juce::PathStrokeType(1.5f));
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) ---
// --- Patch LCD (amber 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.
@ -374,16 +396,16 @@ void PatchLCD::paint(juce::Graphics& g) {
auto b = getLocalBounds();
// Dark LCD panel
g.setColour(juce::Colour(0xcc061206));
g.setColour(juce::Colour(0xcc1a0e00));
g.fillRoundedRectangle(b.toFloat(), 3.0f);
g.setColour(juce::Colour(0xff114411));
g.setColour(juce::Colour(0xff5a3a00));
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));
g.setColour(juce::Colour(0xff1a1000));
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);
@ -402,9 +424,9 @@ void PatchLCD::paint(juce::Graphics& g) {
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.setColour(juce::Colour(0xff3a2000));
g.fillEllipse(px - pitch * 0.5f, py - pitch * 0.5f, pitch, pitch);
g.setColour(juce::Colour(0xff33ff66));
g.setColour(juce::Colour(0xffc59c07));
g.fillEllipse(px - pitch * 0.4f, py - pitch * 0.4f, pitch * 0.8f, pitch * 0.8f);
}
}
@ -414,8 +436,32 @@ void PatchLCD::paint(juce::Graphics& g) {
}
}
// --- MIDI Signal LED ---
void MidiLed::paint(juce::Graphics& g) {
auto b = getLocalBounds().toFloat();
auto centre = b.getCentre();
float radius = juce::jmin(b.getWidth(), b.getHeight()) * 0.5f;
bool active = processor.isAnyNoteActive();
if (active) {
g.setColour(juce::Colour(0xffc59c07).withAlpha(0.2f));
g.fillEllipse(centre.x - radius * 1.8f, centre.y - radius * 1.8f, radius * 3.6f, radius * 3.6f);
}
g.setColour(juce::Colour(0xff1a1200));
g.fillEllipse(centre.x - radius, centre.y - radius, radius * 2.0f, radius * 2.0f);
if (active)
g.setColour(juce::Colour(0xffc59c07));
else
g.setColour(juce::Colour(0xff3a2a06));
g.fillEllipse(centre.x - radius * 0.75f, centre.y - radius * 0.75f, radius * 1.5f, radius * 1.5f);
}
MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
: processorRef(p), vuMeter(p), waveformDisplay(p), spectrumAnalyzer(p), pianoRoll(p) {
: processorRef(p), vuMeter(p), waveformDisplay(p), spectrumAnalyzer(p), pianoRoll(p), midiLed(p) {
auto setupParam = [&](juce::Slider& knob, std::unique_ptr<SliderAttachment>& attach,
const juce::String& paramId, const juce::String& name) {
@ -534,6 +580,7 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
addAndMakeVisible(presetButton);
addAndMakeVisible(patchLCD);
addAndMakeVisible(midiLed);
patchLCD.onClick = [this]() { showFileMenu(); };
auto setupArrow = [&](juce::TextButton& btn) {
@ -576,6 +623,12 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
scaleLabel.setJustificationType(juce::Justification::centredRight);
addAndMakeVisible(scaleLabel);
midiLabel.setText("MIDI IN", juce::dontSendNotification);
midiLabel.setFont(juce::Font(9.0f));
midiLabel.setColour(juce::Label::textColourId, juce::Colour(0xff888888));
midiLabel.setJustificationType(juce::Justification::centredLeft);
addAndMakeVisible(midiLabel);
auto setupTransposeLabel = [&](juce::Label& label, const juce::String& text) {
label.setText(text, juce::dontSendNotification);
label.setFont(juce::Font(9.0f));
@ -916,6 +969,10 @@ void MainContentComponent::paint(juce::Graphics& g) {
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));
// Soft glow behind section
auto glow = rect.expanded(6.0f);
g.setColour(juce::Colour(0xff222222).withAlpha(0.25f));
g.fillRoundedRectangle(glow, 12.0f);
// 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);
@ -938,6 +995,9 @@ void MainContentComponent::paint(juce::Graphics& g) {
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));
auto glow = rect.expanded(6.0f);
g.setColour(juce::Colour(0xff222222).withAlpha(0.25f));
g.fillRoundedRectangle(glow, 12.0f);
juce::ColourGradient fillGrad(juce::Colour(0xBB222222), rect.getCentreX(), rect.getY(),
juce::Colour(0xBB111111), rect.getCentreX(), rect.getBottom(), false);
g.setGradientFill(fillGrad);
@ -950,8 +1010,8 @@ void MainContentComponent::paint(juce::Graphics& g) {
};
drawSubSection(10, 508, 510, 140, "DISTORTION", 6);
drawSubSection(528, 508, 468, 140, "COMPRESSOR", 6);
drawSubSection(1004, 508, 268, 140, "AUTO-PAN", 6);
drawSubSection(528, 508, 468, 140, "COMPRESSION", 6);
drawSubSection(1004, 508, 268, 140, "PANNING", 6);
drawSubSection(1280, 508, 370, 140, "LIMITER", 6);
// LFO sub-sections
@ -1048,7 +1108,7 @@ void MainContentComponent::resized() {
distToneKnob.setBounds(dx + (fxKnobSize + 6) * 2, fxKnobY, fxKnobSize, fxKnobSize);
}
// Compressor sub-section (X=528, W=470)
// Compression sub-section (X=528, W=470)
{
int rowW = 5 * fxKnobSize + 4 * 6;
int sx = 528 + (470 - rowW) / 2;
@ -1142,6 +1202,9 @@ void MainContentComponent::resized() {
semitoneTransposeLabel.setBounds(1214, labelY2, 32, labelH2);
semitoneTransposeBox.setBounds(1246, comboY2, 74, comboH2);
midiLabel.setBounds(1340, labelY2, 46, labelH2);
midiLed.setBounds(1390, (headerH - 10) / 2, 10, 10);
scaleLabel.setBounds(1514, labelY2, 42, labelH2);
uiScaleBox.setBounds(1560, comboY2, 80, comboH2);
@ -1324,7 +1387,7 @@ void PianoRollComponent::paint(juce::Graphics& g) {
bool pressed = processor.isNoteActive(note);
if (pressed) {
g.setColour(juce::Colour(0xffccaa44));
g.setColour(juce::Colour(0xff996600));
} 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);

View file

@ -67,6 +67,15 @@ private:
juce::String text = "INIT";
};
class MidiLed : public juce::Component, public juce::Timer {
public:
explicit MidiLed(ChromaFlockProcessor& p) : processor(p) { startTimerHz(30); }
void paint(juce::Graphics& g) override;
void timerCallback() override { repaint(); }
private:
ChromaFlockProcessor& processor;
};
class PianoRollComponent : public juce::Component, public juce::Timer {
public:
explicit PianoRollComponent(ChromaFlockProcessor& p) : processor(p) { startTimerHz(60); }
@ -129,6 +138,7 @@ private:
juce::Label osc1Label, osc2Label, filterLabel, envLabel, fEnvLabel, globalLabel;
juce::Label fxLabel, lfoLabel, fx2Label;
juce::Label scaleLabel;
juce::Label midiLabel;
juce::Label octaveTransposeLabel, semitoneTransposeLabel;
juce::ComboBox octaveTransposeBox, semitoneTransposeBox;
@ -167,6 +177,7 @@ private:
// Preset menu
juce::TextButton presetButton{"PRESET"};
PatchLCD patchLCD;
MidiLed midiLed;
juce::TextButton prevPresetButton{"<"}, nextPresetButton{">"};
juce::TextButton randomizeButton{"RANDOM"};
int currentPresetIndex = 0;

View file

@ -276,7 +276,7 @@ void ChromaFlockProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
currentSampleRate = sampleRate;
synth.setCurrentPlaybackSampleRate(sampleRate);
distortion.prepare(sampleRate);
compressor.prepare(sampleRate);
compression.prepare(sampleRate);
limiter.prepare(sampleRate);
delay.prepare(sampleRate);
reverbFX.prepare(sampleRate);
@ -407,7 +407,7 @@ void ChromaFlockProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::
getParam("distSymmetry"),
getParam("distTone"));
compressor.setParameters(
compression.setParameters(
getParam("compThreshold"),
getParam("compRatio"),
getParam("compAttack"),
@ -449,8 +449,8 @@ void ChromaFlockProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::
right = distortion.process(right);
// Compression (stereo)
left = compressor.process(left);
right = compressor.process(right);
left = compression.process(left);
right = compression.process(right);
// Brickwall limiter (stereo)
left = limiter.process(left);
@ -504,7 +504,7 @@ void ChromaFlockProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::
float rms = std::sqrt(sumSquares / static_cast<float>(numSamples));
float level = juce::jmax(rms, peak);
level = std::min(level * 5.0f, 1.0f);
level = std::min(level * 1.5f, 1.0f);
float prev = rmsLevel.load(std::memory_order_relaxed);
if (level > prev)

View file

@ -69,6 +69,11 @@ public:
return midiNote >= 0 && midiNote < 128
&& activeNotes[midiNote].load(std::memory_order_relaxed);
}
bool isAnyNoteActive() const {
for (auto& n : activeNotes)
if (n.load(std::memory_order_relaxed)) return true;
return false;
}
int getTransposeSemitones() const;
@ -88,7 +93,7 @@ private:
static constexpr int maxVoices = 16;
Distortion distortion;
Compressor compressor;
Compression compression;
Limiter limiter;
Delay delay;
ReverbFX reverbFX;