Commitok összehasonlítása
7 Commit-ok
| Szerző | SHA1 | Dátum | |
|---|---|---|---|
|
|
417680ca58 | ||
|
|
db47dd4b3c | ||
|
|
31deac4e88 | ||
|
|
eab1a739f2 | ||
|
|
d750716608 | ||
|
|
1c8b8012f6 | ||
|
|
0237b4fc27 |
116
.gitea/workflows/build.yml
Normal file
116
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
name: Build InstaShadow
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ['v*']
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
PLUGIN_NAME: InstaShadow
|
||||||
|
GITEA_URL: https://1git.eu
|
||||||
|
GITEA_REPO: hariel/InstaShadow
|
||||||
|
|
||||||
|
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)
|
cmake_minimum_required(VERSION 3.22)
|
||||||
project(InstaShadow VERSION 1.0.0)
|
project(InstaShadow VERSION 1.1.0)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|||||||
205
README.md
205
README.md
@@ -1,33 +1,210 @@
|
|||||||
# InstaShadow
|
# InstaShadow
|
||||||
|
|
||||||
Dual-stage mastering compressor plugin (VST3/AU/LV2) inspired by the Shadow Hills Mastering Compressor, built with JUCE.
|
Free, open-source dual-stage mastering compressor plugin inspired by the Shadow Hills Mastering Compressor, built with JUCE. Available as VST3, AU and LV2.
|
||||||
|
|
||||||
## Features
|
      
|
||||||
|
|
||||||
- **Optical Compressor** — Port-Hamiltonian T4B opto-cell model with physically accurate two-stage release and CdS memory effect
|

