6 Commit-ok
v1.1.2 ... main

Szerző SHA1 Üzenet Dátum
hariel1985
8652740bcf Fix Package step: skip missing Standalone/AU
Minden ellenőrzés sikeres volt
Build InstaLPEQ / build-macos (push) Successful in 2m23s
Build InstaLPEQ / build-linux (push) Successful in 6m15s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:19:32 +02:00
hariel1985
def8dd4d98 Add Gitea Actions CI/CD workflow
Some checks failed
Build InstaLPEQ / build-macos (push) Failing after 1m47s
Build InstaLPEQ / build-linux (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:14:26 +02:00
hariel1985
35cf01a163 v1.3.2: Log-spaced auto makeup gain for musical accuracy
Auto makeup now evaluates at log-spaced frequencies (equal weight per
octave) instead of linear. Fixes over-compensation on high shelf boosts
where 5k-22kHz dominated the calculation despite having little musical
energy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:57:28 +01:00
hariel1985
aa546c7357 v1.3: Auto makeup gain, spectrum analyzer, FIR normalization, README overhaul
- Auto makeup gain: RMS-based loudness compensation from actual FIR response
- Real-time FFT spectrum analyzer behind EQ curves
- FIR normalization fix: flat settings now produce exact 0 dB passthrough
- Brickwall limiter (0 dB ceiling) with toggle
- Drag-and-drop signal chain reordering
- Low FIR tap count warning for 512/1024
- Double-click reset on all knobs
- Comprehensive README with linear phase EQ explanation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:28:31 +01:00
hariel1985
9c5b5a3957 v1.2.2: Live spectrum analyzer, makeup gain, drag-and-drop signal chain
- Real-time FFT spectrum analyzer drawn behind EQ curves
- Makeup gain knob (+/- 24 dB) after limiter
- Draggable signal chain panel: reorder Master Gain / Limiter / Makeup Gain
- Chain order saved/restored with DAW session
- Scaled fonts in signal chain panel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:44:27 +01:00
hariel1985
72c7958d98 v1.1.3: Brickwall limiter + double-click reset on all knobs
- Output brickwall limiter (0 dB ceiling) with toggle in master row
- All sliders reset to default on double-click:
  Master gain → 0 dB, Freq → 1000 Hz, Gain → 0 dB, Q → 1.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:06:22 +01:00
14 fájl változott, egészen pontosan 856 új sor hozzáadva és 55 régi sor törölve

116
.gitea/workflows/build.yml Normal file
Fájl megtekintése

@@ -0,0 +1,116 @@
name: Build InstaLPEQ
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
env:
PLUGIN_NAME: InstaLPEQ
GITEA_URL: https://1git.eu
GITEA_REPO: hariel/InstaLPEQ
jobs:
build-linux:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-22.04
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
apt-get update
apt-get install -y build-essential cmake git pkg-config \
libasound2-dev libfreetype6-dev libx11-dev libxrandr-dev \
libxcursor-dev libxinerama-dev libwebkit2gtk-4.1-dev \
libcurl4-openssl-dev zip curl
- name: Clone JUCE
run: git clone --depth 1 https://github.com/juce-framework/JUCE.git ../JUCE
- name: Configure CMake
run: cmake -B build -DCMAKE_BUILD_TYPE=Release
- name: Build Release
run: cmake --build build --config Release --parallel $(nproc)
- name: Package
run: |
cd build/${PLUGIN_NAME}_artefacts/Release
zip -r ${GITHUB_WORKSPACE}/${PLUGIN_NAME}-VST3-Linux-x64.zip VST3/${PLUGIN_NAME}.vst3
cd ${GITHUB_WORKSPACE}
[ -f "build/${PLUGIN_NAME}_artefacts/Release/Standalone/${PLUGIN_NAME}" ] && zip -j ${PLUGIN_NAME}-Standalone-Linux-x64.zip build/${PLUGIN_NAME}_artefacts/Release/Standalone/${PLUGIN_NAME} || true
- name: Upload to Gitea Release
if: startsWith(github.ref, 'refs/tags/v')
run: |
TAG=${GITHUB_REF#refs/tags/}
# Find existing release or create new one
RELEASE_ID=$(curl -s "${GITEA_URL}/api/v1/repos/${GITEA_REPO}/releases/tags/${TAG}" \
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "None" ] || [ "$RELEASE_ID" = "" ]; then
RELEASE_ID=$(curl -s -X POST "${GITEA_URL}/api/v1/repos/${GITEA_REPO}/releases" \
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${TAG}\", \"name\": \"${TAG}\", \"body\": \"${PLUGIN_NAME} ${TAG}\"}" \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))")
fi
echo "Release ID: $RELEASE_ID"
# Upload assets
for f in ${PLUGIN_NAME}-*-Linux-*.zip; do
echo "Uploading $f..."
curl -s -X POST "${GITEA_URL}/api/v1/repos/${GITEA_REPO}/releases/${RELEASE_ID}/assets?name=$(basename $f)" \
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$f"
done
build-macos:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Clone JUCE
run: git clone --depth 1 https://github.com/juce-framework/JUCE.git ../JUCE || true
- name: Configure CMake (Universal)
run: cmake -B build -G Xcode -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0
- name: Build Release
run: cmake --build build --config Release
- name: Package
run: |
cd build/${PLUGIN_NAME}_artefacts/Release
zip -r ${GITHUB_WORKSPACE}/${PLUGIN_NAME}-VST3-macOS.zip VST3/${PLUGIN_NAME}.vst3
[ -d "AU/${PLUGIN_NAME}.component" ] && zip -r ${GITHUB_WORKSPACE}/${PLUGIN_NAME}-AU-macOS.zip AU/${PLUGIN_NAME}.component || true
cd ${GITHUB_WORKSPACE}
[ -d "build/${PLUGIN_NAME}_artefacts/Release/Standalone" ] && zip -r ${PLUGIN_NAME}-Standalone-macOS.zip build/${PLUGIN_NAME}_artefacts/Release/Standalone/${PLUGIN_NAME}.app || true
- name: Upload to Gitea Release
if: startsWith(github.ref, 'refs/tags/v')
run: |
TAG=${GITHUB_REF#refs/tags/}
# Get release ID (created by linux job)
RELEASE_ID=$(curl -s "${GITEA_URL}/api/v1/repos/${GITEA_REPO}/releases/tags/${TAG}" \
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))")
# If release doesn't exist yet, create it
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "None" ]; then
RELEASE_ID=$(curl -s -X POST "${GITEA_URL}/api/v1/repos/${GITEA_REPO}/releases" \
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${TAG}\", \"name\": \"${TAG}\", \"body\": \"${PLUGIN_NAME} ${TAG}\"}" \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))")
fi
# Upload assets
for f in ${PLUGIN_NAME}-*-macOS*.zip; do
curl -s -X POST "${GITEA_URL}/api/v1/repos/${GITEA_REPO}/releases/${RELEASE_ID}/assets?name=$(basename $f)" \
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$f"
done

Fájl megtekintése

