Commitok összehasonlítása
4 Commit-ok
| Szerző | SHA1 | Dátum | |
|---|---|---|---|
|
|
8652740bcf | ||
|
|
def8dd4d98 | ||
|
|
35cf01a163 | ||
|
|
aa546c7357 |
116
.gitea/workflows/build.yml
Normal file
116
.gitea/workflows/build.yml
Normal file
@@ -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
|
||||
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
project(InstaLPEQ VERSION 1.2.2)
|
||||
project(InstaLPEQ VERSION 1.3.2)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
144
README.md
144
README.md
@@ -4,26 +4,42 @@ Free, open-source linear phase EQ plugin built with JUCE. Available as VST3, AU
|
||||
|
||||
      
|
||||
|
||||
## 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
|
||||
|
||||
**[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
|
||||
| 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)
|
||||
| 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-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-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.3/InstaLPEQ-AU-macOS.zip) | Audio Unit — copy to `~/Library/Audio/Plug-Ins/Components/` |
|
||||
|
||||
### Linux (x64, built on Ubuntu 22.04)
|
||||
| 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-LV2-Linux-x64.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.1/InstaLPEQ-LV2-Linux-x64.zip) | LV2 plugin — copy to `~/.lv2/` |
|
||||
| [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.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:
|
||||
> ```bash
|
||||
@@ -33,43 +49,97 @@ Free, open-source linear phase EQ plugin built with JUCE. Available as VST3, AU
|
||||
|
||||
## Features
|
||||
|
||||
### Linear Phase EQ
|
||||
### Linear Phase EQ Engine
|
||||
- True linear phase processing using symmetric FIR convolution
|
||||
- Zero phase distortion at any gain setting
|
||||
- 8192-tap FIR filter (configurable: 4096 / 8192 / 16384)
|
||||
- DAW-compensated latency (~93ms at 44.1kHz default)
|
||||
- Background thread FIR generation — glitch-free parameter changes
|
||||
- Zero phase distortion — the waveform shape is perfectly preserved at any gain setting
|
||||
- Mathematically transparent: only magnitude changes, phase stays untouched
|
||||
- FIR impulse response normalized for unity passthrough (0 dB at flat settings)
|
||||
- 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
|
||||
- Logarithmic frequency axis (20 Hz — 20 kHz)
|
||||
- Linear gain axis (-24 dB to +24 dB)
|
||||
- Click to add EQ nodes (up to 8 bands)
|
||||
- Drag nodes to adjust frequency and gain
|
||||
- Scroll wheel to adjust Q/bandwidth
|
||||
- Right-click for band type selection and delete
|
||||
- Double-click to reset band to 0 dB
|
||||
- Real-time frequency response curve with glow effect
|
||||
- Per-band curve overlay
|
||||
- Click anywhere to add an EQ node (up to 8 bands)
|
||||
- Drag nodes to adjust frequency and gain in real time
|
||||
- Scroll wheel over a node to adjust Q/bandwidth
|
||||
- Right-click a node for band type selection or delete
|
||||
- Double-click a node to reset it to 0 dB
|
||||
- Combined frequency response curve with glow effect
|
||||
- Individual per-band curve overlays (color-coded)
|
||||
- Real-time FFT spectrum analyzer behind the EQ curves (shows live audio content)
|
||||
|
||||
### Band Types
|
||||
- Peak (parametric)
|
||||
- Low Shelf
|
||||
- High Shelf
|
||||
- **Peak** (parametric) — boost or cut a specific frequency range
|
||||
- **Low Shelf** — boost or cut everything below a frequency
|
||||
- **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
|
||||
- Per-band: Frequency, Gain, Q knobs
|
||||
- Master gain (+/- 24 dB)
|
||||
- Bypass toggle
|
||||
- State save/restore (DAW session recall)
|
||||
- **Per-band:** Frequency, Gain, Q knobs with 3D metal styling
|
||||
- **Master Gain:** +/- 24 dB output level control
|
||||
- **Bypass:** global bypass toggle
|
||||
- **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
|
||||
- Dark modern UI matching InstaDrums visual style
|
||||
- 3D metal knobs with glow effects (orange for EQ, blue for Q)
|
||||
- Dark modern UI with InstaDrums visual style
|
||||
- 3D metal knobs with multi-layer glow effects (orange for frequency/gain, blue for Q)
|
||||
- Carbon fiber background texture
|
||||
- Rajdhani custom font
|
||||
- Fully resizable window with proportional scaling
|
||||
- Animated toggle switches
|
||||
- Rajdhani custom font (embedded)
|
||||
- Fully resizable window (700x450 — 1920x1080) with proportional scaling
|
||||
- Animated toggle switches with smooth lerp
|
||||
- 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
|
||||
|
||||
@@ -105,22 +175,10 @@ Output:
|
||||
- AU: `build/InstaLPEQ_artefacts/Release/AU/InstaLPEQ.component` (macOS)
|
||||
- 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
|
||||
|
||||
- **Language:** C++17
|
||||
- **Framework:** JUCE 8
|
||||
- **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)
|
||||
|
||||
@@ -124,11 +124,11 @@ juce::AudioBuffer<float> FIREngine::generateFIR (const std::vector<EQBand>& band
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ public:
|
||||
int getFIRLength() const { return 1 << fftOrder.load(); }
|
||||
int getLatencySamples() const { return getFIRLength() / 2; }
|
||||
|
||||
// Auto makeup gain: A-weighted RMS loudness compensation (dB)
|
||||
float getAutoMakeupGainDb() const { return autoMakeupDb.load(); }
|
||||
|
||||
private:
|
||||
void run() override;
|
||||
juce::AudioBuffer<float> generateFIR (const std::vector<EQBand>& bands, double sr, int order);
|
||||
@@ -43,4 +46,7 @@ private:
|
||||
|
||||
std::vector<float> magnitudeDb;
|
||||
mutable juce::SpinLock magLock;
|
||||
|
||||
std::atomic<float> autoMakeupDb { 0.0f };
|
||||
static float aWeighting (float freq);
|
||||
};
|
||||
|
||||
@@ -92,17 +92,17 @@ InstaLPEQEditor::InstaLPEQEditor (InstaLPEQProcessor& p)
|
||||
limiterLabel.setJustificationType (juce::Justification::centred);
|
||||
addAndMakeVisible (limiterLabel);
|
||||
|
||||
// Makeup gain
|
||||
makeupGainSlider.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
|
||||
makeupGainSlider.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 60, 16);
|
||||
makeupGainSlider.setRange (-24.0, 24.0, 0.1);
|
||||
makeupGainSlider.setValue (0.0);
|
||||
makeupGainSlider.setTextValueSuffix (" dB");
|
||||
makeupGainSlider.setDoubleClickReturnValue (true, 0.0);
|
||||
addAndMakeVisible (makeupGainSlider);
|
||||
makeupGainLabel.setFont (customLookAndFeel.getMediumFont (13.0f));
|
||||
makeupGainLabel.setJustificationType (juce::Justification::centred);
|
||||
addAndMakeVisible (makeupGainLabel);
|
||||
// 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);
|
||||
@@ -189,10 +189,12 @@ void InstaLPEQEditor::resized()
|
||||
limiterLabel.setBounds (masterArea.removeFromLeft (55));
|
||||
limiterToggle.setBounds (masterArea.removeFromLeft (40));
|
||||
|
||||
// Makeup gain knob
|
||||
makeupGainLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale)));
|
||||
makeupGainLabel.setBounds (masterArea.removeFromLeft (55));
|
||||
makeupGainSlider.setBounds (masterArea.removeFromLeft (masterH));
|
||||
// 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)));
|
||||
@@ -218,7 +220,12 @@ void InstaLPEQEditor::timerCallback()
|
||||
processor.bypassed.store (bypassToggle.getToggleState());
|
||||
processor.masterGainDb.store ((float) masterGainSlider.getValue());
|
||||
processor.limiterEnabled.store (limiterToggle.getToggleState());
|
||||
processor.makeupGainDb.store ((float) makeupGainSlider.getValue());
|
||||
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
|
||||
{
|
||||
|
||||
@@ -44,7 +44,7 @@ private:
|
||||
NodeParameterPanel nodePanel;
|
||||
|
||||
juce::Label titleLabel { {}, "INSTALPEQ" };
|
||||
juce::Label versionLabel { {}, "v1.2.2" };
|
||||
juce::Label versionLabel { {}, "v1.3.2" };
|
||||
juce::ToggleButton bypassToggle;
|
||||
juce::Label bypassLabel { {}, "BYPASS" };
|
||||
|
||||
@@ -57,8 +57,9 @@ private:
|
||||
juce::Label masterGainLabel { {}, "MASTER" };
|
||||
juce::ToggleButton limiterToggle;
|
||||
juce::Label limiterLabel { {}, "LIMITER" };
|
||||
juce::Slider makeupGainSlider;
|
||||
juce::Label makeupGainLabel { {}, "MAKEUP" };
|
||||
juce::ToggleButton autoMakeupToggle;
|
||||
juce::Label autoMakeupLabel { {}, "AUTO GAIN" };
|
||||
juce::Label autoMakeupValue { {}, "0.0 dB" };
|
||||
|
||||
SignalChainPanel chainPanel;
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ void InstaLPEQProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::M
|
||||
if (bypassed.load() || ! firLoaded)
|
||||
return;
|
||||
|
||||
// Process through convolution
|
||||
// Process through convolution (EQ)
|
||||
juce::dsp::AudioBlock<float> block (buffer);
|
||||
juce::dsp::ProcessContextReplacing<float> context (block);
|
||||
convolution.process (context);
|
||||
@@ -99,9 +99,12 @@ void InstaLPEQProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::M
|
||||
}
|
||||
case MakeupGain:
|
||||
{
|
||||
float mkGain = juce::Decibels::decibelsToGain (makeupGainDb.load());
|
||||
if (autoMakeupEnabled.load())
|
||||
{
|
||||
float mkGain = juce::Decibels::decibelsToGain (firEngine.getAutoMakeupGainDb());
|
||||
if (std::abs (mkGain - 1.0f) > 0.001f)
|
||||
buffer.applyGain (mkGain);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
@@ -207,6 +210,11 @@ bool InstaLPEQProcessor::getSpectrum (float* dest, int maxBins) const
|
||||
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);
|
||||
@@ -242,7 +250,7 @@ void InstaLPEQProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||
xml.setAttribute ("bypass", bypassed.load());
|
||||
xml.setAttribute ("masterGain", (double) masterGainDb.load());
|
||||
xml.setAttribute ("limiter", limiterEnabled.load());
|
||||
xml.setAttribute ("makeupGain", (double) makeupGainDb.load());
|
||||
xml.setAttribute ("autoMakeup", autoMakeupEnabled.load());
|
||||
|
||||
auto order = getChainOrder();
|
||||
juce::String chainStr;
|
||||
@@ -276,7 +284,7 @@ void InstaLPEQProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||
bypassed.store (xml->getBoolAttribute ("bypass", false));
|
||||
masterGainDb.store ((float) xml->getDoubleAttribute ("masterGain", 0.0));
|
||||
limiterEnabled.store (xml->getBoolAttribute ("limiter", true));
|
||||
makeupGainDb.store ((float) xml->getDoubleAttribute ("makeupGain", 0.0));
|
||||
autoMakeupEnabled.store (xml->getBoolAttribute ("autoMakeup", true));
|
||||
|
||||
auto chainStr = xml->getStringAttribute ("chainOrder", "0,1,2");
|
||||
auto tokens = juce::StringArray::fromTokens (chainStr, ",", "");
|
||||
|
||||
@@ -48,7 +48,10 @@ public:
|
||||
std::atomic<bool> bypassed { false };
|
||||
std::atomic<float> masterGainDb { 0.0f };
|
||||
std::atomic<bool> limiterEnabled { true };
|
||||
std::atomic<float> makeupGainDb { 0.0f }; // -24 to +24 dB
|
||||
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;
|
||||
@@ -86,6 +89,11 @@ public:
|
||||
int currentBlockSize = 512;
|
||||
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;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ juce::String SignalChainPanel::getStageName (InstaLPEQProcessor::ChainStage stag
|
||||
{
|
||||
case InstaLPEQProcessor::MasterGain: return "MASTER GAIN";
|
||||
case InstaLPEQProcessor::Limiter: return "LIMITER";
|
||||
case InstaLPEQProcessor::MakeupGain: return "MAKEUP GAIN";
|
||||
case InstaLPEQProcessor::MakeupGain: return "AUTO GAIN";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user