|
||||||
- **Discrete VCA Compressor** — Feed-forward Class-A topology with 7 ratio settings (1.2:1 to Flood), 6 attack/release presets, and Dual release mode
|
|
||||||
- **Output Transformer** — 3 switchable transformer types (Nickel/Iron/Steel) with frequency-dependent saturation and 4x oversampling
|
## What Is This?
|
||||||
- **Sidechain HPF** — Variable 20-500 Hz high-pass filter to prevent bass-induced pumping
|
|
||||||
- **Stereo Link** — Linked or dual-mono operation
|
InstaShadow is a dual-stage mastering compressor that combines an **optical compressor** and a **discrete VCA compressor** in series, followed by a switchable **output transformer** saturation section. The design is inspired by the Shadow Hills Mastering Compressor — a legendary hardware unit used in professional mastering studios worldwide.
|
||||||
- **Independent bypass** — Each section can be bypassed separately
|
|
||||||
- **GR Metering** — Dedicated optical and discrete gain reduction meters
|
The optical stage provides smooth, program-dependent compression with a natural two-stage release, while the VCA stage offers precise, fast compression with selectable ratio, attack, and release settings. The transformer section adds subtle harmonic coloration with three distinct characters.
|
||||||
- **State save/restore** — All parameters persist with DAW session
|
|
||||||
|
## Download
|
||||||
|
|
||||||
|
**[Latest Release: v1.1](https://github.com/hariel1985/InstaShadow/releases/tag/v1.1)**
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| [InstaShadow-VST3-Win64.zip](https://github.com/hariel1985/InstaShadow/releases/download/v1.1/InstaShadow-VST3-Win64.zip) | VST3 plugin — copy to `C:\Program Files\Common Files\VST3\` |
|
||||||
|
|
||||||
|
### macOS (Universal Binary: Apple Silicon + Intel)
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| [InstaShadow-VST3-macOS.zip](https://github.com/hariel1985/InstaShadow/releases/download/v1.1/InstaShadow-VST3-macOS.zip) | VST3 plugin — copy to `~/Library/Audio/Plug-Ins/VST3/` |
|
||||||
|
| [InstaShadow-AU-macOS.zip](https://github.com/hariel1985/InstaShadow/releases/download/v1.1/InstaShadow-AU-macOS.zip) | Audio Unit — copy to `~/Library/Audio/Plug-Ins/Components/` |
|
||||||
|
|
||||||
|
### Linux (x64, built on Ubuntu 22.04)
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| [InstaShadow-VST3-Linux-x64.zip](https://github.com/hariel1985/InstaShadow/releases/download/v1.1/InstaShadow-VST3-Linux-x64.zip) | VST3 plugin — copy to `~/.vst3/` |
|
||||||
|
| [InstaShadow-LV2-Linux-x64.zip](https://github.com/hariel1985/InstaShadow/releases/download/v1.1/InstaShadow-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
|
||||||
|
> xattr -cr ~/Library/Audio/Plug-Ins/VST3/InstaShadow.vst3
|
||||||
|
> xattr -cr ~/Library/Audio/Plug-Ins/Components/InstaShadow.component
|
||||||
|
> ```
|
||||||
|
|
||||||
## Signal Flow
|
## Signal Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
Input → SC HPF → Optical Comp (T4B) → VCA Comp → Transformer → Output
|
Input → Sidechain HPF → Optical Compressor (T4B) → VCA Compressor → Transformer → Output
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build
|
Each stage can be independently bypassed. The sidechain HPF prevents low-frequency energy from triggering excessive compression (pumping on bass-heavy material).
|
||||||
|
|
||||||
Requires [JUCE](https://github.com/juce-framework/JUCE) cloned at `../JUCE` relative to this project.
|
## Features
|
||||||
|
|
||||||
|
### Optical Compressor — Port-Hamiltonian T4B Model
|
||||||
|
|
||||||
|
The optical stage physically models the electro-optical attenuator (T4B) found in classic hardware compressors like the LA-2A. Rather than using simplified envelope followers, InstaShadow implements a **Port-Hamiltonian** energy-based model:
|
||||||
|
|
||||||
|
- **EL panel** modeled as a capacitive energy store — the audio signal charges the panel, which emits light proportional to stored energy
|
||||||
|
- **CdS photoresistor** modeled as a nonlinear dissipator — resistance follows the gamma curve `R = k · L^(-γ)` where γ ≈ 0.7
|
||||||
|
- **Implicit trapezoidal integration** with Newton-Raphson iteration (3-5 iterations per sample) for numerical stability
|
||||||
|
- **2x oversampling** for the implicit solver
|
||||||
|
- **CdS memory effect** — the photoresistor "remembers" past illumination, creating a natural two-stage release:
|
||||||
|
- Fast phase (~60 ms): first 50-80% of gain reduction releases quickly
|
||||||
|
- Slow phase (0.5-5 s): remaining recovery depends on how long and how hard the signal was compressed
|
||||||
|
- **Fixed 2:1 ratio** and **soft knee** emerge naturally from the physics — not explicitly coded
|
||||||
|
- **Program-dependent attack** (~10 ms average) — reacts differently to transients vs. sustained signals
|
||||||
|
|
||||||
|
| Control | Range | Default |
|
||||||
|
|---------|-------|---------|
|
||||||
|
| Threshold | -40 to 0 dB | -20 dB |
|
||||||
|
| Gain (makeup) | 0 to 20 dB | 0 dB |
|
||||||
|
| Sidechain HPF | 20 to 500 Hz | 90 Hz |
|
||||||
|
| Bypass | On/Off | Off |
|
||||||
|
|
||||||
|
### Discrete VCA Compressor
|
||||||
|
|
||||||
|
A feed-forward VCA compressor with precise, repeatable compression characteristics:
|
||||||
|
|
||||||
|
- **Soft-knee** gain computer (6 dB knee width) for transparent threshold behavior
|
||||||
|
- **7 ratio settings:** 1.2:1, 2:1, 3:1, 4:1, 6:1, 10:1, Flood (20:1)
|
||||||
|
- **6 attack presets:** 0.1 ms, 0.5 ms, 1 ms, 5 ms, 10 ms, 30 ms
|
||||||
|
- **6 release presets:** 100 ms, 250 ms, 500 ms, 800 ms, 1.2 s, **Dual**
|
||||||
|
- **Dual release mode** mimics the optical stage's two-stage behavior within the VCA:
|
||||||
|
- Fast release envelope (~60 ms) handles the initial recovery
|
||||||
|
- Slow release envelope (~2 s) handles the tail
|
||||||
|
- The deeper (more compressed) of the two envelopes is used at any given moment
|
||||||
|
|
||||||
|
| Control | Range | Default |
|
||||||
|
|---------|-------|---------|
|
||||||
|
| Threshold | -40 to 0 dB | -20 dB |
|
||||||
|
| Gain (makeup) | 0 to 20 dB | 0 dB |
|
||||||
|
| Ratio | 1.2:1 — Flood | 2:1 |
|
||||||
|
| Attack | 0.1 ms — 30 ms | 1 ms |
|
||||||
|
| Release | 100 ms — Dual | 500 ms |
|
||||||
|
| Bypass | On/Off | Off |
|
||||||
|
|
||||||
|
### Output Transformer Saturation
|
||||||
|
|
||||||
|
Three switchable transformer types add subtle harmonic coloration, modeled with 4x oversampled waveshaping:
|
||||||
|
|
||||||
|
| Type | Character | Harmonics | Drive | Wet Mix |
|
||||||
|
|------|-----------|-----------|-------|---------|
|
||||||
|
| **Nickel** | Transparent, clean | Minimal | 1.05 | 30% |
|
||||||
|
| **Iron** | Warm, musical | Even-order (2nd) | 1.15 | 50% |
|
||||||
|
| **Steel** | Aggressive, present | Even + odd (2nd + 3rd) | 1.3 | 60% |
|
||||||
|
|
||||||
|
- Waveshaping: `tanh(drive · x) / drive` — preserves unity gain at low levels
|
||||||
|
- Even harmonics via `x · |x|` (asymmetric warmth)
|
||||||
|
- Odd harmonics via `x³` (edge and presence)
|
||||||
|
- Dry/wet blending keeps the effect subtle and musical
|
||||||
|
- Iron adds a +0.2 dB low shelf at 110 Hz
|
||||||
|
- Steel adds a +0.4 dB low shelf at 40 Hz
|
||||||
|
- 4x oversampling (JUCE polyphase IIR) prevents aliasing artifacts
|
||||||
|
|
||||||
|
### Metering
|
||||||
|
|
||||||
|
- **Analog-style needle VU meters** (L/R) with ballistic needle movement — cream-colored face, scale markings from -20 to +3 dB, red zone above 0 dB
|
||||||
|
- **Optical GR meter** — horizontal bar showing optical stage gain reduction
|
||||||
|
- **Discrete GR meter** — horizontal bar showing VCA stage gain reduction
|
||||||
|
- **Output VU meter** — vertical stereo bar meter in the output section
|
||||||
|
|
||||||
|
### Global Controls
|
||||||
|
|
||||||
|
| Control | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| Stereo Link | Links L/R sidechain for matched stereo compression. Off = dual-mono |
|
||||||
|
| Bypass | Global bypass — passes audio unprocessed |
|
||||||
|
| Output Gain | -12 to +12 dB final output level |
|
||||||
|
|
||||||
|
### GUI
|
||||||
|
|
||||||
|
- Layout inspired by the original Shadow Hills Mastering Compressor hardware
|
||||||
|
- Optical controls on the left, discrete controls on the right, meters in the center
|
||||||
|
- Transformer and output controls at the bottom center
|
||||||
|
- Dark modern UI with InstaDrums/InstaGrain visual style
|
||||||
|
- 3D metal knobs with multi-layer glow effects (orange for main controls, blue for sidechain HPF)
|
||||||
|
- Analog needle VU meters with inertial needle movement
|
||||||
|
- Carbon fiber background texture
|
||||||
|
- Rajdhani custom font (embedded)
|
||||||
|
- Fully resizable window (800×500 — 1400×900) with proportional scaling
|
||||||
|
- State save/restore — all settings recalled with DAW session
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### The Port-Hamiltonian Approach
|
||||||
|
|
||||||
|
Traditional plugin compressors use simplified envelope followers with fixed attack/release time constants. This misses the complex, program-dependent behavior of real optical compressors.
|
||||||
|
|
||||||
|
InstaShadow uses a **Port-Hamiltonian** formulation — an energy-based modeling framework from mathematical physics. The system is described by two coupled energy ports:
|
||||||
|
|
||||||
|
1. **Port 1 (EL panel):** A capacitive energy store with Hamiltonian `H = q²/(2C)`. The audio signal drives charge into the capacitor, which converts electrical energy to light.
|
||||||
|
|
||||||
|
2. **Port 2 (CdS cell):** A nonlinear dissipative element whose resistance depends on illumination via a gamma curve. A separate "memory" state variable tracks accumulated illumination history, creating the characteristic two-stage release.
|
||||||
|
|
||||||
|
The coupled system is solved using **implicit trapezoidal integration** — a symplectic integrator that preserves the energy structure of the Hamiltonian. Newton-Raphson iteration (3-5 steps per sample) resolves the implicit equation at each time step. 2x oversampling ensures numerical stability.
|
||||||
|
|
||||||
|
This approach naturally produces:
|
||||||
|
- Program-dependent attack and release (emerges from the physics)
|
||||||
|
- Soft-knee compression (emerges from the nonlinear CdS gamma curve)
|
||||||
|
- Approximately 2:1 ratio (emerges from the voltage divider topology)
|
||||||
|
- Two-stage release with memory effect (emerges from the CdS illumination history)
|
||||||
|
|
||||||
|
None of these behaviors are explicitly programmed — they are consequences of the physical model.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- CMake 3.22+
|
||||||
|
- JUCE framework (cloned to `../JUCE` relative to project)
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
- Visual Studio 2022 Build Tools (C++ workload)
|
||||||
|
|
||||||
|
#### macOS
|
||||||
|
- Xcode 14+
|
||||||
|
|
||||||
|
#### Linux (Ubuntu 22.04+)
|
||||||
|
```bash
|
||||||
|
sudo apt-get install build-essential cmake git libasound2-dev \
|
||||||
|
libfreetype6-dev libx11-dev libxrandr-dev libxcursor-dev \
|
||||||
|
libxinerama-dev libwebkit2gtk-4.1-dev libcurl4-openssl-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Steps
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cmake -B build -G "Visual Studio 17 2022" -A x64
|
git clone https://github.com/juce-framework/JUCE.git ../JUCE
|
||||||
|
cmake -B build -G "Visual Studio 17 2022" -A x64 # Windows
|
||||||
|
cmake -B build -G Xcode # macOS
|
||||||
|
cmake -B build -DCMAKE_BUILD_TYPE=Release # Linux
|
||||||
cmake --build build --config Release
|
cmake --build build --config Release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
- VST3: `build/InstaShadow_artefacts/Release/VST3/InstaShadow.vst3`
|
||||||
|
- AU: `build/InstaShadow_artefacts/Release/AU/InstaShadow.component` (macOS)
|
||||||
|
- LV2: `build/InstaShadow_artefacts/Release/LV2/InstaShadow.lv2`
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Language:** C++17
|
||||||
|
- **Framework:** JUCE 8
|
||||||
|
- **Build:** CMake + MSVC / Xcode / GCC
|
||||||
|
- **Optical DSP:** Custom Port-Hamiltonian solver (implicit trapezoidal + Newton-Raphson)
|
||||||
|
- **VCA DSP:** Custom feed-forward compressor with soft-knee gain computer
|
||||||
|
- **Transformer DSP:** Custom waveshaping with `juce::dsp::Oversampling` (4x polyphase IIR)
|
||||||
|
- **Filters:** `juce::dsp::IIR` (sidechain HPF, tonestack EQ)
|
||||||
|
- **Font:** Rajdhani (SIL Open Font License)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL-3.0
|
GPL-3.0
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ void CompressorEngine::processBlock (juce::AudioBuffer<float>& buffer)
|
|||||||
|
|
||||||
if (globalBypass.load() || numChannels == 0) return;
|
if (globalBypass.load() || numChannels == 0) return;
|
||||||
|
|
||||||
|
// Measure input level BEFORE any processing
|
||||||
|
inputLevelL.store (buffer.getMagnitude (0, 0, numSamples));
|
||||||
|
if (numChannels > 1)
|
||||||
|
inputLevelR.store (buffer.getMagnitude (1, 0, numSamples));
|
||||||
|
else
|
||||||
|
inputLevelR.store (inputLevelL.load());
|
||||||
|
|
||||||
// Read parameters once per block
|
// Read parameters once per block
|
||||||
float optoThresh = optoThresholdDb.load();
|
float optoThresh = optoThresholdDb.load();
|
||||||
float optoGain = optoGainDb.load();
|
float optoGain = optoGainDb.load();
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ public:
|
|||||||
// --- Metering (audio → GUI) ---
|
// --- Metering (audio → GUI) ---
|
||||||
std::atomic<float> optoGrDb { 0.0f };
|
std::atomic<float> optoGrDb { 0.0f };
|
||||||
std::atomic<float> vcaGrDb { 0.0f };
|
std::atomic<float> vcaGrDb { 0.0f };
|
||||||
|
std::atomic<float> inputLevelL { 0.0f };
|
||||||
|
std::atomic<float> inputLevelR { 0.0f };
|
||||||
std::atomic<float> outputLevelL { 0.0f };
|
std::atomic<float> outputLevelL { 0.0f };
|
||||||
std::atomic<float> outputLevelR { 0.0f };
|
std::atomic<float> outputLevelR { 0.0f };
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,27 @@ class GRMeter : public juce::Component
|
|||||||
public:
|
public:
|
||||||
void setGainReduction (float grDb)
|
void setGainReduction (float grDb)
|
||||||
{
|
{
|
||||||
|
// 0dB GR = 0.0 (empty), -30dB GR = 1.0 (full bar)
|
||||||
|
// Bar fills from RIGHT to LEFT showing how much GR
|
||||||
float clamped = juce::jlimit (-30.0f, 0.0f, grDb);
|
float clamped = juce::jlimit (-30.0f, 0.0f, grDb);
|
||||||
float normalised = -clamped / 30.0f;
|
float normalised = -clamped / 30.0f; // 0dB→0.0, -30dB→1.0
|
||||||
currentGr = std::max (normalised, currentGr * 0.92f);
|
currentLevel = std::max (normalised, currentLevel * 0.92f);
|
||||||
if (normalised > peakGr) peakGr = normalised;
|
if (normalised > peakLevel) peakLevel = normalised;
|
||||||
else peakGr *= 0.998f;
|
else peakLevel *= 0.998f;
|
||||||
|
leftToRight = false; // right-to-left
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input level meter (left-to-right, linear level 0..1)
|
||||||
|
void setInputLevel (float linearLevel)
|
||||||
|
{
|
||||||
|
float db = (linearLevel > 0.0001f) ? 20.0f * std::log10 (linearLevel) : -60.0f;
|
||||||
|
// Map -30..0 dB to 0..1
|
||||||
|
float normalised = juce::jlimit (0.0f, 1.0f, (db + 30.0f) / 30.0f);
|
||||||
|
currentLevel = std::max (normalised, currentLevel * 0.92f);
|
||||||
|
if (normalised > peakLevel) peakLevel = normalised;
|
||||||
|
else peakLevel *= 0.998f;
|
||||||
|
leftToRight = true;
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,22 +37,28 @@ public:
|
|||||||
{
|
{
|
||||||
auto bounds = getLocalBounds().toFloat().reduced (1);
|
auto bounds = getLocalBounds().toFloat().reduced (1);
|
||||||
|
|
||||||
// Background
|
|
||||||
g.setColour (juce::Colour (0xff111122));
|
g.setColour (juce::Colour (0xff111122));
|
||||||
g.fillRoundedRectangle (bounds, 2.0f);
|
g.fillRoundedRectangle (bounds, 2.0f);
|
||||||
|
|
||||||
// GR bar (fills from right to left)
|
float w = bounds.getWidth() * currentLevel;
|
||||||
float w = bounds.getWidth() * currentGr;
|
|
||||||
auto filled = bounds.withLeft (bounds.getRight() - w);
|
juce::Rectangle<float> filled;
|
||||||
|
if (leftToRight)
|
||||||
|
filled = bounds.withWidth (w); // left to right for input level
|
||||||
|
else
|
||||||
|
filled = bounds.withLeft (bounds.getRight() - w); // right to left for GR
|
||||||
|
|
||||||
g.setColour (barColour);
|
g.setColour (barColour);
|
||||||
g.fillRoundedRectangle (filled, 2.0f);
|
g.fillRoundedRectangle (filled, 2.0f);
|
||||||
|
|
||||||
// Peak hold line
|
// Peak hold line
|
||||||
if (peakGr > 0.01f)
|
if (peakLevel > 0.01f)
|
||||||
{
|
{
|
||||||
float peakX = bounds.getRight() - bounds.getWidth() * peakGr;
|
float peakX = leftToRight
|
||||||
|
? bounds.getX() + bounds.getWidth() * peakLevel
|
||||||
|
: bounds.getRight() - bounds.getWidth() * peakLevel;
|
||||||
g.setColour (juce::Colours::white.withAlpha (0.8f));
|
g.setColour (juce::Colours::white.withAlpha (0.8f));
|
||||||
g.fillRect (peakX, bounds.getY(), 1.5f, bounds.getHeight());
|
g.fillRect (peakX - 0.75f, bounds.getY(), 1.5f, bounds.getHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Label
|
// Label
|
||||||
@@ -45,15 +67,20 @@ public:
|
|||||||
g.drawText (label, bounds.reduced (4, 0), juce::Justification::centredLeft);
|
g.drawText (label, bounds.reduced (4, 0), juce::Justification::centredLeft);
|
||||||
|
|
||||||
// dB readout
|
// dB readout
|
||||||
float dbVal = -currentGr * 30.0f;
|
if (currentLevel > 0.001f)
|
||||||
if (currentGr > 0.001f)
|
{
|
||||||
|
float dbVal = leftToRight
|
||||||
|
? (currentLevel * 30.0f - 30.0f) // input: -30..0 dB
|
||||||
|
: (-currentLevel * 30.0f); // GR: 0..-30 dB
|
||||||
g.drawText (juce::String (dbVal, 1) + " dB", bounds.reduced (4, 0),
|
g.drawText (juce::String (dbVal, 1) + " dB", bounds.reduced (4, 0),
|
||||||
juce::Justification::centredRight);
|
juce::Justification::centredRight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
float currentGr = 0.0f;
|
float currentLevel = 0.0f;
|
||||||
float peakGr = 0.0f;
|
float peakLevel = 0.0f;
|
||||||
|
bool leftToRight = false;
|
||||||
juce::Colour barColour { 0xffff8833 };
|
juce::Colour barColour { 0xffff8833 };
|
||||||
juce::String label;
|
juce::String label;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,28 +2,31 @@
|
|||||||
#include <JuceHeader.h>
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Analog-style needle VU meter (semicircular, like Shadow Hills)
|
// Analog-style needle meter (semicircular)
|
||||||
|
// Two modes: VU (level) and GR (gain reduction)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
class NeedleVuMeter : public juce::Component
|
class NeedleVuMeter : public juce::Component
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
enum Mode { VU, GR };
|
||||||
|
|
||||||
|
void setMode (Mode m) { mode = m; repaint(); }
|
||||||
|
|
||||||
void setLevel (float linearLevel)
|
void setLevel (float linearLevel)
|
||||||
{
|
{
|
||||||
// Convert to dB, map to needle position
|
|
||||||
float db = (linearLevel > 0.0001f)
|
float db = (linearLevel > 0.0001f)
|
||||||
? 20.0f * std::log10 (linearLevel)
|
? 20.0f * std::log10 (linearLevel)
|
||||||
: -60.0f;
|
: -60.0f;
|
||||||
|
|
||||||
// VU range: -20 to +3 dB → 0.0 to 1.0
|
|
||||||
float target = juce::jlimit (0.0f, 1.0f, (db + 20.0f) / 23.0f);
|
float target = juce::jlimit (0.0f, 1.0f, (db + 20.0f) / 23.0f);
|
||||||
|
applyNeedlePhysics (target);
|
||||||
|
}
|
||||||
|
|
||||||
// Smooth needle movement (ballistic)
|
// For GR mode: pass negative dB value (e.g. -6.0 = 6dB reduction)
|
||||||
if (target > needlePos)
|
// Standard VU scale, needle rests at 0dB mark, moves LEFT with compression
|
||||||
needlePos += (target - needlePos) * 0.07f; // slow attack (inertia)
|
void setGainReduction (float grDb)
|
||||||
else
|
{
|
||||||
needlePos += (target - needlePos) * 0.05f; // moderate release
|
float target = juce::jlimit (0.0f, 1.0f, (grDb + 20.0f) / 23.0f);
|
||||||
|
applyNeedlePhysics (target);
|
||||||
repaint();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLabel (const juce::String& text) { label = text; }
|
void setLabel (const juce::String& text) { label = text; }
|
||||||
@@ -31,13 +34,12 @@ public:
|
|||||||
void paint (juce::Graphics& g) override
|
void paint (juce::Graphics& g) override
|
||||||
{
|
{
|
||||||
auto bounds = getLocalBounds().toFloat().reduced (2);
|
auto bounds = getLocalBounds().toFloat().reduced (2);
|
||||||
float w = bounds.getWidth();
|
|
||||||
float h = bounds.getHeight();
|
float h = bounds.getHeight();
|
||||||
|
|
||||||
// Meter face background (warm cream)
|
|
||||||
float arcH = h * 0.85f;
|
float arcH = h * 0.85f;
|
||||||
auto faceRect = bounds.withHeight (arcH);
|
auto faceRect = bounds.withHeight (arcH);
|
||||||
|
|
||||||
|
// Dark background
|
||||||
g.setColour (juce::Colour (0xff1a1a22));
|
g.setColour (juce::Colour (0xff1a1a22));
|
||||||
g.fillRoundedRectangle (bounds, 4.0f);
|
g.fillRoundedRectangle (bounds, 4.0f);
|
||||||
|
|
||||||
@@ -50,97 +52,165 @@ public:
|
|||||||
g.fillRoundedRectangle (arcArea, 3.0f);
|
g.fillRoundedRectangle (arcArea, 3.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arc center point (bottom center of arc area)
|
|
||||||
float cx = arcArea.getCentreX();
|
float cx = arcArea.getCentreX();
|
||||||
float cy = arcArea.getBottom() - 4.0f;
|
float cy = arcArea.getBottom() - 4.0f;
|
||||||
float radius = std::min (arcArea.getWidth() * 0.45f, arcArea.getHeight() * 0.8f);
|
float radius = std::min (arcArea.getWidth() * 0.45f, arcArea.getHeight() * 0.8f);
|
||||||
|
|
||||||
// Scale markings
|
float startAngle = juce::MathConstants<float>::pi * 1.25f;
|
||||||
float startAngle = juce::MathConstants<float>::pi * 1.25f; // -225 deg
|
float endAngle = juce::MathConstants<float>::pi * 1.75f;
|
||||||
float endAngle = juce::MathConstants<float>::pi * 1.75f; // -315 deg (sweep right)
|
|
||||||
|
|
||||||
// Draw scale ticks and labels
|
|
||||||
g.setFont (std::max (6.0f, h * 0.045f));
|
g.setFont (std::max (6.0f, h * 0.045f));
|
||||||
const float dbValues[] = { -20, -10, -7, -5, -3, -1, 0, 1, 2, 3 };
|
|
||||||
const int numTicks = 10;
|
|
||||||
|
|
||||||
for (int i = 0; i < numTicks; ++i)
|
// Always use VU scale — in GR mode the needle just starts at 0 and goes left
|
||||||
|
drawVuScale (g, cx, cy, radius, startAngle, endAngle);
|
||||||
|
|
||||||
|
// Needle with leaf-shaped arrowhead
|
||||||
{
|
{
|
||||||
float norm = (dbValues[i] + 20.0f) / 23.0f;
|
float angle = startAngle + needlePos * (endAngle - startAngle);
|
||||||
float angle = startAngle + norm * (endAngle - startAngle);
|
|
||||||
|
|
||||||
float cosA = std::cos (angle);
|
float cosA = std::cos (angle);
|
||||||
float sinA = std::sin (angle);
|
float sinA = std::sin (angle);
|
||||||
|
|
||||||
float innerR = radius * 0.82f;
|
float tipX = cx + cosA * radius * 0.88f;
|
||||||
float outerR = radius * 0.95f;
|
float tipY = cy + sinA * radius * 0.88f;
|
||||||
|
|
||||||
|
// Needle shadow
|
||||||
|
g.setColour (juce::Colours::black.withAlpha (0.25f));
|
||||||
|
g.drawLine (cx + 1, cy + 1, tipX + 1, tipY + 1, 1.5f);
|
||||||
|
|
||||||
|
// Needle shaft (thin line from pivot to base of arrowhead)
|
||||||
|
float shaftEnd = radius * 0.65f;
|
||||||
|
float shaftX = cx + cosA * shaftEnd;
|
||||||
|
float shaftY = cy + sinA * shaftEnd;
|
||||||
|
g.setColour (juce::Colour (0xff222222));
|
||||||
|
g.drawLine (cx, cy, shaftX, shaftY, 1.2f);
|
||||||
|
|
||||||
|
// Leaf-shaped arrowhead (elongated diamond from shaft end to tip)
|
||||||
|
float leafW = radius * 0.035f; // half-width of leaf
|
||||||
|
float perpX = -sinA; // perpendicular to needle direction
|
||||||
|
float perpY = cosA;
|
||||||
|
|
||||||
|
juce::Path leaf;
|
||||||
|
leaf.startNewSubPath (shaftX, shaftY); // base (narrow)
|
||||||
|
leaf.lineTo (cx + cosA * radius * 0.76f + perpX * leafW,
|
||||||
|
cy + sinA * radius * 0.76f + perpY * leafW); // left bulge
|
||||||
|
leaf.lineTo (tipX, tipY); // tip (narrow)
|
||||||
|
leaf.lineTo (cx + cosA * radius * 0.76f - perpX * leafW,
|
||||||
|
cy + sinA * radius * 0.76f - perpY * leafW); // right bulge
|
||||||
|
leaf.closeSubPath();
|
||||||
|
|
||||||
|
// Shadow
|
||||||
|
g.setColour (juce::Colours::black.withAlpha (0.2f));
|
||||||
|
g.fillPath (leaf, juce::AffineTransform::translation (0.5f, 0.5f));
|
||||||
|
|
||||||
|
// Fill
|
||||||
|
g.setColour (juce::Colour (0xff111111));
|
||||||
|
g.fillPath (leaf);
|
||||||
|
|
||||||
|
// Pivot dot
|
||||||
|
g.setColour (juce::Colour (0xff333333));
|
||||||
|
g.fillEllipse (cx - 3, cy - 3, 6, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label
|
||||||
|
g.setColour (juce::Colour (0xffaaaaaa));
|
||||||
|
g.setFont (std::max (7.0f, h * 0.05f));
|
||||||
|
g.drawText (label, bounds.getX(), bounds.getBottom() - h * 0.18f,
|
||||||
|
bounds.getWidth(), h * 0.15f, juce::Justification::centred);
|
||||||
|
|
||||||
|
g.setColour (juce::Colour (0xff333344));
|
||||||
|
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Mode mode = VU;
|
||||||
|
float needlePos = 0.0f;
|
||||||
|
float needleVelocity = 0.0f;
|
||||||
|
juce::String label;
|
||||||
|
|
||||||
|
void applyNeedlePhysics (float target)
|
||||||
|
{
|
||||||
|
// VU mode: heavier needle, more damping (lazy, smooth movement)
|
||||||
|
// GR mode: lighter needle, less damping (responsive to compression changes)
|
||||||
|
float spring = (mode == VU) ? 0.12f : 0.35f;
|
||||||
|
float damping = (mode == VU) ? 0.70f : 0.55f;
|
||||||
|
|
||||||
|
float force = spring * (target - needlePos);
|
||||||
|
needleVelocity = needleVelocity * (1.0f - damping) + force;
|
||||||
|
needlePos += needleVelocity;
|
||||||
|
needlePos = juce::jlimit (0.0f, 1.05f, needlePos);
|
||||||
|
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawVuScale (juce::Graphics& g, float cx, float cy, float radius,
|
||||||
|
float startAngle, float endAngle)
|
||||||
|
{
|
||||||
|
const float dbValues[] = { -20, -10, -7, -5, -3, -1, 0, 1, 2, 3 };
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
{
|
||||||
|
float norm = (dbValues[i] + 20.0f) / 23.0f;
|
||||||
|
float angle = startAngle + norm * (endAngle - startAngle);
|
||||||
|
float cosA = std::cos (angle), sinA = std::sin (angle);
|
||||||
|
|
||||||
|
float innerR = radius * 0.82f, outerR = radius * 0.95f;
|
||||||
bool isMajor = (dbValues[i] == -20 || dbValues[i] == -10 || dbValues[i] == -5
|
bool isMajor = (dbValues[i] == -20 || dbValues[i] == -10 || dbValues[i] == -5
|
||||||
|| dbValues[i] == 0 || dbValues[i] == 3);
|
|| dbValues[i] == 0 || dbValues[i] == 3);
|
||||||
|
|
||||||
// Tick line
|
|
||||||
g.setColour (dbValues[i] >= 0 ? juce::Colour (0xffcc3333) : juce::Colour (0xff333333));
|
g.setColour (dbValues[i] >= 0 ? juce::Colour (0xffcc3333) : juce::Colour (0xff333333));
|
||||||
float tickInner = isMajor ? innerR * 0.9f : innerR;
|
g.drawLine (cx + cosA * (isMajor ? innerR * 0.9f : innerR), cy + sinA * (isMajor ? innerR * 0.9f : innerR),
|
||||||
g.drawLine (cx + cosA * tickInner, cy + sinA * tickInner,
|
cx + cosA * outerR, cy + sinA * outerR, isMajor ? 1.5f : 0.8f);
|
||||||
cx + cosA * outerR, cy + sinA * outerR,
|
|
||||||
isMajor ? 1.5f : 0.8f);
|
|
||||||
|
|
||||||
// Label for major ticks
|
|
||||||
if (isMajor)
|
if (isMajor)
|
||||||
{
|
{
|
||||||
float labelR = radius * 0.7f;
|
float lx = cx + cosA * radius * 0.7f, ly = cy + sinA * radius * 0.7f;
|
||||||
float lx = cx + cosA * labelR;
|
|
||||||
float ly = cy + sinA * labelR;
|
|
||||||
juce::String txt = (dbValues[i] > 0 ? "+" : "") + juce::String ((int) dbValues[i]);
|
juce::String txt = (dbValues[i] > 0 ? "+" : "") + juce::String ((int) dbValues[i]);
|
||||||
g.setColour (dbValues[i] >= 0 ? juce::Colour (0xffcc3333) : juce::Colour (0xff444444));
|
g.setColour (dbValues[i] >= 0 ? juce::Colour (0xffcc3333) : juce::Colour (0xff444444));
|
||||||
g.drawText (txt, (int) (lx - 12), (int) (ly - 6), 24, 12, juce::Justification::centred);
|
g.drawText (txt, (int) (lx - 12), (int) (ly - 6), 24, 12, juce::Justification::centred);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Red zone arc (0 to +3 dB)
|
// Red zone arc
|
||||||
{
|
float redStart = startAngle + (20.0f / 23.0f) * (endAngle - startAngle);
|
||||||
float redStart = startAngle + (20.0f / 23.0f) * (endAngle - startAngle);
|
juce::Path redArc;
|
||||||
juce::Path redArc;
|
redArc.addCentredArc (cx, cy, radius * 0.92f, radius * 0.92f, 0, redStart, endAngle, true);
|
||||||
redArc.addCentredArc (cx, cy, radius * 0.92f, radius * 0.92f, 0,
|
g.setColour (juce::Colour (0x33ff3333));
|
||||||
redStart, endAngle, true);
|
g.strokePath (redArc, juce::PathStrokeType (radius * 0.08f));
|
||||||
g.setColour (juce::Colour (0x33ff3333));
|
|
||||||
g.strokePath (redArc, juce::PathStrokeType (radius * 0.08f));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Needle
|
|
||||||
{
|
|
||||||
float angle = startAngle + needlePos * (endAngle - startAngle);
|
|
||||||
float cosA = std::cos (angle);
|
|
||||||
float sinA = std::sin (angle);
|
|
||||||
|
|
||||||
// Needle shadow
|
|
||||||
g.setColour (juce::Colours::black.withAlpha (0.3f));
|
|
||||||
g.drawLine (cx + 1, cy + 1,
|
|
||||||
cx + cosA * radius * 0.88f + 1, cy + sinA * radius * 0.88f + 1,
|
|
||||||
2.0f);
|
|
||||||
|
|
||||||
// Needle
|
|
||||||
g.setColour (juce::Colour (0xff222222));
|
|
||||||
g.drawLine (cx, cy,
|
|
||||||
cx + cosA * radius * 0.88f, cy + sinA * radius * 0.88f,
|
|
||||||
1.5f);
|
|
||||||
|
|
||||||
// Needle pivot dot
|
|
||||||
g.setColour (juce::Colour (0xff333333));
|
|
||||||
g.fillEllipse (cx - 3, cy - 3, 6, 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Label below
|
|
||||||
g.setColour (juce::Colour (0xffaaaaaa));
|
|
||||||
g.setFont (std::max (7.0f, h * 0.05f));
|
|
||||||
g.drawText (label, bounds.getX(), bounds.getBottom() - h * 0.18f,
|
|
||||||
bounds.getWidth(), h * 0.15f, juce::Justification::centred);
|
|
||||||
|
|
||||||
// Border
|
|
||||||
g.setColour (juce::Colour (0xff333344));
|
|
||||||
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
void drawGrScale (juce::Graphics& g, float cx, float cy, float radius,
|
||||||
float needlePos = 0.0f; // 0..1 mapped to -20..+3 dB
|
float startAngle, float endAngle)
|
||||||
juce::String label;
|
{
|
||||||
|
// GR scale: 0 (left, rest) to -20 (right, max compression)
|
||||||
|
const float grValues[] = { 0, -2, -4, -6, -8, -10, -14, -20 };
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; ++i)
|
||||||
|
{
|
||||||
|
float norm = -grValues[i] / 20.0f; // 0→0.0, -20→1.0
|
||||||
|
float angle = startAngle + norm * (endAngle - startAngle);
|
||||||
|
float cosA = std::cos (angle), sinA = std::sin (angle);
|
||||||
|
|
||||||
|
float innerR = radius * 0.82f, outerR = radius * 0.95f;
|
||||||
|
bool isMajor = (grValues[i] == 0 || grValues[i] == -6 || grValues[i] == -10 || grValues[i] == -20);
|
||||||
|
|
||||||
|
g.setColour (grValues[i] <= -10 ? juce::Colour (0xffcc3333) : juce::Colour (0xff333333));
|
||||||
|
g.drawLine (cx + cosA * (isMajor ? innerR * 0.9f : innerR), cy + sinA * (isMajor ? innerR * 0.9f : innerR),
|
||||||
|
cx + cosA * outerR, cy + sinA * outerR, isMajor ? 1.5f : 0.8f);
|
||||||
|
|
||||||
|
if (isMajor)
|
||||||
|
{
|
||||||
|
float lx = cx + cosA * radius * 0.7f, ly = cy + sinA * radius * 0.7f;
|
||||||
|
juce::String txt = juce::String ((int) grValues[i]);
|
||||||
|
g.setColour (grValues[i] <= -10 ? juce::Colour (0xffcc3333) : juce::Colour (0xff444444));
|
||||||
|
g.drawText (txt, (int) (lx - 12), (int) (ly - 6), 24, 12, juce::Justification::centred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning zone arc (-10 to -20 dB GR)
|
||||||
|
float warnStart = startAngle + (10.0f / 20.0f) * (endAngle - startAngle);
|
||||||
|
juce::Path warnArc;
|
||||||
|
warnArc.addCentredArc (cx, cy, radius * 0.92f, radius * 0.92f, 0, warnStart, endAngle, true);
|
||||||
|
g.setColour (juce::Colour (0x33ff3333));
|
||||||
|
g.strokePath (warnArc, juce::PathStrokeType (radius * 0.08f));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,20 +41,50 @@ InstaShadowEditor::InstaShadowEditor (InstaShadowProcessor& p)
|
|||||||
addAndMakeVisible (transformerPanel);
|
addAndMakeVisible (transformerPanel);
|
||||||
addAndMakeVisible (outputPanel);
|
addAndMakeVisible (outputPanel);
|
||||||
|
|
||||||
// Needle VU meters
|
// Needle meters (default: GR)
|
||||||
vuMeterL.setLabel ("L");
|
needleMeterL.setLabel ("OPTICAL GR");
|
||||||
addAndMakeVisible (vuMeterL);
|
needleMeterL.setMode (NeedleVuMeter::GR);
|
||||||
vuMeterR.setLabel ("R");
|
addAndMakeVisible (needleMeterL);
|
||||||
addAndMakeVisible (vuMeterR);
|
needleMeterR.setLabel ("DISCRETE GR");
|
||||||
|
needleMeterR.setMode (NeedleVuMeter::GR);
|
||||||
|
addAndMakeVisible (needleMeterR);
|
||||||
|
|
||||||
// GR meters (compact bars)
|
// Bar meters (default: input level)
|
||||||
optoGrMeter.setLabel ("OPTICAL GR");
|
barMeterL.setLabel ("INPUT L");
|
||||||
optoGrMeter.setBarColour (juce::Colour (0xffff8833));
|
barMeterL.setBarColour (juce::Colour (0xff00cc44));
|
||||||
addAndMakeVisible (optoGrMeter);
|
addAndMakeVisible (barMeterL);
|
||||||
|
barMeterR.setLabel ("INPUT R");
|
||||||
|
barMeterR.setBarColour (juce::Colour (0xff00cc44));
|
||||||
|
addAndMakeVisible (barMeterR);
|
||||||
|
|
||||||
vcaGrMeter.setLabel ("DISCRETE GR");
|
// Meter swap button
|
||||||
vcaGrMeter.setBarColour (juce::Colour (0xff4488ff));
|
meterSwapButton.onClick = [this]
|
||||||
addAndMakeVisible (vcaGrMeter);
|
{
|
||||||
|
metersSwapped = ! metersSwapped;
|
||||||
|
if (metersSwapped)
|
||||||
|
{
|
||||||
|
needleMeterL.setLabel ("INPUT L");
|
||||||
|
needleMeterL.setMode (NeedleVuMeter::VU);
|
||||||
|
needleMeterR.setLabel ("INPUT R");
|
||||||
|
needleMeterR.setMode (NeedleVuMeter::VU);
|
||||||
|
barMeterL.setLabel ("OPTICAL GR");
|
||||||
|
barMeterL.setBarColour (juce::Colour (0xffff8833));
|
||||||
|
barMeterR.setLabel ("DISCRETE GR");
|
||||||
|
barMeterR.setBarColour (juce::Colour (0xff4488ff));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
needleMeterL.setLabel ("OPTICAL GR");
|
||||||
|
needleMeterL.setMode (NeedleVuMeter::GR);
|
||||||
|
needleMeterR.setLabel ("DISCRETE GR");
|
||||||
|
needleMeterR.setMode (NeedleVuMeter::GR);
|
||||||
|
barMeterL.setLabel ("INPUT L");
|
||||||
|
barMeterL.setBarColour (juce::Colour (0xff00cc44));
|
||||||
|
barMeterR.setLabel ("INPUT R");
|
||||||
|
barMeterR.setBarColour (juce::Colour (0xff00cc44));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addAndMakeVisible (meterSwapButton);
|
||||||
|
|
||||||
syncKnobsToEngine();
|
syncKnobsToEngine();
|
||||||
startTimerHz (30);
|
startTimerHz (30);
|
||||||
@@ -116,13 +146,22 @@ void InstaShadowEditor::timerCallback()
|
|||||||
|
|
||||||
auto& eng = processor.getEngine();
|
auto& eng = processor.getEngine();
|
||||||
|
|
||||||
// Needle VU meters
|
if (! metersSwapped)
|
||||||
vuMeterL.setLevel (eng.outputLevelL.load());
|
{
|
||||||
vuMeterR.setLevel (eng.outputLevelR.load());
|
// Default: needles = GR, bars = input
|
||||||
|
needleMeterL.setGainReduction (eng.optoGrDb.load());
|
||||||
// GR meters
|
needleMeterR.setGainReduction (eng.vcaGrDb.load());
|
||||||
optoGrMeter.setGainReduction (eng.optoGrDb.load());
|
barMeterL.setInputLevel (eng.inputLevelL.load());
|
||||||
vcaGrMeter.setGainReduction (eng.vcaGrDb.load());
|
barMeterR.setInputLevel (eng.inputLevelR.load());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Swapped: needles = input, bars = GR
|
||||||
|
needleMeterL.setLevel (eng.inputLevelL.load());
|
||||||
|
needleMeterR.setLevel (eng.inputLevelR.load());
|
||||||
|
barMeterL.setGainReduction (eng.optoGrDb.load());
|
||||||
|
barMeterR.setGainReduction (eng.vcaGrDb.load());
|
||||||
|
}
|
||||||
|
|
||||||
// Output panel VU
|
// Output panel VU
|
||||||
outputPanel.vuMeter.setLevel (eng.outputLevelL.load(), eng.outputLevelR.load());
|
outputPanel.vuMeter.setLevel (eng.outputLevelL.load(), eng.outputLevelR.load());
|
||||||
@@ -185,20 +224,24 @@ void InstaShadowEditor::resized()
|
|||||||
// Center column: VU meters, GR bars, Transformer, Output — all stacked
|
// Center column: VU meters, GR bars, Transformer, Output — all stacked
|
||||||
auto centerArea = mainRow;
|
auto centerArea = mainRow;
|
||||||
|
|
||||||
// Two needle VU meters side by side (~30%)
|
// Two needle meters side by side (~30%)
|
||||||
int vuH = (int) (centerArea.getHeight() * 0.30f);
|
int vuH = (int) (centerArea.getHeight() * 0.30f);
|
||||||
auto vuRow = centerArea.removeFromTop (vuH);
|
auto needleRow = centerArea.removeFromTop (vuH);
|
||||||
int vuW = (vuRow.getWidth() - pad) / 2;
|
int needleW = (needleRow.getWidth() - pad) / 2;
|
||||||
vuMeterL.setBounds (vuRow.removeFromLeft (vuW));
|
needleMeterL.setBounds (needleRow.removeFromLeft (needleW));
|
||||||
vuRow.removeFromLeft (pad);
|
needleRow.removeFromLeft (pad);
|
||||||
vuMeterR.setBounds (vuRow);
|
needleMeterR.setBounds (needleRow);
|
||||||
centerArea.removeFromTop (pad);
|
centerArea.removeFromTop (pad);
|
||||||
|
|
||||||
// Two GR meter bars (~15%)
|
// Swap button (compact, between needles and bars)
|
||||||
int grBarH = (int) (centerArea.getHeight() * 0.12f);
|
meterSwapButton.setBounds (centerArea.removeFromTop (20).reduced (centerArea.getWidth() / 4, 0));
|
||||||
optoGrMeter.setBounds (centerArea.removeFromTop (grBarH));
|
|
||||||
centerArea.removeFromTop (pad);
|
centerArea.removeFromTop (pad);
|
||||||
vcaGrMeter.setBounds (centerArea.removeFromTop (grBarH));
|
|
||||||
|
// Two bar meters (~10%)
|
||||||
|
int barH = (int) (centerArea.getHeight() * 0.10f);
|
||||||
|
barMeterL.setBounds (centerArea.removeFromTop (barH));
|
||||||
|
centerArea.removeFromTop (pad);
|
||||||
|
barMeterR.setBounds (centerArea.removeFromTop (barH));
|
||||||
centerArea.removeFromTop (pad);
|
centerArea.removeFromTop (pad);
|
||||||
|
|
||||||
// Transformer + Output side by side in remaining center space
|
// Transformer + Output side by side in remaining center space
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "GRMeter.h"
|
#include "GRMeter.h"
|
||||||
#include "NeedleVuMeter.h"
|
#include "NeedleVuMeter.h"
|
||||||
|
|
||||||
static constexpr const char* kInstaShadowVersion = "v1.0";
|
static constexpr const char* kInstaShadowVersion = "v1.1";
|
||||||
|
|
||||||
class InstaShadowEditor : public juce::AudioProcessorEditor,
|
class InstaShadowEditor : public juce::AudioProcessorEditor,
|
||||||
public juce::Timer
|
public juce::Timer
|
||||||
@@ -38,11 +38,15 @@ private:
|
|||||||
OpticalPanel opticalPanel;
|
OpticalPanel opticalPanel;
|
||||||
DiscretePanel discretePanel;
|
DiscretePanel discretePanel;
|
||||||
|
|
||||||
// Center: needle VU meters + GR bars
|
// Center: needle meters + bar meters (swappable)
|
||||||
NeedleVuMeter vuMeterL;
|
NeedleVuMeter needleMeterL;
|
||||||
NeedleVuMeter vuMeterR;
|
NeedleVuMeter needleMeterR;
|
||||||
GRMeter optoGrMeter;
|
GRMeter barMeterL;
|
||||||
GRMeter vcaGrMeter;
|
GRMeter barMeterR;
|
||||||
|
|
||||||
|
// Meter swap toggle
|
||||||
|
juce::TextButton meterSwapButton { "GR / INPUT" };
|
||||||
|
bool metersSwapped = false; // false: needle=GR, bar=input | true: needle=input, bar=GR
|
||||||
|
|
||||||
// Bottom panels
|
// Bottom panels
|
||||||
TransformerPanel transformerPanel;
|
TransformerPanel transformerPanel;
|
||||||
|
|||||||
BINáris
screenshot.png
Normal file
BINáris
screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Méret: 540 KiB |
Reference in New Issue
Block a user