@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.22) cmake_minimum_required(VERSION 3.22)
project(InstaLPEQ VERSION 1.1.2) project(InstaLPEQ VERSION 1.3.2)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -35,6 +35,7 @@ target_sources(InstaLPEQ
Source/EQCurveDisplay.cpp Source/EQCurveDisplay.cpp
Source/FIREngine.cpp Source/FIREngine.cpp
Source/NodeParameterPanel.cpp Source/NodeParameterPanel.cpp
Source/SignalChainPanel.cpp
) )
target_compile_definitions(InstaLPEQ target_compile_definitions(InstaLPEQ

144
README.md
Fájl megtekintése

@@ -4,26 +4,42 @@ Free, open-source linear phase EQ plugin built with JUCE. Available as VST3, AU
![VST3](https://img.shields.io/badge/format-VST3-blue) ![AU](https://img.shields.io/badge/format-AU-blue) ![LV2](https://img.shields.io/badge/format-LV2-blue) ![C++](https://img.shields.io/badge/language-C%2B%2B17-orange) ![JUCE](https://img.shields.io/badge/framework-JUCE-green) ![License](https://img.shields.io/badge/license-GPL--3.0-lightgrey) ![Build](https://github.com/hariel1985/InstaLPEQ/actions/workflows/build.yml/badge.svg) ![VST3](https://img.shields.io/badge/format-VST3-blue) ![AU](https://img.shields.io/badge/format-AU-blue) ![LV2](https://img.shields.io/badge/format-LV2-blue) ![C++](https://img.shields.io/badge/language-C%2B%2B17-orange) ![JUCE](https://img.shields.io/badge/framework-JUCE-green) ![License](https://img.shields.io/badge/license-GPL--3.0-lightgrey) ![Build](https://github.com/hariel1985/InstaLPEQ/actions/workflows/build.yml/badge.svg)
## Why Linear Phase EQ?
Traditional (minimum phase) EQs alter the **phase** of the signal at the frequencies they boost or cut. This causes:
- **Phase smearing** — transients lose their shape, especially on drums and percussive material
- **Asymmetric waveforms** — the signal before and after the EQ change point don't align in time
- **Coloration** — even subtle EQ moves can change the character of the sound beyond the intended frequency adjustment
A **linear phase EQ** applies the exact same time delay to all frequencies. This means:
- **Zero phase distortion** — the waveform shape is perfectly preserved
- **Pristine transients** — drums, plucks, and attacks stay tight and punchy
- **Transparent tonal shaping** — only the frequency balance changes, nothing else
- **Perfect for mastering** — no cumulative phase artifacts when stacking multiple EQ moves
- **Ideal for parallel processing** — EQ'd and dry signals stay perfectly time-aligned when summed
The trade-off is a small amount of latency (automatically compensated by the DAW), which makes linear phase EQ unsuitable for live monitoring but perfect for mixing and mastering.
## Download ## Download
**[Latest Release: v1.1](https://github.com/hariel1985/InstaLPEQ/releases/tag/v1.1)** **[Latest Release: v1.3](https://github.com/hariel1985/InstaLPEQ/releases/tag/v1.3)**
### Windows ### Windows
| File | Description | | File | Description |
|------|-------------| |------|-------------|
| [InstaLPEQ-VST3-Win64.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.1/InstaLPEQ-VST3-Win64.zip) | VST3 plugin — copy to `C:\Program Files\Common Files\VST3\` | | [InstaLPEQ-VST3-Win64.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.3/InstaLPEQ-VST3-Win64.zip) | VST3 plugin — copy to `C:\Program Files\Common Files\VST3\` |
### macOS (Universal Binary: Apple Silicon + Intel) ### macOS (Universal Binary: Apple Silicon + Intel)
| File | Description | | File | Description |
|------|-------------| |------|-------------|
| [InstaLPEQ-VST3-macOS.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.1/InstaLPEQ-VST3-macOS.zip) | VST3 plugin — copy to `~/Library/Audio/Plug-Ins/VST3/` | | [InstaLPEQ-VST3-macOS.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.3/InstaLPEQ-VST3-macOS.zip) | VST3 plugin — copy to `~/Library/Audio/Plug-Ins/VST3/` |
| [InstaLPEQ-AU-macOS.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.1/InstaLPEQ-AU-macOS.zip) | Audio Unit — copy to `~/Library/Audio/Plug-Ins/Components/` | | [InstaLPEQ-AU-macOS.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.3/InstaLPEQ-AU-macOS.zip) | Audio Unit — copy to `~/Library/Audio/Plug-Ins/Components/` |
### Linux (x64, built on Ubuntu 22.04) ### Linux (x64, built on Ubuntu 22.04)
| File | Description | | File | Description |
|------|-------------| |------|-------------|
| [InstaLPEQ-VST3-Linux-x64.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.1/InstaLPEQ-VST3-Linux-x64.zip) | VST3 plugin — copy to `~/.vst3/` | | [InstaLPEQ-VST3-Linux-x64.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.3/InstaLPEQ-VST3-Linux-x64.zip) | VST3 plugin — copy to `~/.vst3/` |
| [InstaLPEQ-LV2-Linux-x64.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.1/InstaLPEQ-LV2-Linux-x64.zip) | LV2 plugin — copy to `~/.lv2/` | | [InstaLPEQ-LV2-Linux-x64.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.3/InstaLPEQ-LV2-Linux-x64.zip) | LV2 plugin — copy to `~/.lv2/` |
> **macOS note:** Builds are Universal Binary (Apple Silicon + Intel). Not code-signed — after copying the plugin, remove the quarantine flag in Terminal: > **macOS note:** Builds are Universal Binary (Apple Silicon + Intel). Not code-signed — after copying the plugin, remove the quarantine flag in Terminal:
> ```bash > ```bash
@@ -33,43 +49,97 @@ Free, open-source linear phase EQ plugin built with JUCE. Available as VST3, AU
## Features ## Features
### Linear Phase EQ ### Linear Phase EQ Engine
- True linear phase processing using symmetric FIR convolution - True linear phase processing using symmetric FIR convolution
- Zero phase distortion at any gain setting - Zero phase distortion — the waveform shape is perfectly preserved at any gain setting
- 8192-tap FIR filter (configurable: 4096 / 8192 / 16384) - Mathematically transparent: only magnitude changes, phase stays untouched
- DAW-compensated latency (~93ms at 44.1kHz default) - FIR impulse response normalized for unity passthrough (0 dB at flat settings)
- Background thread FIR generation — glitch-free parameter changes - Background thread FIR generation — glitch-free, click-free parameter changes
- DAW-compensated latency for seamless integration
### Configurable FIR Resolution
Six quality levels to balance precision vs. latency:
| Taps | Latency (44.1 kHz) | Best for |
|------|---------------------|----------|
| 512 | ~6 ms | Low-latency monitoring |
| 1024 | ~12 ms | Tracking |
| **2048** | **~23 ms** | **Default — mixing** |
| 4096 | ~46 ms | Detailed work |
| 8192 | ~93 ms | Mastering |
| 16384 | ~186 ms | Maximum precision |
Low tap counts have reduced accuracy below ~100 Hz — a warning is displayed when using 512 or 1024 taps.
### Interactive EQ Curve Display ### Interactive EQ Curve Display
- Logarithmic frequency axis (20 Hz — 20 kHz) - Logarithmic frequency axis (20 Hz — 20 kHz)
- Linear gain axis (-24 dB to +24 dB) - Linear gain axis (-24 dB to +24 dB)
- Click to add EQ nodes (up to 8 bands) - Click anywhere to add an EQ node (up to 8 bands)
- Drag nodes to adjust frequency and gain - Drag nodes to adjust frequency and gain in real time
- Scroll wheel to adjust Q/bandwidth - Scroll wheel over a node to adjust Q/bandwidth
- Right-click for band type selection and delete - Right-click a node for band type selection or delete
- Double-click to reset band to 0 dB - Double-click a node to reset it to 0 dB
- Real-time frequency response curve with glow effect - Combined frequency response curve with glow effect
- Per-band curve overlay - Individual per-band curve overlays (color-coded)
- Real-time FFT spectrum analyzer behind the EQ curves (shows live audio content)
### Band Types ### Band Types
- Peak (parametric) - **Peak** (parametric) — boost or cut a specific frequency range
- Low Shelf - **Low Shelf** — boost or cut everything below a frequency
- High Shelf - **High Shelf** — boost or cut everything above a frequency
### Auto Makeup Gain
- Automatically compensates for the loudness change caused by EQ settings
- Computed from the actual FIR frequency response (not theoretical) — accounts for FIR resolution limits
- RMS-based calculation with linear frequency weighting (matches white noise / broadband signals)
- Toggleable on/off — displays the current compensation value in dB
- Mastering-safe: fixed value based on EQ curve, no signal-dependent gain changes
### Output Limiter
- Brickwall limiter with 0 dB ceiling
- Toggleable on/off
- Prevents clipping when applying large EQ boosts
- 50 ms release time
### Drag-and-Drop Signal Chain
- Reorderable processing chain at the bottom of the GUI
- Three blocks: **Master Gain**, **Limiter**, **Auto Gain**
- Drag blocks to change processing order (e.g., put limiter before or after gain)
- Visual arrows show signal flow direction
- Chain order saved/restored with DAW session
### Controls ### Controls
- Per-band: Frequency, Gain, Q knobs - **Per-band:** Frequency, Gain, Q knobs with 3D metal styling
- Master gain (+/- 24 dB) - **Master Gain:** +/- 24 dB output level control
- Bypass toggle - **Bypass:** global bypass toggle
- State save/restore (DAW session recall) - **New Band:** button to add a new EQ node at 1 kHz / 0 dB
- **FIR Quality:** dropdown to select tap count / latency
- All knobs reset to default on double-click
### GUI ### GUI
- Dark modern UI matching InstaDrums visual style - Dark modern UI with InstaDrums visual style
- 3D metal knobs with glow effects (orange for EQ, blue for Q) - 3D metal knobs with multi-layer glow effects (orange for frequency/gain, blue for Q)
- Carbon fiber background texture - Carbon fiber background texture
- Rajdhani custom font - Rajdhani custom font (embedded)
- Fully resizable window with proportional scaling - Fully resizable window (700x450 — 1920x1080) with proportional scaling
- Animated toggle switches - Animated toggle switches with smooth lerp
- Color-coded EQ bands (8 distinct colors) - Color-coded EQ bands (8 distinct colors)
- All fonts and UI elements scale with window size
- State save/restore — all settings recalled with DAW session
## How It Works
InstaLPEQ uses a **FIR-based linear phase** approach:
1. Each EQ band's target magnitude response is computed from IIR filter coefficients (Peak, Low Shelf, or High Shelf)
2. All band magnitudes are multiplied together to form the combined target frequency response
3. An inverse FFT converts the magnitude-only spectrum (zero phase) into a symmetric time-domain impulse response
4. A Blackman-Harris window is applied to minimize truncation artifacts
5. The FIR is normalized so a flat spectrum produces exactly 0 dB passthrough
6. The FIR filter is applied via JUCE's efficient FFT-based partitioned `Convolution` engine
7. Auto makeup gain is computed from the actual FIR frequency response (forward FFT of the final filter)
This ensures **mathematically perfect phase linearity** — the only thing that changes is the frequency balance. The original waveform shape, transient character, and stereo image are completely preserved.
## Building ## Building
@@ -105,22 +175,10 @@ Output:
- AU: `build/InstaLPEQ_artefacts/Release/AU/InstaLPEQ.component` (macOS) - AU: `build/InstaLPEQ_artefacts/Release/AU/InstaLPEQ.component` (macOS)
- LV2: `build/InstaLPEQ_artefacts/Release/LV2/InstaLPEQ.lv2` - LV2: `build/InstaLPEQ_artefacts/Release/LV2/InstaLPEQ.lv2`
## How It Works
InstaLPEQ uses a **FIR-based linear phase** approach:
1. Each EQ band's target magnitude response is computed from IIR filter coefficients (Peak, Low Shelf, or High Shelf)
2. All band magnitudes are multiplied together to form the combined target response
3. An inverse FFT converts the magnitude-only spectrum into a symmetric time-domain impulse response
4. A Blackman-Harris window is applied to minimize truncation artifacts
5. The FIR filter is applied via JUCE's efficient FFT-based `Convolution` engine
This ensures **zero phase distortion** regardless of EQ settings — ideal for mastering, surgical corrections, and transparent tonal shaping.
## Tech Stack ## Tech Stack
- **Language:** C++17 - **Language:** C++17
- **Framework:** JUCE 8 - **Framework:** JUCE 8
- **Build:** CMake + MSVC / Xcode / GCC - **Build:** CMake + MSVC / Xcode / GCC
- **Audio DSP:** juce::dsp (FFT, Convolution, IIR coefficient design) - **Audio DSP:** juce::dsp (FFT, Convolution, IIR coefficient design, Limiter)
- **Font:** Rajdhani (SIL Open Font License) - **Font:** Rajdhani (SIL Open Font License)

Fájl megtekintése

@@ -17,6 +17,13 @@ void EQCurveDisplay::setMagnitudeResponse (const std::vector<float>& magnitudesD
repaint(); repaint();
} }
void EQCurveDisplay::setSpectrum (const float* data, int numBins, double sampleRate, int fftSize)
{
spectrumDb.assign (data, data + numBins);
spectrumSampleRate = sampleRate;
spectrumFftSize = fftSize;
}
void EQCurveDisplay::setSelectedBand (int index) void EQCurveDisplay::setSelectedBand (int index)
{ {
if (selectedBand != index) if (selectedBand != index)
@@ -93,6 +100,7 @@ void EQCurveDisplay::paint (juce::Graphics& g)
g.drawRoundedRectangle (bounds, 4.0f, 1.0f); g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
drawGrid (g); drawGrid (g);
drawSpectrum (g);
drawPerBandCurves (g); drawPerBandCurves (g);
drawResponseCurve (g); drawResponseCurve (g);
drawNodes (g); drawNodes (g);
@@ -140,6 +148,55 @@ void EQCurveDisplay::drawGrid (juce::Graphics& g)
} }
} }
void EQCurveDisplay::drawSpectrum (juce::Graphics& g)
{
if (spectrumDb.empty())
return;
auto area = getPlotArea();
int numBins = (int) spectrumDb.size();
juce::Path specPath;
specPath.startNewSubPath (area.getX(), area.getBottom());
bool hasPoints = false;
for (float px = area.getX(); px <= area.getRight(); px += 1.5f)
{
float freq = xToFreq (px);
if (freq < 1.0f || freq > spectrumSampleRate * 0.5)
continue;
float binFloat = freq * (float) spectrumFftSize / (float) spectrumSampleRate;
int bin = (int) binFloat;
float frac = binFloat - (float) bin;
if (bin < 0 || bin >= numBins - 1)
continue;
float dbVal = spectrumDb[bin] * (1.0f - frac) + spectrumDb[bin + 1] * frac;
// Map dB range: -100 dB = bottom, 0 dB = top area
// Shift up so typical audio is visible
float mapped = juce::jmap (dbVal, -80.0f, 0.0f, minDb, maxDb);
mapped = juce::jlimit (minDb - 6.0f, maxDb, mapped);
float yPos = dbToY (mapped);
specPath.lineTo (px, yPos);
hasPoints = true;
}
if (! hasPoints)
return;
specPath.lineTo (area.getRight(), area.getBottom());
specPath.closeSubPath();
// Fill with subtle gradient
juce::ColourGradient specGrad (juce::Colour (0xff4488ff).withAlpha (0.12f), 0, area.getY(),
juce::Colour (0xff4488ff).withAlpha (0.03f), 0, area.getBottom(), false);
g.setGradientFill (specGrad);
g.fillPath (specPath);
}
void EQCurveDisplay::drawResponseCurve (juce::Graphics& g) void EQCurveDisplay::drawResponseCurve (juce::Graphics& g)
{ {
if (magnitudeResponseDb.empty()) if (magnitudeResponseDb.empty())

Fájl megtekintése

@@ -19,6 +19,7 @@ public:
void setListener (Listener* l) { listener = l; } void setListener (Listener* l) { listener = l; }
void setBands (const std::vector<EQBand>& bands); void setBands (const std::vector<EQBand>& bands);
void setMagnitudeResponse (const std::vector<float>& magnitudesDb, double sampleRate, int fftSize); void setMagnitudeResponse (const std::vector<float>& magnitudesDb, double sampleRate, int fftSize);
void setSpectrum (const float* data, int numBins, double sampleRate, int fftSize);
int getSelectedBandIndex() const { return selectedBand; } int getSelectedBandIndex() const { return selectedBand; }
void setSelectedBand (int index); void setSelectedBand (int index);
@@ -32,6 +33,9 @@ public:
private: private:
std::vector<EQBand> bands; std::vector<EQBand> bands;
std::vector<float> magnitudeResponseDb; std::vector<float> magnitudeResponseDb;
std::vector<float> spectrumDb;
double spectrumSampleRate = 44100.0;
int spectrumFftSize = 2048;
double responseSampleRate = 44100.0; double responseSampleRate = 44100.0;
int responseFftSize = 8192; int responseFftSize = 8192;
int selectedBand = -1; int selectedBand = -1;
@@ -62,6 +66,7 @@ private:
float yToDb (float y) const; float yToDb (float y) const;
void drawGrid (juce::Graphics& g); void drawGrid (juce::Graphics& g);
void drawSpectrum (juce::Graphics& g);
void drawResponseCurve (juce::Graphics& g); void drawResponseCurve (juce::Graphics& g);
void drawPerBandCurves (juce::Graphics& g); void drawPerBandCurves (juce::Graphics& g);
void drawNodes (juce::Graphics& g); void drawNodes (juce::Graphics& g);

Fájl megtekintése

@@ -124,11 +124,11 @@ juce::AudioBuffer<float> FIREngine::generateFIR (const std::vector<EQBand>& band
magnitudes[i] *= bandMag[i]; magnitudes[i] *= bandMag[i];
} }
// Store magnitude in dB for display // Store theoretical magnitude in dB for display (from IIR target curve)
{ {
std::vector<float> magDb (numBins); std::vector<float> magDb (numBins);
for (int i = 0; i < numBins; ++i) for (int i = 0; i < numBins; ++i)
magDb[i] = (float) juce::Decibels::gainToDecibels (magnitudes[i], -60.0); magDb[i] = (float) juce::Decibels::gainToDecibels ((float) magnitudes[i], -60.0f);
const juce::SpinLock::ScopedLockType lock (magLock); const juce::SpinLock::ScopedLockType lock (magLock);
magnitudeDb = std::move (magDb); magnitudeDb = std::move (magDb);
@@ -168,5 +168,94 @@ juce::AudioBuffer<float> FIREngine::generateFIR (const std::vector<EQBand>& band
juce::dsp::WindowingFunction<float> window (fftSize, juce::dsp::WindowingFunction<float>::blackmanHarris); juce::dsp::WindowingFunction<float> window (fftSize, juce::dsp::WindowingFunction<float>::blackmanHarris);
window.multiplyWithWindowingTable (firData, fftSize); window.multiplyWithWindowingTable (firData, fftSize);
// Normalize: ensure flat spectrum → unity DC gain
// Without this, IFFT scaling + windowing cause incorrect base level
float dcGain = 0.0f;
for (int i = 0; i < fftSize; ++i)
dcGain += firData[i];
if (std::abs (dcGain) > 1e-6f)
{
float normFactor = 1.0f / dcGain;
for (int i = 0; i < fftSize; ++i)
firData[i] *= normFactor;
}
// Compute auto makeup from the ACTUAL final FIR frequency response
// (includes windowing + normalization effects)
{
std::vector<float> analysisBuf (fftSize * 2, 0.0f);
std::copy (firData, firData + fftSize, analysisBuf.data());
juce::dsp::FFT analysisFft (order);
analysisFft.performRealOnlyForwardTransform (analysisBuf.data());
// Extract actual magnitude from FFT result
// Format: [DC_real, Nyquist_real, bin1_real, bin1_imag, bin2_real, bin2_imag, ...]
// Evaluate at log-spaced frequencies (equal weight per octave)
// This matches musical content much better than linear spacing
// (linear gives 77% weight to 5k-22k where music has little energy)
const int numPoints = 512;
double powerSum = 0.0;
double binRes = sr / (double) fftSize; // Hz per bin
for (int p = 0; p < numPoints; ++p)
{
// Log-spaced 20 Hz — 20 kHz
double freq = 20.0 * std::pow (1000.0, (double) p / (double) (numPoints - 1));
// Interpolate magnitude from FFT bins
double binFloat = freq / binRes;
int bin = (int) binFloat;
double frac = binFloat - (double) bin;
if (bin < 1 || bin >= fftSize / 2 - 1)
continue;
float re0 = analysisBuf[bin * 2];
float im0 = analysisBuf[bin * 2 + 1];
float re1 = analysisBuf[(bin + 1) * 2];
float im1 = analysisBuf[(bin + 1) * 2 + 1];
double mag0 = std::sqrt ((double) (re0 * re0 + im0 * im0));
double mag1 = std::sqrt ((double) (re1 * re1 + im1 * im1));
double mag = mag0 * (1.0 - frac) + mag1 * frac;
powerSum += mag * mag;
}
if (numPoints > 0)
{
double avgPower = powerSum / (double) numPoints;
float rmsGain = (float) std::sqrt (avgPower);
float makeupDb = -20.0f * std::log10 (std::max (rmsGain, 1e-10f));
autoMakeupDb.store (makeupDb);
}
// (magnitudeDb stays as theoretical IIR curve for display)
}
return firBuffer; return firBuffer;
} }
// A-weighting curve (IEC 61672:2003)
// Returns linear amplitude weighting factor for given frequency
float FIREngine::aWeighting (float f)
{
if (f < 10.0f) return 0.0f;
double f2 = (double) f * (double) f;
double f4 = f2 * f2;
double num = 12194.0 * 12194.0 * f4;
double den = (f2 + 20.6 * 20.6)
* std::sqrt ((f2 + 107.7 * 107.7) * (f2 + 737.9 * 737.9))
* (f2 + 12194.0 * 12194.0);
double ra = num / den;
// Normalize so A(1000 Hz) = 1.0
// A(1000) unnormalized ≈ 0.7943
static const double norm = 1.0 / 0.7943282347;
return (float) (ra * norm);
}

Fájl megtekintése

@@ -27,6 +27,9 @@ public:
int getFIRLength() const { return 1 << fftOrder.load(); } int getFIRLength() const { return 1 << fftOrder.load(); }
int getLatencySamples() const { return getFIRLength() / 2; } int getLatencySamples() const { return getFIRLength() / 2; }
// Auto makeup gain: A-weighted RMS loudness compensation (dB)
float getAutoMakeupGainDb() const { return autoMakeupDb.load(); }
private: private:
void run() override; void run() override;
juce::AudioBuffer<float> generateFIR (const std::vector<EQBand>& bands, double sr, int order); juce::AudioBuffer<float> generateFIR (const std::vector<EQBand>& bands, double sr, int order);
@@ -43,4 +46,7 @@ private:
std::vector<float> magnitudeDb; std::vector<float> magnitudeDb;
mutable juce::SpinLock magLock; mutable juce::SpinLock magLock;
std::atomic<float> autoMakeupDb { 0.0f };
static float aWeighting (float freq);
}; };

Fájl megtekintése

@@ -5,11 +5,14 @@ NodeParameterPanel::NodeParameterPanel()
{ {
setupSlider (freqSlider, freqLabel, 20.0, 20000.0, 1.0, " Hz"); setupSlider (freqSlider, freqLabel, 20.0, 20000.0, 1.0, " Hz");
freqSlider.setSkewFactorFromMidPoint (1000.0); freqSlider.setSkewFactorFromMidPoint (1000.0);
freqSlider.setDoubleClickReturnValue (true, 1000.0);
setupSlider (gainSlider, gainLabel, -24.0, 24.0, 0.1, " dB"); setupSlider (gainSlider, gainLabel, -24.0, 24.0, 0.1, " dB");
gainSlider.setDoubleClickReturnValue (true, 0.0);
setupSlider (qSlider, qLabel, 0.1, 18.0, 0.01, ""); setupSlider (qSlider, qLabel, 0.1, 18.0, 0.01, "");
qSlider.setSkewFactorFromMidPoint (1.0); qSlider.setSkewFactorFromMidPoint (1.0);
qSlider.setDoubleClickReturnValue (true, 1.0);
qSlider.getProperties().set (InstaLPEQLookAndFeel::knobTypeProperty, "dark"); qSlider.getProperties().set (InstaLPEQLookAndFeel::knobTypeProperty, "dark");
typeSelector.addItem ("Peak", 1); typeSelector.addItem ("Peak", 1);

Fájl megtekintése

@@ -48,12 +48,22 @@ InstaLPEQEditor::InstaLPEQEditor (InstaLPEQProcessor& p)
int sel = qualitySelector.getSelectedId(); int sel = qualitySelector.getSelectedId();
int order = sel + 8; // 1->9, 2->10, 3->11, 4->12, 5->13, 6->14 int order = sel + 8; // 1->9, 2->10, 3->11, 4->12, 5->13, 6->14
processor.setQuality (order); processor.setQuality (order);
if (sel <= 2) // 512 or 1024
qualityWarning.setText ("Low freq accuracy reduced", juce::dontSendNotification);
else
qualityWarning.setText ("", juce::dontSendNotification);
}; };
addAndMakeVisible (qualitySelector); addAndMakeVisible (qualitySelector);
qualityLabel.setFont (customLookAndFeel.getMediumFont (13.0f)); qualityLabel.setFont (customLookAndFeel.getMediumFont (13.0f));
qualityLabel.setJustificationType (juce::Justification::centredRight); qualityLabel.setJustificationType (juce::Justification::centredRight);
addAndMakeVisible (qualityLabel); addAndMakeVisible (qualityLabel);
qualityWarning.setFont (customLookAndFeel.getRegularFont (11.0f));
qualityWarning.setColour (juce::Label::textColourId, juce::Colour (0xffff6644));
qualityWarning.setJustificationType (juce::Justification::centredRight);
addAndMakeVisible (qualityWarning);
// EQ curve // EQ curve
curveDisplay.setListener (this); curveDisplay.setListener (this);
addAndMakeVisible (curveDisplay); addAndMakeVisible (curveDisplay);
@@ -68,11 +78,37 @@ InstaLPEQEditor::InstaLPEQEditor (InstaLPEQProcessor& p)
masterGainSlider.setRange (-24.0, 24.0, 0.1); masterGainSlider.setRange (-24.0, 24.0, 0.1);
masterGainSlider.setValue (0.0); masterGainSlider.setValue (0.0);
masterGainSlider.setTextValueSuffix (" dB"); masterGainSlider.setTextValueSuffix (" dB");
masterGainSlider.setDoubleClickReturnValue (true, 0.0);
addAndMakeVisible (masterGainSlider); addAndMakeVisible (masterGainSlider);
masterGainLabel.setFont (customLookAndFeel.getMediumFont (13.0f)); masterGainLabel.setFont (customLookAndFeel.getMediumFont (13.0f));
masterGainLabel.setJustificationType (juce::Justification::centred); masterGainLabel.setJustificationType (juce::Justification::centred);
addAndMakeVisible (masterGainLabel); addAndMakeVisible (masterGainLabel);
// Limiter toggle
limiterToggle.setToggleState (processor.limiterEnabled.load(), juce::dontSendNotification);
addAndMakeVisible (limiterToggle);
limiterLabel.setFont (customLookAndFeel.getMediumFont (13.0f));
limiterLabel.setColour (juce::Label::textColourId, InstaLPEQLookAndFeel::textSecondary);
limiterLabel.setJustificationType (juce::Justification::centred);
addAndMakeVisible (limiterLabel);
// Auto makeup gain
autoMakeupToggle.setToggleState (processor.autoMakeupEnabled.load(), juce::dontSendNotification);
addAndMakeVisible (autoMakeupToggle);
autoMakeupLabel.setFont (customLookAndFeel.getMediumFont (13.0f));
autoMakeupLabel.setColour (juce::Label::textColourId, InstaLPEQLookAndFeel::textSecondary);
autoMakeupLabel.setJustificationType (juce::Justification::centred);
addAndMakeVisible (autoMakeupLabel);
autoMakeupValue.setFont (customLookAndFeel.getRegularFont (12.0f));
autoMakeupValue.setColour (juce::Label::textColourId, InstaLPEQLookAndFeel::accent);
autoMakeupValue.setJustificationType (juce::Justification::centred);
addAndMakeVisible (autoMakeupValue);
// Signal chain panel
chainPanel.setListener (this);
chainPanel.setOrder (processor.getChainOrder());
addAndMakeVisible (chainPanel);
// Sizing // Sizing
constrainer.setMinimumSize (700, 450); constrainer.setMinimumSize (700, 450);
constrainer.setMaximumSize (1920, 1080); constrainer.setMaximumSize (1920, 1080);
@@ -135,7 +171,11 @@ void InstaLPEQEditor::resized()
newBandButton.setBounds (header.removeFromRight ((int) (90 * scale)).reduced (2)); newBandButton.setBounds (header.removeFromRight ((int) (90 * scale)).reduced (2));
// Bottom master row // Signal chain panel (bottom-most)
int chainH = (int) std::max (28.0f, 36.0f * scale);
chainPanel.setBounds (bounds.removeFromBottom (chainH).reduced (pad, 2));
// Master controls row (above chain)
int masterH = (int) std::max (50.0f, 65.0f * scale); int masterH = (int) std::max (50.0f, 65.0f * scale);
auto masterArea = bounds.removeFromBottom (masterH).reduced (pad, 2); auto masterArea = bounds.removeFromBottom (masterH).reduced (pad, 2);
@@ -144,11 +184,25 @@ void InstaLPEQEditor::resized()
masterGainLabel.setBounds (labelArea); masterGainLabel.setBounds (labelArea);
masterGainSlider.setBounds (masterArea.removeFromLeft (masterH)); masterGainSlider.setBounds (masterArea.removeFromLeft (masterH));
// Limiter toggle next to master gain
limiterLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale)));
limiterLabel.setBounds (masterArea.removeFromLeft (55));
limiterToggle.setBounds (masterArea.removeFromLeft (40));
// Auto makeup gain toggle + value display
autoMakeupLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale)));
autoMakeupLabel.setBounds (masterArea.removeFromLeft (70));
autoMakeupToggle.setBounds (masterArea.removeFromLeft (40));
autoMakeupValue.setFont (customLookAndFeel.getRegularFont (std::max (10.0f, 12.0f * scale)));
autoMakeupValue.setBounds (masterArea.removeFromLeft (60));
// Quality selector on the right side of master row // Quality selector on the right side of master row
qualityLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale))); qualityLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale)));
auto qLabelArea = masterArea.removeFromRight (30); auto qLabelArea = masterArea.removeFromRight (30);
qualityLabel.setBounds (qLabelArea); qualityLabel.setBounds (qLabelArea);
qualitySelector.setBounds (masterArea.removeFromRight ((int) (130 * scale)).reduced (2, (masterH - 24) / 2)); qualitySelector.setBounds (masterArea.removeFromRight ((int) (130 * scale)).reduced (2, (masterH - 24) / 2));
qualityWarning.setFont (customLookAndFeel.getRegularFont (std::max (9.0f, 11.0f * scale)));
qualityWarning.setBounds (masterArea.removeFromRight ((int) (170 * scale)));
// Node parameter panel (15% of remaining height) // Node parameter panel (15% of remaining height)
int nodePanelH = (int) (bounds.getHeight() * 0.18f); int nodePanelH = (int) (bounds.getHeight() * 0.18f);
@@ -162,9 +216,24 @@ void InstaLPEQEditor::resized()
void InstaLPEQEditor::timerCallback() void InstaLPEQEditor::timerCallback()
{ {
// Sync bypass // Sync bypass & limiter
processor.bypassed.store (bypassToggle.getToggleState()); processor.bypassed.store (bypassToggle.getToggleState());
processor.masterGainDb.store ((float) masterGainSlider.getValue()); processor.masterGainDb.store ((float) masterGainSlider.getValue());
processor.limiterEnabled.store (limiterToggle.getToggleState());
processor.autoMakeupEnabled.store (autoMakeupToggle.getToggleState());
// Update auto makeup display
float mkDb = processor.getActiveAutoMakeupDb();
juce::String mkStr = (mkDb >= 0 ? "+" : "") + juce::String (mkDb, 1) + " dB";
autoMakeupValue.setText (mkStr, juce::dontSendNotification);
// Update spectrum analyzer
{
std::array<float, 1024> specData {};
if (processor.getSpectrum (specData.data(), (int) specData.size()))
curveDisplay.setSpectrum (specData.data(), (int) specData.size(),
processor.getCurrentSampleRate(), 2048);
}
// Update display with latest magnitude response // Update display with latest magnitude response
auto magDb = processor.getFIREngine().getMagnitudeResponseDb(); auto magDb = processor.getFIREngine().getMagnitudeResponseDb();
@@ -240,6 +309,11 @@ void InstaLPEQEditor::nodeDeleteRequested (int bandIndex)
syncDisplayFromProcessor(); syncDisplayFromProcessor();
} }
void InstaLPEQEditor::chainOrderChanged (const std::array<InstaLPEQProcessor::ChainStage, InstaLPEQProcessor::numChainStages>& order)
{
processor.setChainOrder (order);
}
void InstaLPEQEditor::syncDisplayFromProcessor() void InstaLPEQEditor::syncDisplayFromProcessor()
{ {
auto currentBands = processor.getBands(); auto currentBands = processor.getBands();

Fájl megtekintése

@@ -4,11 +4,13 @@
#include "LookAndFeel.h" #include "LookAndFeel.h"
#include "EQCurveDisplay.h" #include "EQCurveDisplay.h"
#include "NodeParameterPanel.h" #include "NodeParameterPanel.h"
#include "SignalChainPanel.h"
class InstaLPEQEditor : public juce::AudioProcessorEditor, class InstaLPEQEditor : public juce::AudioProcessorEditor,
private juce::Timer, private juce::Timer,
private EQCurveDisplay::Listener, private EQCurveDisplay::Listener,
private NodeParameterPanel::Listener private NodeParameterPanel::Listener,
private SignalChainPanel::Listener
{ {
public: public:
explicit InstaLPEQEditor (InstaLPEQProcessor& p); explicit InstaLPEQEditor (InstaLPEQProcessor& p);
@@ -30,6 +32,9 @@ private:
void nodeParameterChanged (int bandIndex, const EQBand& band) override; void nodeParameterChanged (int bandIndex, const EQBand& band) override;
void nodeDeleteRequested (int bandIndex) override; void nodeDeleteRequested (int bandIndex) override;
// SignalChainPanel::Listener
void chainOrderChanged (const std::array<InstaLPEQProcessor::ChainStage, InstaLPEQProcessor::numChainStages>& order) override;
void syncDisplayFromProcessor(); void syncDisplayFromProcessor();
InstaLPEQProcessor& processor; InstaLPEQProcessor& processor;
@@ -39,16 +44,24 @@ private:
NodeParameterPanel nodePanel; NodeParameterPanel nodePanel;
juce::Label titleLabel { {}, "INSTALPEQ" }; juce::Label titleLabel { {}, "INSTALPEQ" };
juce::Label versionLabel { {}, "v1.1.2" }; juce::Label versionLabel { {}, "v1.3.2" };
juce::ToggleButton bypassToggle; juce::ToggleButton bypassToggle;
juce::Label bypassLabel { {}, "BYPASS" }; juce::Label bypassLabel { {}, "BYPASS" };
juce::TextButton newBandButton { "NEW BAND" }; juce::TextButton newBandButton { "NEW BAND" };
juce::ComboBox qualitySelector; juce::ComboBox qualitySelector;
juce::Label qualityLabel { {}, "FIR" }; juce::Label qualityLabel { {}, "FIR" };
juce::Label qualityWarning { {}, "" };
juce::Slider masterGainSlider; juce::Slider masterGainSlider;
juce::Label masterGainLabel { {}, "MASTER" }; juce::Label masterGainLabel { {}, "MASTER" };
juce::ToggleButton limiterToggle;
juce::Label limiterLabel { {}, "LIMITER" };
juce::ToggleButton autoMakeupToggle;
juce::Label autoMakeupLabel { {}, "AUTO GAIN" };
juce::Label autoMakeupValue { {}, "0.0 dB" };
SignalChainPanel chainPanel;
juce::ComponentBoundsConstrainer constrainer; juce::ComponentBoundsConstrainer constrainer;

Fájl megtekintése

@@ -28,6 +28,9 @@ void InstaLPEQProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
juce::dsp::ProcessSpec spec { sampleRate, (juce::uint32) samplesPerBlock, 2 }; juce::dsp::ProcessSpec spec { sampleRate, (juce::uint32) samplesPerBlock, 2 };
convolution.prepare (spec); convolution.prepare (spec);
limiter.prepare (spec);
limiter.setThreshold (0.0f);
limiter.setRelease (50.0f);
firEngine.start (sampleRate); firEngine.start (sampleRate);
updateFIR(); updateFIR();
@@ -58,15 +61,89 @@ void InstaLPEQProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::M
if (bypassed.load() || ! firLoaded) if (bypassed.load() || ! firLoaded)
return; return;
// Process through convolution // Process through convolution (EQ)
juce::dsp::AudioBlock<float> block (buffer); juce::dsp::AudioBlock<float> block (buffer);
juce::dsp::ProcessContextReplacing<float> context (block); juce::dsp::ProcessContextReplacing<float> context (block);
convolution.process (context); convolution.process (context);
// Apply master gain // Apply chain in configured order
float gain = juce::Decibels::decibelsToGain (masterGainDb.load()); std::array<ChainStage, numChainStages> order;
if (std::abs (gain - 1.0f) > 0.001f) {
buffer.applyGain (gain); const juce::SpinLock::ScopedTryLockType lock (chainLock);
if (lock.isLocked())
order = chainOrder;
else
order = { MasterGain, Limiter, MakeupGain };
}
for (auto stage : order)
{
switch (stage)
{
case MasterGain:
{
float gain = juce::Decibels::decibelsToGain (masterGainDb.load());
if (std::abs (gain - 1.0f) > 0.001f)
buffer.applyGain (gain);
break;
}
case Limiter:
{
if (limiterEnabled.load())
{
juce::dsp::AudioBlock<float> limBlock (buffer);
juce::dsp::ProcessContextReplacing<float> limContext (limBlock);
limiter.process (limContext);
}
break;
}
case MakeupGain:
{
if (autoMakeupEnabled.load())
{
float mkGain = juce::Decibels::decibelsToGain (firEngine.getAutoMakeupGainDb());
if (std::abs (mkGain - 1.0f) > 0.001f)
buffer.applyGain (mkGain);
}
break;
}
default: break;
}
}
// Feed spectrum analyzer (mono mix of output)
const int numSamples = buffer.getNumSamples();
const int numChannels = buffer.getNumChannels();
for (int i = 0; i < numSamples; ++i)
{
float sample = 0.0f;
for (int ch = 0; ch < numChannels; ++ch)
sample += buffer.getSample (ch, i);
sample /= (float) numChannels;
fifoBuffer[fifoIndex++] = sample;
if (fifoIndex >= spectrumFFTSize)
{
fifoIndex = 0;
std::copy (fifoBuffer.begin(), fifoBuffer.end(), fftData.begin());
std::fill (fftData.begin() + spectrumFFTSize, fftData.end(), 0.0f);
spectrumWindow.multiplyWithWindowingTable (fftData.data(), spectrumFFTSize);
spectrumFFT.performFrequencyOnlyForwardTransform (fftData.data());
{
const juce::SpinLock::ScopedLockType lock (spectrumLock);
for (int b = 0; b < spectrumFFTSize / 2; ++b)
{
float mag = fftData[b] / (float) spectrumFFTSize;
float dbVal = juce::Decibels::gainToDecibels (mag, -100.0f);
// Smooth: 70% old + 30% new
spectrumMagnitude[b] = spectrumMagnitude[b] * 0.7f + dbVal * 0.3f;
}
}
spectrumReady.store (true);
}
}
} }
// ============================================================ // ============================================================
@@ -119,6 +196,37 @@ int InstaLPEQProcessor::getNumBands() const
return (int) bands.size(); return (int) bands.size();
} }
bool InstaLPEQProcessor::getSpectrum (float* dest, int maxBins) const
{
if (! spectrumReady.load())
return false;
const juce::SpinLock::ScopedTryLockType lock (spectrumLock);
if (! lock.isLocked())
return false;
int bins = std::min (maxBins, spectrumFFTSize / 2);
std::copy (spectrumMagnitude.begin(), spectrumMagnitude.begin() + bins, dest);
return true;
}
float InstaLPEQProcessor::getActiveAutoMakeupDb() const
{
return autoMakeupEnabled.load() ? firEngine.getAutoMakeupGainDb() : 0.0f;
}
std::array<InstaLPEQProcessor::ChainStage, InstaLPEQProcessor::numChainStages> InstaLPEQProcessor::getChainOrder() const
{
const juce::SpinLock::ScopedLockType lock (chainLock);
return chainOrder;
}
void InstaLPEQProcessor::setChainOrder (const std::array<ChainStage, numChainStages>& order)
{
const juce::SpinLock::ScopedLockType lock (chainLock);
chainOrder = order;
}
void InstaLPEQProcessor::updateFIR() void InstaLPEQProcessor::updateFIR()
{ {
auto currentBands = getBands(); auto currentBands = getBands();
@@ -141,6 +249,17 @@ void InstaLPEQProcessor::getStateInformation (juce::MemoryBlock& destData)
juce::XmlElement xml ("InstaLPEQ"); juce::XmlElement xml ("InstaLPEQ");
xml.setAttribute ("bypass", bypassed.load()); xml.setAttribute ("bypass", bypassed.load());
xml.setAttribute ("masterGain", (double) masterGainDb.load()); xml.setAttribute ("masterGain", (double) masterGainDb.load());
xml.setAttribute ("limiter", limiterEnabled.load());
xml.setAttribute ("autoMakeup", autoMakeupEnabled.load());
auto order = getChainOrder();
juce::String chainStr;
for (int i = 0; i < numChainStages; ++i)
{
if (i > 0) chainStr += ",";
chainStr += juce::String ((int) order[i]);
}
xml.setAttribute ("chainOrder", chainStr);
auto currentBands = getBands(); auto currentBands = getBands();
for (int i = 0; i < (int) currentBands.size(); ++i) for (int i = 0; i < (int) currentBands.size(); ++i)
@@ -164,6 +283,18 @@ void InstaLPEQProcessor::setStateInformation (const void* data, int sizeInBytes)
bypassed.store (xml->getBoolAttribute ("bypass", false)); bypassed.store (xml->getBoolAttribute ("bypass", false));
masterGainDb.store ((float) xml->getDoubleAttribute ("masterGain", 0.0)); masterGainDb.store ((float) xml->getDoubleAttribute ("masterGain", 0.0));
limiterEnabled.store (xml->getBoolAttribute ("limiter", true));
autoMakeupEnabled.store (xml->getBoolAttribute ("autoMakeup", true));
auto chainStr = xml->getStringAttribute ("chainOrder", "0,1,2");
auto tokens = juce::StringArray::fromTokens (chainStr, ",", "");
if (tokens.size() == numChainStages)
{
std::array<ChainStage, numChainStages> order;
for (int i = 0; i < numChainStages; ++i)
order[i] = static_cast<ChainStage> (tokens[i].getIntValue());
setChainOrder (order);
}
std::vector<EQBand> loadedBands; std::vector<EQBand> loadedBands;
for (auto* bandXml : xml->getChildIterator()) for (auto* bandXml : xml->getChildIterator())

Fájl megtekintése

@@ -40,9 +40,22 @@ public:
void removeBand (int index); void removeBand (int index);
int getNumBands() const; int getNumBands() const;
// Signal chain stages
enum ChainStage { MasterGain = 0, Limiter, MakeupGain, NumStages };
static constexpr int numChainStages = (int) NumStages;
// Settings // Settings
std::atomic<bool> bypassed { false }; std::atomic<bool> bypassed { false };
std::atomic<float> masterGainDb { 0.0f }; std::atomic<float> masterGainDb { 0.0f };
std::atomic<bool> limiterEnabled { true };
std::atomic<bool> autoMakeupEnabled { true };
float getActiveAutoMakeupDb() const;
float getMeasuredAutoMakeupDb() const { return measuredMakeupDb.load(); }
// Chain order (read/write from GUI, read from audio thread)
std::array<ChainStage, numChainStages> getChainOrder() const;
void setChainOrder (const std::array<ChainStage, numChainStages>& order);
void setQuality (int fftOrder); void setQuality (int fftOrder);
@@ -55,11 +68,35 @@ private:
FIREngine firEngine; FIREngine firEngine;
juce::dsp::Convolution convolution; juce::dsp::Convolution convolution;
juce::dsp::Limiter<float> limiter;
// Spectrum analyzer
static constexpr int spectrumFFTOrder = 11; // 2048-point FFT
static constexpr int spectrumFFTSize = 1 << spectrumFFTOrder;
juce::dsp::FFT spectrumFFT { spectrumFFTOrder };
juce::dsp::WindowingFunction<float> spectrumWindow { spectrumFFTSize, juce::dsp::WindowingFunction<float>::hann };
std::array<float, spectrumFFTSize> fifoBuffer {};
int fifoIndex = 0;
std::array<float, spectrumFFTSize * 2> fftData {};
std::array<float, spectrumFFTSize / 2> spectrumMagnitude {};
juce::SpinLock spectrumLock;
std::atomic<bool> spectrumReady { false };
public:
bool getSpectrum (float* dest, int maxBins) const;
double currentSampleRate = 44100.0; double currentSampleRate = 44100.0;
int currentBlockSize = 512; int currentBlockSize = 512;
bool firLoaded = false; bool firLoaded = false;
// Signal-based auto makeup measurement
double smoothedInputRms = 0.0;
double smoothedOutputRms = 0.0;
std::atomic<float> measuredMakeupDb { 0.0f };
std::array<ChainStage, numChainStages> chainOrder { MasterGain, Limiter, MakeupGain };
juce::SpinLock chainLock;
void updateFIR(); void updateFIR();
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaLPEQProcessor) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaLPEQProcessor)

165
Source/SignalChainPanel.cpp Normal file
Fájl megtekintése

@@ -0,0 +1,165 @@
#include "SignalChainPanel.h"
#include "LookAndFeel.h"
SignalChainPanel::SignalChainPanel()
{
setMouseCursor (juce::MouseCursor::DraggingHandCursor);
}
void SignalChainPanel::setOrder (const std::array<InstaLPEQProcessor::ChainStage, numBlocks>& order)
{
if (currentOrder != order)
{
currentOrder = order;
repaint();
}
}
juce::String SignalChainPanel::getStageName (InstaLPEQProcessor::ChainStage stage) const
{
switch (stage)
{
case InstaLPEQProcessor::MasterGain: return "MASTER GAIN";
case InstaLPEQProcessor::Limiter: return "LIMITER";
case InstaLPEQProcessor::MakeupGain: return "AUTO GAIN";
default: return "?";
}
}
juce::Colour SignalChainPanel::getStageColour (InstaLPEQProcessor::ChainStage stage) const
{
switch (stage)
{
case InstaLPEQProcessor::MasterGain: return juce::Colour (0xffff8833);
case InstaLPEQProcessor::Limiter: return juce::Colour (0xffff4455);
case InstaLPEQProcessor::MakeupGain: return juce::Colour (0xff44bbff);
default: return InstaLPEQLookAndFeel::textSecondary;
}
}
juce::Rectangle<float> SignalChainPanel::getBlockRect (int index) const
{
auto bounds = getLocalBounds().toFloat().reduced (2);
float gap = 6.0f;
float blockW = (bounds.getWidth() - gap * (numBlocks - 1)) / numBlocks;
float x = bounds.getX() + index * (blockW + gap);
return { x, bounds.getY(), blockW, bounds.getHeight() };
}
int SignalChainPanel::getBlockAtX (float x) const
{
for (int i = 0; i < numBlocks; ++i)
{
if (getBlockRect (i).contains (x, getHeight() * 0.5f))
return i;
}
return -1;
}
void SignalChainPanel::paint (juce::Graphics& g)
{
auto bounds = getLocalBounds().toFloat();
// Background
g.setColour (InstaLPEQLookAndFeel::bgDark.darker (0.3f));
g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (InstaLPEQLookAndFeel::bgLight.withAlpha (0.2f));
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
auto* lf = dynamic_cast<InstaLPEQLookAndFeel*> (&getLookAndFeel());
// Draw arrows between blocks (scale with height)
float arrowScale = bounds.getHeight() / 30.0f;
for (int i = 0; i < numBlocks - 1; ++i)
{
auto r1 = getBlockRect (i);
auto r2 = getBlockRect (i + 1);
float arrowX = (r1.getRight() + r2.getX()) * 0.5f;
float arrowY = bounds.getCentreY();
float aw = 5.0f * arrowScale;
float ah = 6.0f * arrowScale;
g.setColour (InstaLPEQLookAndFeel::textSecondary.withAlpha (0.5f));
juce::Path arrow;
arrow.addTriangle (arrowX - aw, arrowY - ah, arrowX - aw, arrowY + ah, arrowX + aw, arrowY);
g.fillPath (arrow);
}
// Draw blocks
for (int i = 0; i < numBlocks; ++i)
{
bool isDragged = (i == draggedIndex);
auto rect = getBlockRect (i);
// If this block is being dragged, offset it
if (isDragged)
{
float offset = dragCurrentX - dragOffsetX;
rect = rect.withX (rect.getX() + offset);
}
auto colour = getStageColour (currentOrder[i]);
// Block background
g.setColour (isDragged ? colour.withAlpha (0.25f) : colour.withAlpha (0.12f));
g.fillRoundedRectangle (rect, 4.0f);
// Block border
g.setColour (isDragged ? colour.withAlpha (0.8f) : colour.withAlpha (0.4f));
g.drawRoundedRectangle (rect, 4.0f, isDragged ? 2.0f : 1.0f);
// Label — scale with block height
juce::Font font = lf ? lf->getBoldFont (std::max (12.0f, rect.getHeight() * 0.45f))
: juce::Font (juce::FontOptions (14.0f));
g.setFont (font);
g.setColour (isDragged ? colour : colour.withAlpha (0.8f));
g.drawText (getStageName (currentOrder[i]), rect.reduced (4), juce::Justification::centred, false);
}
// "SIGNAL CHAIN" label on the left
if (lf)
{
g.setFont (lf->getRegularFont (10.0f));
g.setColour (InstaLPEQLookAndFeel::textSecondary.withAlpha (0.5f));
}
}
void SignalChainPanel::resized() {}
void SignalChainPanel::mouseDown (const juce::MouseEvent& e)
{
draggedIndex = getBlockAtX (e.position.x);
if (draggedIndex >= 0)
{
dragOffsetX = e.position.x;
dragCurrentX = e.position.x;
}
}
void SignalChainPanel::mouseDrag (const juce::MouseEvent& e)
{
if (draggedIndex < 0)
return;
dragCurrentX = e.position.x;
// Check if we should swap with a neighbor
int targetIndex = getBlockAtX (e.position.x);
if (targetIndex >= 0 && targetIndex != draggedIndex)
{
std::swap (currentOrder[draggedIndex], currentOrder[targetIndex]);
draggedIndex = targetIndex;
dragOffsetX = e.position.x;
if (listener)
listener->chainOrderChanged (currentOrder);
}
repaint();
}
void SignalChainPanel::mouseUp (const juce::MouseEvent&)
{
draggedIndex = -1;
repaint();
}

46
Source/SignalChainPanel.h Normal file
Fájl megtekintése

@@ -0,0 +1,46 @@
#pragma once
#include <JuceHeader.h>
#include "PluginProcessor.h"
class SignalChainPanel : public juce::Component
{
public:
struct Listener
{
virtual ~Listener() = default;
virtual void chainOrderChanged (const std::array<InstaLPEQProcessor::ChainStage, InstaLPEQProcessor::numChainStages>& order) = 0;
};
SignalChainPanel();
void setListener (Listener* l) { listener = l; }
void setOrder (const std::array<InstaLPEQProcessor::ChainStage, InstaLPEQProcessor::numChainStages>& order);
std::array<InstaLPEQProcessor::ChainStage, InstaLPEQProcessor::numChainStages> getOrder() const { return currentOrder; }
void paint (juce::Graphics& g) override;
void resized() override;
void mouseDown (const juce::MouseEvent& e) override;
void mouseDrag (const juce::MouseEvent& e) override;
void mouseUp (const juce::MouseEvent& e) override;
private:
static constexpr int numBlocks = InstaLPEQProcessor::numChainStages;
std::array<InstaLPEQProcessor::ChainStage, numBlocks> currentOrder {
InstaLPEQProcessor::MasterGain,
InstaLPEQProcessor::Limiter,
InstaLPEQProcessor::MakeupGain
};
int draggedIndex = -1;
float dragOffsetX = 0.0f;
float dragCurrentX = 0.0f;
Listener* listener = nullptr;
juce::Rectangle<float> getBlockRect (int index) const;
int getBlockAtX (float x) const;
juce::String getStageName (InstaLPEQProcessor::ChainStage stage) const;
juce::Colour getStageColour (InstaLPEQProcessor::ChainStage stage) const;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SignalChainPanel)
};