diff --git a/README.md b/README.md index 266cb63..f89be86 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ will work reliably, be useful in any way, or even make any sense whatsoever. ## Signal Flow ``` -Osc1 + Osc2 → Amp Envelope × Velocity → Filter → Drive → Distortion → Compressor → Auto-Pan → Delay → Reverb → Output +Osc1 + Osc2 → Amp Envelope × Velocity → Filter → Drive → Distortion → Compression → Auto-Pan → Delay → Reverb → Output ``` ## Oscillators @@ -81,7 +81,7 @@ All effects are stereo and processed in series after the synth voice. - **Mix** (dry/wet): 0.0 – 1.0 - Bypassed when amount < 0.001 -### Compressor +### Compression - **Threshold**: -60 dB – 0 dB - **Ratio**: 1:1 – 20:1 @@ -165,7 +165,7 @@ Custom dark theme with gold accents. ~50 knobs across 9 sections. | FILTER ENV | Attack, Decay, Sustain, Release knobs | | Master | Pan, Drive, Level, PB Range knobs | | Distortion | Type selector, Amount, Mix knobs | -| Compressor | Threshold, Ratio, Attack, Release, Makeup, Mix knobs | +| Compression | Threshold, Ratio, Attack, Release, Makeup, Mix knobs | | Auto-Pan | Rate, Depth knobs | | LFO 1/2 | Rate, Depth knobs, Shape and Destination selectors | | Delay | Time, Feedback, Mix knobs, Ping-Pong selector, Spread knob | diff --git a/Source/DSP/Compressor.h b/Source/DSP/Compressor.h index 080b21c..7c72d63 100644 --- a/Source/DSP/Compressor.h +++ b/Source/DSP/Compressor.h @@ -2,7 +2,7 @@ #include #include -class Compressor { +class Compression { public: void prepare(double sr) { sampleRate = sr; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 38e5c43..7bbb3ec 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -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(numBars); + float bottom = bounds.getBottom() - 2.0f; int maxBin = fftSize / 4; + float sampleRate = static_cast(processor.getSampleRate()); + float binHz = sampleRate / static_cast(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(bar) / static_cast(numBars); - float tNext = static_cast(bar + 1) / static_cast(numBars); + std::vector mags(numPoints + 1); + for (int i = 0; i <= numPoints; ++i) { + float t = static_cast(i) / static_cast(numPoints); int binStart = static_cast(std::pow(t, 2.0f) * static_cast(maxBin)); - int binEnd = static_cast(std::pow(tNext, 2.0f) * static_cast(maxBin)); + int binEnd = static_cast(std::pow(t + 1.0f / static_cast(numPoints), 2.0f) * static_cast(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(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); + float centerHz = static_cast((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(i) / static_cast(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(x) - 0.5f, static_cast(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(cx + col * pitch + pitch / 2); float py = static_cast(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& 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(static_cast(x), static_cast(y), static_cast(w), static_cast(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(static_cast(x), static_cast(y), static_cast(w), static_cast(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(bx), 0.0f, juce::Colour(0xff222222), static_cast(bx), static_cast(blackKeyHeight), false); diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 319b4ed..8844127 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -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; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index feea29c..2368d7f 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -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& 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& 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& buffer, juce:: float rms = std::sqrt(sumSquares / static_cast(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) diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 28b4bf4..31f9dd2 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -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;