Commitok összehasonlítása
7 Commit-ok
| Szerző | SHA1 | Dátum | |
|---|---|---|---|
|
|
35cf01a163 | ||
|
|
aa546c7357 | ||
|
|
9c5b5a3957 | ||
|
|
72c7958d98 | ||
|
|
2c440d8deb | ||
|
|
f95c6e4b17 | ||
|
|
6a5f331185 |
31
.github/workflows/build.yml
vendored
31
.github/workflows/build.yml
vendored
@@ -25,21 +25,12 @@ jobs:
|
|||||||
- name: Package VST3
|
- name: Package VST3
|
||||||
run: Compress-Archive -Path "build/InstaLPEQ_artefacts/Release/VST3/InstaLPEQ.vst3" -DestinationPath "InstaLPEQ-VST3-Win64.zip"
|
run: Compress-Archive -Path "build/InstaLPEQ_artefacts/Release/VST3/InstaLPEQ.vst3" -DestinationPath "InstaLPEQ-VST3-Win64.zip"
|
||||||
|
|
||||||
- name: Package Standalone
|
|
||||||
run: Compress-Archive -Path "build/InstaLPEQ_artefacts/Release/Standalone/InstaLPEQ.exe" -DestinationPath "InstaLPEQ-Standalone-Win64.zip"
|
|
||||||
|
|
||||||
- name: Upload VST3
|
- name: Upload VST3
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: InstaLPEQ-VST3-Win64
|
name: InstaLPEQ-VST3-Win64
|
||||||
path: InstaLPEQ-VST3-Win64.zip
|
path: InstaLPEQ-VST3-Win64.zip
|
||||||
|
|
||||||
- name: Upload Standalone
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: InstaLPEQ-Standalone-Win64
|
|
||||||
path: InstaLPEQ-Standalone-Win64.zip
|
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -62,10 +53,6 @@ jobs:
|
|||||||
working-directory: build/InstaLPEQ_artefacts/Release
|
working-directory: build/InstaLPEQ_artefacts/Release
|
||||||
run: zip -r $GITHUB_WORKSPACE/InstaLPEQ-AU-macOS.zip AU/InstaLPEQ.component
|
run: zip -r $GITHUB_WORKSPACE/InstaLPEQ-AU-macOS.zip AU/InstaLPEQ.component
|
||||||
|
|
||||||
- name: Package Standalone
|
|
||||||
working-directory: build/InstaLPEQ_artefacts/Release
|
|
||||||
run: zip -r $GITHUB_WORKSPACE/InstaLPEQ-Standalone-macOS.zip Standalone/InstaLPEQ.app
|
|
||||||
|
|
||||||
- name: Upload VST3
|
- name: Upload VST3
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -78,12 +65,6 @@ jobs:
|
|||||||
name: InstaLPEQ-AU-macOS
|
name: InstaLPEQ-AU-macOS
|
||||||
path: InstaLPEQ-AU-macOS.zip
|
path: InstaLPEQ-AU-macOS.zip
|
||||||
|
|
||||||
- name: Upload Standalone
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: InstaLPEQ-Standalone-macOS
|
|
||||||
path: InstaLPEQ-Standalone-macOS.zip
|
|
||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
@@ -113,9 +94,6 @@ jobs:
|
|||||||
working-directory: build/InstaLPEQ_artefacts/Release
|
working-directory: build/InstaLPEQ_artefacts/Release
|
||||||
run: zip -r $GITHUB_WORKSPACE/InstaLPEQ-LV2-Linux-x64.zip LV2/InstaLPEQ.lv2
|
run: zip -r $GITHUB_WORKSPACE/InstaLPEQ-LV2-Linux-x64.zip LV2/InstaLPEQ.lv2
|
||||||
|
|
||||||
- name: Package Standalone
|
|
||||||
run: zip -j InstaLPEQ-Standalone-Linux-x64.zip build/InstaLPEQ_artefacts/Release/Standalone/InstaLPEQ
|
|
||||||
|
|
||||||
- name: Upload VST3
|
- name: Upload VST3
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -128,12 +106,6 @@ jobs:
|
|||||||
name: InstaLPEQ-LV2-Linux-x64
|
name: InstaLPEQ-LV2-Linux-x64
|
||||||
path: InstaLPEQ-LV2-Linux-x64.zip
|
path: InstaLPEQ-LV2-Linux-x64.zip
|
||||||
|
|
||||||
- name: Upload Standalone
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: InstaLPEQ-Standalone-Linux-x64
|
|
||||||
path: InstaLPEQ-Standalone-Linux-x64.zip
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
needs: [build-windows, build-macos, build-linux]
|
needs: [build-windows, build-macos, build-linux]
|
||||||
@@ -151,11 +123,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
artifacts/InstaLPEQ-VST3-Win64/InstaLPEQ-VST3-Win64.zip
|
artifacts/InstaLPEQ-VST3-Win64/InstaLPEQ-VST3-Win64.zip
|
||||||
artifacts/InstaLPEQ-Standalone-Win64/InstaLPEQ-Standalone-Win64.zip
|
|
||||||
artifacts/InstaLPEQ-VST3-macOS/InstaLPEQ-VST3-macOS.zip
|
artifacts/InstaLPEQ-VST3-macOS/InstaLPEQ-VST3-macOS.zip
|
||||||
artifacts/InstaLPEQ-AU-macOS/InstaLPEQ-AU-macOS.zip
|
artifacts/InstaLPEQ-AU-macOS/InstaLPEQ-AU-macOS.zip
|
||||||
artifacts/InstaLPEQ-Standalone-macOS/InstaLPEQ-Standalone-macOS.zip
|
|
||||||
artifacts/InstaLPEQ-VST3-Linux-x64/InstaLPEQ-VST3-Linux-x64.zip
|
artifacts/InstaLPEQ-VST3-Linux-x64/InstaLPEQ-VST3-Linux-x64.zip
|
||||||
artifacts/InstaLPEQ-LV2-Linux-x64/InstaLPEQ-LV2-Linux-x64.zip
|
artifacts/InstaLPEQ-LV2-Linux-x64/InstaLPEQ-LV2-Linux-x64.zip
|
||||||
artifacts/InstaLPEQ-Standalone-Linux-x64/InstaLPEQ-Standalone-Linux-x64.zip
|
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.22)
|
cmake_minimum_required(VERSION 3.22)
|
||||||
project(InstaLPEQ VERSION 1.0.0)
|
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)
|
||||||
@@ -13,7 +13,7 @@ juce_add_plugin(InstaLPEQ
|
|||||||
NEEDS_MIDI_OUTPUT FALSE
|
NEEDS_MIDI_OUTPUT FALSE
|
||||||
PLUGIN_MANUFACTURER_CODE Inst
|
PLUGIN_MANUFACTURER_CODE Inst
|
||||||
PLUGIN_CODE Ilpe
|
PLUGIN_CODE Ilpe
|
||||||
FORMATS VST3 AU LV2 Standalone
|
FORMATS VST3 AU LV2
|
||||||
LV2URI "https://github.com/hariel1985/InstaLPEQ"
|
LV2URI "https://github.com/hariel1985/InstaLPEQ"
|
||||||
PRODUCT_NAME "InstaLPEQ"
|
PRODUCT_NAME "InstaLPEQ"
|
||||||
COPY_PLUGIN_AFTER_BUILD FALSE
|
COPY_PLUGIN_AFTER_BUILD FALSE
|
||||||
@@ -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
|
||||||
|
|||||||
150
README.md
150
README.md
@@ -1,32 +1,45 @@
|
|||||||
# InstaLPEQ
|
# InstaLPEQ
|
||||||
|
|
||||||
Free, open-source linear phase EQ plugin built with JUCE. Available as VST3, AU, LV2 and Standalone.
|
Free, open-source linear phase EQ plugin built with JUCE. Available as VST3, AU and LV2.
|
||||||
|
|
||||||
      
|
      
|
||||||
|
|
||||||
|
## 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.0](https://github.com/hariel1985/InstaLPEQ/releases/tag/v1.0)**
|
**[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.0/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\` |
|
||||||
| [InstaLPEQ-Standalone-Win64.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.0/InstaLPEQ-Standalone-Win64.zip) | Standalone application |
|
|
||||||
|
|
||||||
### 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.0/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.0/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/` |
|
||||||
| [InstaLPEQ-Standalone-macOS.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.0/InstaLPEQ-Standalone-macOS.zip) | Standalone application |
|
|
||||||
|
|
||||||
### 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.0/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.0/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/` |
|
||||||
| [InstaLPEQ-Standalone-Linux-x64.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.0/InstaLPEQ-Standalone-Linux-x64.zip) | Standalone application |
|
|
||||||
|
|
||||||
> **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
|
||||||
@@ -36,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
|
||||||
|
|
||||||
@@ -107,24 +174,11 @@ Output:
|
|||||||
- VST3: `build/InstaLPEQ_artefacts/Release/VST3/InstaLPEQ.vst3`
|
- VST3: `build/InstaLPEQ_artefacts/Release/VST3/InstaLPEQ.vst3`
|
||||||
- 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`
|
||||||
- Standalone: `build/InstaLPEQ_artefacts/Release/Standalone/InstaLPEQ.exe`
|
|
||||||
|
|
||||||
## 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)
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ void FIREngine::setBands (const std::vector<EQBand>& newBands)
|
|||||||
|
|
||||||
void FIREngine::setFFTOrder (int order)
|
void FIREngine::setFFTOrder (int order)
|
||||||
{
|
{
|
||||||
fftOrder.store (juce::jlimit (12, 14, order));
|
fftOrder.store (juce::jlimit (9, 14, order));
|
||||||
needsUpdate.store (true);
|
needsUpdate.store (true);
|
||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
class FIREngine : private juce::Thread
|
class FIREngine : private juce::Thread
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static constexpr int defaultFFTOrder = 13; // 8192 taps
|
static constexpr int defaultFFTOrder = 11; // 2048 taps
|
||||||
static constexpr int maxBands = 8;
|
static constexpr int maxBands = 8;
|
||||||
|
|
||||||
FIREngine();
|
FIREngine();
|
||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -23,6 +23,47 @@ InstaLPEQEditor::InstaLPEQEditor (InstaLPEQProcessor& p)
|
|||||||
bypassLabel.setColour (juce::Label::textColourId, InstaLPEQLookAndFeel::textSecondary);
|
bypassLabel.setColour (juce::Label::textColourId, InstaLPEQLookAndFeel::textSecondary);
|
||||||
addAndMakeVisible (bypassLabel);
|
addAndMakeVisible (bypassLabel);
|
||||||
|
|
||||||
|
// New Band button
|
||||||
|
newBandButton.onClick = [this]
|
||||||
|
{
|
||||||
|
if (processor.getNumBands() < InstaLPEQProcessor::maxBands)
|
||||||
|
{
|
||||||
|
processor.addBand (1000.0f, 0.0f);
|
||||||
|
syncDisplayFromProcessor();
|
||||||
|
curveDisplay.setSelectedBand (processor.getNumBands() - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addAndMakeVisible (newBandButton);
|
||||||
|
|
||||||
|
// Quality selector (FIR latency)
|
||||||
|
qualitySelector.addItem ("512 (~6ms)", 1);
|
||||||
|
qualitySelector.addItem ("1024 (~12ms)", 2);
|
||||||
|
qualitySelector.addItem ("2048 (~23ms)", 3);
|
||||||
|
qualitySelector.addItem ("4096 (~46ms)", 4);
|
||||||
|
qualitySelector.addItem ("8192 (~93ms)", 5);
|
||||||
|
qualitySelector.addItem ("16384 (~186ms)", 6);
|
||||||
|
qualitySelector.setSelectedId (3, juce::dontSendNotification); // default 2048
|
||||||
|
qualitySelector.onChange = [this]
|
||||||
|
{
|
||||||
|
int sel = qualitySelector.getSelectedId();
|
||||||
|
int order = sel + 8; // 1->9, 2->10, 3->11, 4->12, 5->13, 6->14
|
||||||
|
processor.setQuality (order);
|
||||||
|
|
||||||
|
if (sel <= 2) // 512 or 1024
|
||||||
|
qualityWarning.setText ("Low freq accuracy reduced", juce::dontSendNotification);
|
||||||
|
else
|
||||||
|
qualityWarning.setText ("", juce::dontSendNotification);
|
||||||
|
};
|
||||||
|
addAndMakeVisible (qualitySelector);
|
||||||
|
qualityLabel.setFont (customLookAndFeel.getMediumFont (13.0f));
|
||||||
|
qualityLabel.setJustificationType (juce::Justification::centredRight);
|
||||||
|
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);
|
||||||
@@ -37,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);
|
||||||
@@ -102,18 +169,41 @@ void InstaLPEQEditor::resized()
|
|||||||
bypassLabel.setBounds (bypassArea.removeFromLeft (50));
|
bypassLabel.setBounds (bypassArea.removeFromLeft (50));
|
||||||
bypassToggle.setBounds (bypassArea);
|
bypassToggle.setBounds (bypassArea);
|
||||||
|
|
||||||
// Bottom master row
|
newBandButton.setBounds (header.removeFromRight ((int) (90 * scale)).reduced (2));
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
// Divider above master
|
|
||||||
// (painted in paint())
|
|
||||||
|
|
||||||
masterGainLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale)));
|
masterGainLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale)));
|
||||||
auto labelArea = masterArea.removeFromLeft (60);
|
auto labelArea = masterArea.removeFromLeft (60);
|
||||||
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
|
||||||
|
qualityLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale)));
|
||||||
|
auto qLabelArea = masterArea.removeFromRight (30);
|
||||||
|
qualityLabel.setBounds (qLabelArea);
|
||||||
|
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);
|
||||||
auto nodePanelArea = bounds.removeFromBottom (nodePanelH).reduced (pad, 2);
|
auto nodePanelArea = bounds.removeFromBottom (nodePanelH).reduced (pad, 2);
|
||||||
@@ -126,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();
|
||||||
@@ -204,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();
|
||||||
|
|||||||
@@ -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,12 +44,24 @@ private:
|
|||||||
NodeParameterPanel nodePanel;
|
NodeParameterPanel nodePanel;
|
||||||
|
|
||||||
juce::Label titleLabel { {}, "INSTALPEQ" };
|
juce::Label titleLabel { {}, "INSTALPEQ" };
|
||||||
juce::Label versionLabel { {}, "v1.0" };
|
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::ComboBox qualitySelector;
|
||||||
|
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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
std::array<ChainStage, numChainStages> order;
|
||||||
|
{
|
||||||
|
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());
|
float gain = juce::Decibels::decibelsToGain (masterGainDb.load());
|
||||||
if (std::abs (gain - 1.0f) > 0.001f)
|
if (std::abs (gain - 1.0f) > 0.001f)
|
||||||
buffer.applyGain (gain);
|
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,12 +196,50 @@ 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();
|
||||||
firEngine.setBands (currentBands);
|
firEngine.setBands (currentBands);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InstaLPEQProcessor::setQuality (int fftOrder)
|
||||||
|
{
|
||||||
|
firEngine.setFFTOrder (fftOrder);
|
||||||
|
setLatencySamples (firEngine.getLatencySamples());
|
||||||
|
updateFIR();
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// State save/restore
|
// State save/restore
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -134,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)
|
||||||
@@ -157,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())
|
||||||
|
|||||||
@@ -40,9 +40,24 @@ 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);
|
||||||
|
|
||||||
FIREngine& getFIREngine() { return firEngine; }
|
FIREngine& getFIREngine() { return firEngine; }
|
||||||
double getCurrentSampleRate() const { return currentSampleRate; }
|
double getCurrentSampleRate() const { return currentSampleRate; }
|
||||||
@@ -53,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
165
Source/SignalChainPanel.cpp
Normal file
@@ -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
46
Source/SignalChainPanel.h
Normal file
@@ -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)
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user